jvm虚拟机入门

第一章:JVM-java虚拟机
jvm的内存模型
寄存器 堆 虚拟机栈 本地方法栈 方法区

01寄存器(程序计数器):
它的作用可以看做是当前线程所执行的字节码的行号指示器。

02 Java堆(线程共享)被所有线程共享的一块内存区域,在虚拟机启动的时候创建,用于存放对象实例。对可以按照可扩展来实现(通过-Xmx 和-Xms 来控制)当队中没有内存可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。

03 java 虚拟机栈
java 虚拟机栈也是线程私有的。
每个方法在执行的时候也会创建一个栈帧,存储了局部变量,操作数,动态链接,方法返回地址。
每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。
通常所说的栈,一般是指在虚拟机栈中的局部变量部分。
局部变量所需内存在编译期间完成分配,
如果线程请求的栈深度大于虚拟机所允许的深度,则StackOverflowError。

04本地方法栈和虚拟机栈类似,主要为虚拟机使用到的Native方法服务。
也会抛出StackOverflowError 和OutOfMemoryError。

05方法区
被所有方法线程共享的一块内存区域。用于存储已经被虚拟机加载的类信息,常量,静态变量等。这个区域的内存回收目标主要针对常量池的回收和堆类型的卸载。

jvm的垃圾回收算法
如何确定垃圾(对象存活判断)
Java采用引用计数法和可达性分析来确定对象是否应该被回收。
但是,引用计数法容易产生循环引用的问题。
引用计数法在Java中如果要操作对象,就必须先获取该对象的引用,因此可以通过引用计数法来判断一个对象是否可以被回收。在为对象添加一个引用时,引用计数加1;在为对象删除一个引用时,引进计数减1;如果一个对象的引用计数为0,则表示此刻该对象没有被引用,可以被回收。引用计数法容易产生循环引用问题。循环引用指两个对象相互引用,导致它们的引用一直存在,而不能被回收,如图1-7所示,Object1与Object2互为引用,如果采用引用计数法,则Object1和Object2由于互为引用,其引用计数一直为1,因而无法被回收。

可达性分析通过根搜索算法(GC Roots Tracing)来实现。根搜索算法以一系列GC Roots的点作为起点向下搜索,在一个对象到任何GCRoots都没有引用链相连时,说明其已经死亡。根搜索算法主要针对栈中的引用、方法区中的静态引用和JNI中的引用展开分析,如图1-6所示。

可达性分析
可达性分析为了解决引用计数法的循环引用问题,Java还采用了可达性分析来判断对象是否可以被回收。具体做法是首先定义一些GC Roots对象,然后以这些GC Roots对象作为起点向下搜索,如果在GC roots和一个对象之间没有可达路径,则称该对象是不可达的。
不可达对象要经过至少两次标记才能判定其是否可以被回收,如果在两次标记后该对象仍然是不可达的,则将被垃圾收集器回收。

Java中的4种引用类型
在Java中一切皆对象,对象的操作是通过该对象的引用(Reference)实现的,Java中的引用类型有4种,分别为强引用、软引用、弱引用和虚引用,如图1-13所示。

(1)强引用:在Java中最常见的就是强引用。在把一个对象赋给一个引用变量时,这个引用变量就是一个强引用。有强引用的对象一定为可达性状态,所以不会被垃圾回收机制回收。因此,强引用是造成Java内存泄漏(Memory Link)的主要原因。 eg:String a = new String();
(2)软引用:软引用通过SoftReference类实现。如果一个对象只有软引用,则在系统内存空间不足时该对象将被回收。 eg:比如高速缓存就有用到软引用
(3)弱引用:弱引用通过WeakReference类实现,如果一个对象只有弱引用,则在垃圾回收过程中一定会被回收。 eg:弱引用具体指的是java.lang.ref.WeakReference类
(4)虚引用:虚引用通过PhantomReference类实现,虚引用和引用队列联合使用,主要用于跟踪对象的垃圾回收状态。 eg:配合ReferenceQueue使用
02.Java中常用的垃圾回收算法
Java中常用的垃圾回收算法有标记清除(Mark-Sweep)、复制(Copying)、标记整理(Mark-Compact)和分代收集(Generational Collecting)这4种垃圾回收算法,如图1-8所示。

标记清除算法
标记清除算法是基础的垃圾回收算法,其过程分为标记和清除两个阶段。在标记阶段标记所有需要回收的对象,在清除阶段清除可回收的对象并释放其所占用的内存空间,如图1-9所示。

由于标记清除算法在清理对象所占用的内存空间后并没有重新整理可用的内存空间,因此如果内存中可被回收的小对象居多,则会引起内存碎片化的问题,继而引起大对象无法获得连续可用空间的问题。
复制算法
复制算法是为了解决标记清除算法内存碎片化的问题而设计的。复制算法首先将内存划分为两块大小相等的内存区域,即区域1和区域2,新生成的对象都被存放在区域1中,在区域1内的对象存储满后会对区域1进行一次标记,并将标记后仍然存活的对象全部复制到区域2中,这时区域1将不存在任何存活的对象,直接清理整个区域1的内存即可,如图1-10所示。

复制算法的内存清理效率高且易于实现,但由于同一时刻只有一个内存区域可用,即可用的内存空间被压缩到原来的一半,因此存在大量的内存浪费。同时,在系统中有大量长时间存活的对象时,这些对象将在内存区域1和内存区域2之间来回复制而影响系统的运行效率。因此,该算法只在对象为“朝生夕死”状态时运行效率较高。
标记整理算法
标记整理算法结合了标记清除算法和复制算法的优点,其标记阶段和标记清除算法的标记阶段相同,在标记完成后将存活的对象移到内存的另一端,然后清除该端的对象并释放内存,如图1-11所示。

分代收集算法
无论是标记清除算法、复制算法还是标记整理算法,都无法对所有类型(长生命周期、短生命周期、大对象、小对象)的对象都进行垃圾回收。因此,针对不同的对象类型,JVM采用了不同的垃圾回收算法,该算法被称为分代收集算法。分代收集算法根据对象的不同类型将内存划分为不同的区域,JVM将堆划分为新生代和老年代。
· 新生代主要存放新生成的对象,其特点是对象数量多但是生命周期短,在每次进行垃圾回收时都有大量的对象被回收;老年代主要存放大对象和生命周期长的对象,因此可回收的对象相对较少。
因此,JVM根据不同的区域对象的特点选择了不同的算法。
· 目前,大部分JVM在新生代都采用了复制算法,因为在新生代中每次进行垃圾回收时都有大量的对象被回收,需要复制的对象(存活的对象)较少,不存在大量的对象在内存中被来回复制的问题,因此采用复制算法能安全、高效地回收新生代大量的短生命周期的对象并释放内存。
· JVM将新生代进一步划分为一块较大的Eden区和两块较小的Servivor区,Servivor区又分为ServivorFrom区和ServivorTo区。JVM在运行过程中主要使用Eden区和ServivorFrom区,进行垃圾回收时会将在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区,然后清理Eden区和ServivorFrom区的内存空间,如图1-12所示。

老年代主要存放生命周期较长的对象和大对象,因而每次只有少量非存活的对象被回收,因而在老年代采用标记清除算法。在JVM中还有一个区域,即方法区的永久代,永久代用来存储Class类、常量、方法描述等。在永久代主要回收废弃的常量和无用的类。JVM内存中的对象主要被分配到新生代的Eden区和ServivorFrom区,在少数情况下会被直接分配到老年代。在新生代的Eden区和ServivorFrom区的内存空间不足时会触发一次GC,该过程被称为MinorGC。在MinorGC后,在Eden区和ServivorFrom区中存活的对象会被复制到ServivorTo区,然后Eden区和ServivorFrom区被清理。如果此时在ServivorTo区无法找到连续的内存空间存储某个对象,则将这个对象直接存储到老年代。若Servivor区的对象经过一次GC后仍然存活,则其年龄加1。在默认情况下,对象在年龄达到15时,将被移到老年代。
垃圾收集器
Java堆内存分为新生代和老年代:新生代主要存储短生命周期的对象,适合使用复制算法进行垃圾回收;老年代主要存储长生命周期的对象,适合使用标记整理算法进行垃圾回收。因此,JVM针对新生代和老年代分别提供了多种不同的垃圾收集器,针对新生代提供的垃圾收集器有Serial、ParNew、Parallel Scavenge,针对老年代提供的垃圾收集器有Serial Old、Parallel Old、CMS,还有针对不同区域的G1分区收集算法,如图1-14所示。

CMS收集器(Concurrent Mark-Sweep)
CMS收集器是一个关注系统停顿时间的收集器。它的主要思想是把收集器分成了不同的阶段,其中某些阶段是可以用户程序并行的,从而减少了整体的系统停顿时间。它主要分成了以下几个阶段:

  • 初始标记 initial mark
  • 并发标记 concurrent mark
  • 重新标记 remark
  • 并发清理 concurrent clean
  • 并发重置 concurrent reset
    CMS虽然能减少系统的停顿时间,但是它也有其缺点:
  1. 标记-清除收集器,运行了一段时间后,内存会产生碎片,从而导致无法找到连续空间来分配大对象。
  2. CMS收集器在运行过程中会占用一些内存,同时系统还在运行,如果系统产生新对象的速度比CMS清理的速度快的话,会导致CMS运行失败。
    G1收集器
    G1(Garbage First)垃圾收集器为了避免全区域垃圾收集引起的系统停顿,将堆内存划分为大小固定的几个独立区域,独立使用这些区域的内存资源并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾收集时间,优先回收垃圾最多的区域。
    G1垃圾收集器通过内存区域独立划分使用和根据不同优先级回收各区域垃圾的机制,确保了G1垃圾收集器在有限时间内获得最高的垃圾收集效率。
    相对于CMS收集器,G1垃圾收集器两个突出的改进。
    ◎ 基于标记整理算法,不产生内存碎片。
    ◎ 可以精确地控制停顿时间,在不牺牲吞吐量的前提下实现短停顿垃圾回收。

jvm的类加载机制
什么是类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且提供了访问方法区内的数据结构的接口。

1.10.1 JVM的类加载阶段
JVM的类加载分为5个阶段:加载、验证、准备、解析、初始化。在类初始化完成后就可以使用该类的信息,在一个类不再被需要时可以从JVM中卸载,如图1-20所示。

● 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
● 连接,连接又包含三块内容:验证、准备、初始化。
1) 验证,文件格式、元数据、字节码、符号引用验证,通过验证Class文件才能被JVM加载;
2) 准备,为类的静态变量分配内存,并将其初始化为默认值;
3) 解析,把类中的符号引用转换为直接引用
● 初始化,为类的静态变量赋予正确的初始值
● 使用,new出对象供程序中使用
● 卸载,执行垃圾回收

1.10.2 类加载器
JVM提供了3种类加载器,分别是启动类加载器、扩展类加载器和应用程序类加载器,如图1-21所示。

(1)启动类加载器:负责加载Java_HOME/lib目录中的类库,或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库。
(2)扩展类加载器:负责加载Java_HOME/lib/ext目录中的类库,或通过java.ext.dirs系统变量加载指定路径中的类库。
(3)应用程序类加载器:负责加载用户路径(classpath)上的类库。除了上述3种类加载器,我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

1.10.3 双亲委派机制
JVM通过双亲委派机制对类进行加载。双亲委派机制指一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类去完成,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。若父类加载器在接收到类加载请求后发现自己也无法加载该类(通常原因是该类的Class文件在父类的类加载路径中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被成功加载,若找不到该类,则JVM会抛出ClassNotFoud异常。双亲委派类加载机制的类加载流程如下,如图1-22所示。

(1)将自定义加载器挂载到应用程序类加载器。
(2)应用程序类加载器将类加载请求委托给扩展类加载器。
(3)扩展类加载器将类加载请求委托给启动类加载器。
(4)启动类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由扩展类加载器加载。
(5)扩展类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由应用程序类加载器加载。
(6)应用程序类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由自定义加载器加载。
(7)在自定义加载器下查找并加载用户指定目录下的Class文件,如果在自定义加载路径下未找到目标Class文件,则抛出ClassNotFoud异常。

双亲委派机制的核心是保障类的唯一性和安全性。例如在加载rt.jar包中的java.lang.Object类时,无论是哪个类加载器加载这个类,最终都将类加载请求委托给启动类加载器加载,这样就保证了类加载的唯一性。如果在JVM中存在包名和类名相同的两个类,则该类将无法被加载,JVM也无法完成类加载流程。

Java虚拟机常用命令:
java -XX:+PrintCommandLineFlags -version

Guess you like

Origin blog.csdn.net/Nick_zcy/article/details/116277980