《面试补习》- JVM知识点大梳理

概述

一、什么是虚拟机?

Java 虚拟机,是一个能够执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件( .class )。html

跨平台的是 Java 程序(包括字节码文件),,而不是 JVM。JVM 是用 C/C++ 开发的,是编译后的机器码,不能跨平台,不一样平台下须要安装不一样版本的 JVM 。java

二、JVM 组成部分

  • 类加载器,在 JVM 启动时或者类运行时将须要的 class 加载到 JVM 中。算法

  • 内存区,将内存划分红若干个区以模拟实际机器上的存储、记录和调度功能模块,如实际机器上的各类功能的寄存器或者 PC 指针的记录器等。数组

  • 执行引擎,执行引擎的任务是负责执行 class 文件中包含的字节码指令,至关于实际机器上的 CPU 。缓存

  • 本地方法调用,调用 C 或 C++ 实现的本地方法的代码返回结果。安全

一、类加载器

从类被加载到虚拟机内存中开始,到卸御出内存为止,它的整个生命周期分为7个阶段,加载(Loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸御(Unloading)。其中验证、准备、解析三个部分统称为链接。 7个阶段发生的顺序以下:bash

加载(Loading)、验证(Verification)、准备(Preparation)、初始化(Initialization)、卸载(Unloading) 这五个阶段的过程是固定的,在类加载过程当中必须按照这种顺序循序渐进地进行,而解析阶段则不必定,他在某种状况下能够在初始化以后进行,这个是为了支持Java语言的运行时绑定(也称为动态绑定或者晚期绑定)。数据结构

1.一、加载

加载阶段,虚拟机须要完成3件事:多线程

  • 经过一个类的全限定名获取定义此类的二进制字节流。
  • 将这个字节流所表明的静态存储结构转换为方法区的运行时数据结构。
  • 在内存中生成一个表明这个类的java.lang.Class对象,做为方法区这个类的各类数据结构的访问入口。
加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,并且在Java堆中也建立一个java.lang.Class类的对象,这样即可以经过该对象访问方法区中的这些数据。 
复制代码

1.二、验证

验证阶段主要,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合虚拟机的要求,而且不会危害虚拟机自身的安全。并发

验证阶段主要完成下面4个阶段的校验动做:

文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围以内、常量池中的常量是否有不被支持的类型。

元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object以外。

字节码验证:经过数据流和控制流分析,肯定程序语义是合法的、符合逻辑的。

符号引用验证:确保解析动做能正确执行。
复制代码

1.三、准备

准备阶段是正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存都将在方法区分配。

进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

初始值一般状况下是数据类型默认的零值(如0、0L、null、false等)
复制代码

1.四、解析

解析阶段是将虚拟机常量池内的符号引用替换为直接引用的过程。解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号进行.

符号引用:简单的理解就是字符串,好比引用一个类,java.util.ArrayList 这就是一个符号引用,字符串引用的对象不必定被加载。

直接引用:指针或者地址偏移量。引用对象必定在内存(已经加载)。
复制代码

1.五、初始化

类初始化是类加载的最后一步,除了加载阶段,用户能够经过自定义的类加载器参与,其余阶段都彻底由虚拟机主导和控制。到了初始化阶段才真正执行Java代码。

类的初始化的主要工做是为静态变量赋程序设定的初值

如static int a = 100;在准备阶段,a被赋默认值0,在初始化阶段就会被赋值为100。
复制代码

Java虚拟机规范中严格规定了有且只有五种状况必须对类进行初始化:

  • 一、使用new字节码指令建立类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。

  • 二、经过java.lang.reflect包的方法对类进行反射调用的时候,若是类没有进行过初始化,则要首先进行初始化。

  • 三、当初始化一个类的时候,若是发现其父类没有进行过初始化,则首先触发父类初始化。

  • 四、当虚拟机启动时,用户须要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。

  • 五、使用jdk1.7的动态语言支持时,若是一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,而且这个方法句柄对应的类没有进行初始化,则须要先触发其初始化。

二、对象的建立过程

Java 中对象的建立就是在堆上分配内存空间的过程,此处说的对象建立仅限于 new 关键字建立的普通 Java 对象,不包括数组对象的建立。

当虚拟机遇到一条含有new的指令时,会进行一系列对象建立的操做:

2.一、检查类是否被加载

一、检查常量池中是否有即将要建立的这个对象所属的类的符号引用; 若常量池中没有这个类的符号引用,说明这个类尚未被定义!抛出ClassNotFoundException;

二、进而检查这个符号引用所表明的类是否已经被JVM加载; 若该类尚未被加载,就找该类的class文件,并加载进方法区; 若该类已经被JVM加载,则准备为对象分配内存;

2.二、为对象分配内存

三、根据方法区中该类的信息肯定该类所需的内存大小; 一个对象所需的内存大小是在这个对象所属类被定义完就能肯定的!且一个类所生产的全部对象的内存大小是同样的!JVM在一个类被加载进方法区的时候就知道该类生产的每个对象所须要的内存大小。 四、从堆中划分一块对应大小的内存空间给新的对象; 分配堆中内存有两种方式: 指针碰撞 若是JVM的垃圾收集器采用复制算法或标记-整理算法,那么堆中空闲内存是完整的区域,而且空闲内存和已使用内存之间由一个指针标记。那么当为一个对象分配内存时,只需移动指针便可。所以,这种在完整空闲区域上经过移动指针来分配内存的方式就叫作“指针碰撞”。 空闲列表 若是JVM的垃圾收集器采用标记-清除算法,那么堆中空闲区域和已使用区域交错,所以须要用一张“空闲列表”来记录堆中哪些区域是空闲区域,从而在建立对象的时候根据这张“空闲列表”找到空闲区域,并分配内存。 综上所述:JVM究竟采用哪一种内存分配方法,取决于它使用了何种垃圾收集器。

多线程并发时会出现正在给对象 A 分配内存,还没来得及修改指针,对象 B 又用这个指针分配内存,这样就出现问题了。
解决这种问题有两种方案:
第一种,是采用同步的办法,使用 CAS 来保证操做的原子性。
另外一种,是每一个线程分配内存都在本身的空间内进行,便是每一个线程都在堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),分配内存的时候再TLAB上分配,互不干扰。能够经过 -XX:+/-UseTLAB 参数决定。
复制代码

2.三、为分配的内存空间初始化零值

五、为对象中的成员变量赋上初始值(默认初始化);

对象的内存分配完成后,还须要将对象的内存空间都初始化为零值,这样能保证对象即便没有赋初值,也能够直接使用

2.四、为对象进行其余设置

六、设置对象头中的信息;

所属的类,类的元数据信息,对象的 hashcode ,GC 分代年龄等信息

2.五、执行 init 方法

七、调用对象的构造函数进行初始化

执行完上面的步骤以后,在虚拟机里这个对象就算建立成功了,可是对于 Java 程序来讲还须要执行 init 方法才算真正的建立完成,由于这个时候对象只是被初始化零值了,尚未真正的去根据程序中的代码分配初始值,调用了 init 方法以后,这个对象才真正能使用。

初始化顺序:

在new B一个实例时首先要进行类的装载。(类只有在使用New调用建立的时候才会被java类装载器装入)

在装载类时,先装载父类A,再装载子类B

装载父类A后,完成静态动做(包括静态代码和变量,它们的级别是相同的,按照代码中出现的顺序初始化)

装载子类B后,完成静态动做

类装载完成,开始进行实例化

在实例化子类B时,先要实例化父类A2,实例化父类A时,先成员实例化(非静态代码)
父类A的构造方法
子类B的成员实例化(非静态代码)
子类B的构造方法

先初始化父类的静态代码--->初始化子类的静态代码-->初始化父类的非静态代码--->初始化父类构造函数--->初始化子类非静态代码--->初始化子类构造函数

复制代码

三、对象的内存布局

3.一、对象头(markword)

  • 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳、对象分代年龄,这部分信息称为“Mark Word”;Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽可能多的信息,它会根据本身的状态复用本身的存储空间。
  • 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例;
Klass Word  这里实际上是虚拟机设计的一个oop-klass model模型,这里的OOP是指Ordinary Object Pointer(普通对象指针),看起来像个指针其实是藏在指针里的对象。而 klass 则包含 元数据和方法信息,用来描述 Java 类。它在64位虚拟机开启压缩指针的环境下占用 32bits 空间。
复制代码
  • 若是对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。由于虚拟机能够经过普通 Java 对象的元数据信息肯定 Java 对象的大小,可是从数组的元数据中没法肯定数组的大小。

在32位系统下,对象头8字节,64位则是16个字节【未开启压缩指针,开启后12字节】。

假设当前为32bit,在对象未被锁定状况下。25bit为存储对象的哈希码、4bit用于存储分代年龄,2bit用于存储锁标志位,1bit固定为0。

不一样状态下存放数据:

这其中锁标识位须要特别关注下。锁标志位与是否为偏向锁对应到惟一的锁状态。

锁的状态分为四种无锁状态、偏向锁、轻量级锁和重量级锁

不一样状态时对象头的区间含义,如图所示。

3.二、实例数据(Instance Data)

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各类类型的字段内容。

这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在 Java 源码中定义顺序的影响。

分配策略:相同宽度的字段老是放在一块儿,好比double和long

3.三、对其填充(Padding)

对齐填充不是必然存在的,没有特别的含义,它仅起到占位符的做用。

因为HotSpot规定对象的大小必须是8的整数倍,对象头恰好是整数倍,若是实例数据不是的话,就须要占位符对齐填充。
复制代码

3.四、预估对象大小

32 位系统下,当使用 new Object() 时,JVM 将会分配 8(Mark Word+类型指针) 字节的空间,128 个 Object 对象将占用 1KB 的空间。 若是是 new Integer(),那么对象里还有一个 int 值,其占用 4 字节,这个对象也就是 8+4=12 字节,对齐后,该对象就是 16 字节。

以上只是一些简单的对象,那么对象的内部属性是怎么排布的?

Class A {
    int i;
    byte b;
    String str;
}
复制代码

其中对象头部占用 ‘Mark Word’4 + ‘类型指针’4 = 8 字节;byte 8 位长,占用 1 字节;int 32 位长,占用 4 字节;String 只有引用,占用 4 字节; 那么对象 A 一共占用了 8+1+4+4=17 字节,按照 8 字节对齐原则,对象大小也就是 24 字节。

这个计算看起来是没有问题的,对象的大小也确实是 24 字节,可是对齐(padding)的位置并不对:

在 HotSpot VM 中,对象排布时,间隙是在 4 字节基础上的(在 32 位和 64 位压缩模式下),上述例子中,int 后面的 byte,空隙只剩下 3 字节,接下来的 String 对象引用须要 4 字节来存放,所以 byte 和对象引用之间就会有 3 字节对齐,对象引用排布后,最后会有 4 字节对齐,所以结果上依然是 7 字节对齐。此时对象的结构示意图,以下图所示:

四、对象访问

对象的访问方式由虚拟机决定,java虚拟机提供两种主流的方式
1.句柄访问对象
2.直接指针访问对象。(Sun HotSpot使用这种方式)
复制代码

4.一、句柄访问

简单来讲就是java堆划出一块内存做为句柄池,引用中存储对象的句柄地址,句柄中包含对象实例数据、类型数据的地址信息。

优势:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不须要改动引用【ref】自己。

4.二、直接指针

与句柄访问不一样的是,ref中直接存储的就是对象的实例数据,可是类型数据跟句柄访问方式同样。

优势:优点很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。【多是出于Java中对象的访问时十分频繁的,平时咱们经常使用的JVM HotSpot采用此种方式】

五、JVM 内存区域

5.一、虚拟机栈

描述的是方法执行时的内存模型,是线程私有的,生命周期与线程相同,每一个方法被执行的同时会建立栈桢,主要保存执行方法时的局部变量表、操做数栈、动态链接和方法返回地址等信息,方法执行时入栈,方法执行完出栈,出栈就至关于清空了数据,入栈出栈的时机很明确,因此这块区域不须要进行 GC。

Java虚拟机栈可能出现两种类型的异常:

  • 线程请求的栈深度大于虚拟机容许的栈深度,将抛出StackOverflowError

  • 虚拟机栈空间能够动态扩展,当动态扩展是没法申请到足够的空间时,抛出OutOfMemory异常

  • 拓展link: 栈帧

5.二、本地方法栈

与虚拟机栈功能很是相似,主要区别在于虚拟机栈为虚拟机执行 Java 方法时服务,而本地方法栈为虚拟机执行本地方法时服务的。这块区域也不须要进行 GC。

5.三、程序计数器

  • 程序计数器是一块很小的内存空间,它是线程私有的,能够认做为当前线程的行号指示器。
  • 程序计数器的主要做用是记录线程运行时的状态,方便线程被唤醒时能从上一次被挂起时的状态继续执行
  • 程序计数器是惟一一个在 Java 虚拟机规范中没有规定任何 OOM 状况的区域,因此这块区域也不须要进行 GC

5.四、本地内存

  • 线程共享区域,Java 8 中,本地内存,也是咱们一般说的堆外内存,包括元空间和方法区
  • 主要存储类的信息,常量,静态变量,即时编译器编译后代码等,这部分因为是在堆中实现的,受 GC 的管理,不过因为永久代有 -XX:MaxPermSize 的上限
  • 因此若是动态生成类(将类信息放入永久代)或大量地执行 String.intern (将字段串放入永久代中的常量区),很容易形成 OOM,有人说能够把永久代设置得足够大,但很难肯定一个合适的大小,受类数量,常量数量的多少影响很大。
  • 因此在 Java 8 中就把方法区的实现移到了本地内存中的元空间中,这样方法区就不受 JVM 的控制了,也就不会进行 GC,也所以提高了性能(发生 GC 会发生 Stop The Word,形成性能受到必定影响,后文会提到),也就不存在因为永久代限制大小而致使的 OOM 异常了(假设总内存2G,JVM 被分配内存 100M, 理论上元空间能够分配 2G-100M = 1.9G,空间大小足够),也方便在元空间中统一管理。
  • 综上所述,在 Java 8 之后这一区域也不须要进行 GC
  • 拓展link: 堆外内存回收

5.五、堆

  • 对象实例和数组都是在堆上分配的,GC 也主要对这两类数据进行回收。
  • java虚拟机规范对这块的描述是:全部对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,可是大多数状况都是这样的。
  • 堆细分: 新生代(Eden,survior)和老年代

六、对象存活判断

  • 引用计数
  • 可达性分析

6.一、引用计数

每一个对象有一个引用计数属性,新增一个引用时计数加 1 ,引用释放时计数减 1 ,计数为 0 时能够回收。此方法简单,没法解决对象相互循环引用的问题。目前在用的有 Python、ActionScript3 等语言。

6.二、可达性分析

从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证实此对象是不可用的。不可达对象。目前在用的有 Java、C# 等语言。

GC Roots 对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中的类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中 JNI(即通常说的 Native 方法)中引用的对象。
复制代码

如何判断无用的类:

该类全部实例都被回收(Java 堆中没有该类的对象)。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,没法在任何地方利用反射访问该类。
复制代码

6.三、finalize

finallize()方法,是在释放该对象内存前由 GC (垃圾回收器)调用。

一般建议在这个方法中释放该对象持有的资源,例如持有的堆外内存、和远程服务的长链接。 通常状况下,不建议重写该方法。 对于一个对象,该方法有且仅会被调用一次。

6.四、对象引用类型

  • 强引用
  • 软引用(SoftReference)
  • 弱引用(WeakReference)
  • 虚引用(PhantomReference)

6.4.一、强引用

若是一个对象具备强引用,那就相似于必不可少的生活用品,垃圾回收器毫不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具备强引用的对象来解决内存不足问题

6.4.二、软引用

若是一个对象只具备软引用,那就相似于无关紧要的生活用品。若是内存空间足够,垃圾回收器就不会回收它,若是内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就能够被程序使用。软引用可用来实现内存敏感的高速缓存。

6.4.三、弱引用

弱引用与软引用的区别在于:只具备弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程当中,一旦发现了只具备弱引用的对象,无论当前内存空间足够与否,都会回收它的内存。

6.4.四、虚引用

“虚引用”顾名思义,就是形同虚设,与其余几种引用都不一样,虚引用并不会决定对象的生命周期。若是一个对象仅持有虚引用,那么它就和没有任何引用同样,在任什么时候候均可能被垃圾回收。。当垃 圾回收器准备回收一个对象时,若是发现它还有虚引用,就会在回收对象的内存以前,把这个虚引用加入到与之关联的引用队列中。程序能够经过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序若是发现某个虚引用已经被加入到引用队列,那么就能够在所引用的对象的内存被回收以前采起必要的行动。

拓展

利用软引用和弱引用解决 OOM 问题。用一个 HashMap 来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM 会自动回收这些缓存图片对象所占用的空间,从而有效地避免了 OOM 的问题. 经过软引用实现 Java 对象的高速缓存。好比咱们建立了一 Person 的类,若是每次须要查询一我的的信息,哪怕是几秒中以前刚刚查询过的,都要从新构建一个实例,这将引发大量 Person 对象的消耗,而且因为这些对象的生命周期相对较短,会引发屡次 GC 影响性能。此时,经过软引用和 HashMap 的结合能够构建高速缓存,提供性能。

七、垃圾回收算法

  • 标记-清除算法
  • 标记-整理算法
  • 复制算法
  • 分代收集算法

7.一、标记-清除

在标记阶段,首先经过根节点,标记全部从根节点开始的可达对象。所以,未被标记的对象就是未被引用的垃圾对象(好多资料说标记出要回收的对象,其实明白大概意思就能够了)。而后,在清除阶段,清除全部未被标记的对象。

缺点:

  • 一、效率问题,标记和清除两个过程的效率都不高。
  • 二、空间问题,标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能会致使之后在程序运行过程当中须要分配较大的对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。

7.二、标记-整理

标记整理算法,相似与标记清除算法,不过它标记完对象后,不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉边界之外的内存。

优势:

  • 一、相对标记清除算法,解决了内存碎片问题。
  • 二、没有内存碎片后,对象建立内存分配也更快速了(可使用TLAB进行分配)。

缺点:

  • 一、效率问题,(同标记清除算法)标记和整理两个过程的效率都不高。

7.三、复制算法

复制算法,能够解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,而后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可(还可以使用TLAB进行高效分配内存)

优势:

  • 一、效率高,没有内存碎片。

缺点:

  • 一、浪费一半的内存空间。
  • 二、复制收集算法在对象存活率较高时就要进行较多的复制操做,效率将会变低。

7.四、分代算法

当前商业虚拟机都是采用分代收集算法,它根据对象存活周期的不一样将内存划分为几块,通常是把 Java 堆分为新生代和老年代,而后根据各个年代的特色采用最适当的收集算法。

在新生代中,每次垃圾收集都发现有大批对象死去,只有少许存活,就选用复制算法。 而老年代中,由于对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记清理”或者“标记整理”算法来进行回收。

图的左半部分是未回收前的内存区域,右半部分是回收后的内存区域。

对象分配策略: 对象优先在 Eden 区域分配,若是对象过大直接分配到 Old 区域。 长时间存活的对象进入到 Old 区域。

改进自复制算法

如今的商业虚拟机都采用这种收集算法来回收新生代,IBM 公司的专门研究代表,新生代中的对象 98% 是“朝生夕死”的,因此并不须要按照 1:1 的比例来划份内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。 HotSpot 虚拟机默认 Eden 和 2 块 Survivor 的大小比例是 8:1:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的内存会被“浪费”。固然,98% 的对象可回收只是通常场景下的数据,咱们没有办法保证每次回收都只有很少于 10% 的对象存活,当 Survivor 空间不够用时,须要依赖其余内存(这里指老年代)进行分配担保(Handle Promotion)。

八、安全点

8.一、安全点

SafePoint 安全点,顾名思义是指一些特定的位置,当线程运行到这些位置时,线程的一些状态能够被肯定(the thread’s representation of it’s Java machine state is well described),好比记录OopMap 的状态,从而肯定 GC Root 的信息,使 JVM 能够安全的进行一些操做,好比开始 GC 。

SafePoint 指的特定位置主要有:

  • 循环的末尾 (防止大循环的时候一直不进入 Safepoint ,而其余线程在等待它进入 Safepoint )。
  • 方法返回前。
  • 调用方法的 Call 以后。
  • 抛出异常的位置。

8.二、安全区域

安全点完美的解决了如何进入GC问题,实际状况可能比这个更复杂,可是若是程序长时间不执行,好比线程调用的sleep方法,这时候程序没法响应JVM中断请求这时候线程没法到达安全点,显然JVM也不可能等待程序唤醒,这时候就须要安全区域了。

安全区域是指一段代码片中,引用关系不会发生变化,在这个区域任何地方GC都是安全的,安全区域能够看作是安全点的一个扩展。线程执行到安全区域的代码时,首先标识本身进入了安全区域,这样GC时就不用管进入安全区域的线层了,线层要离开安全区域时就检查JVM是否完成了GC Roots枚举,若是完成就继续执行,若是没有完成就等待直到收到能够安全离开的信号。

九、JVM 垃圾回收器

收集器 串行、并行or并发 新生代/老年代 算法 目标 适用场景
Serial 串行 新生代 复制算法 响应速度优先 单CPU环境下的Client模式
Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后备预案
ParNew 并行 新生代 复制算法 响应速度优先 多CPU环境时在Server模式下与CMS配合
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后台运算而不须要太多交互的任务
Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不须要太多交互的任务
CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网站或B/S系统 服务端上的Java应用
G1 并发 both 标记-整理+复制算法 响应速度优先 面向服务端应用,未来替换CMS
ZGC 并发 both 标记-整理+复制算法 响应速度优先 面向服务端应用,未来替换CMS

9.一、Serial (新生代)

  • 最基本的单线程垃圾收集器。使用一个CPU或一条收集线程去执行垃圾收集工做。
  • 工做时会Stop The World,暂停全部用户线程,形成卡顿。适合运行在Client模式下的虚拟机。
  • 用做新生代收集器,复制算法。

9.二、ParNew(新生代)

  • Serial收集器的多线程版本,和Serial的惟一区别就是使用了多条线程去垃圾收集。
  • 除了Serial,只有它能够和CMS搭配使用的收集器。
  • 用做新生代收集器,复制算法。

9.三、Parallel Scavenge(新生代)

用做新生代收集器,复制算法。 关注高吞吐量,能够高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不须要太多交互的任务。 Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

9.四、Serial Old(老年代)

  • Serial收集器的老年代版本,单线程,标记-整理 算法。
  • 通常用于Client模式的虚拟机。
  • 当虚拟机是Server模式时,有2个用途:一种用途是在JDK 1.5以及以前的版本中与Parallel Scavenge收集器搭配使用 ,另外一种用途就是做为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

9.五、Parallel Old(老年代)

  • Parallel Scavenge收集器的老年代版本,使用多线程和 标记-整理 算法。在JDK 1.6中开始提供。 在注重吞吐量的场合,配合Parallel Scavenge收集器使用。

9.六、CMS(Concurrent Mark Sweep)(老年代)

  • 一种以获取最短回收停顿时间为目标的收集器。适合须要与用户交互的程序,良好的响应速度能提高用户体验。
  • 基于 标记—清除 算法。适合做为老年代收集器。
  • 收集过程分4步:
一、 初始标记(CMS initial mark):只是标记一下GC Roots能直接关联到的对象,速度很快,会Stop The World。
二、 并发标记(CMS concurrent mark):进行GC Roots Tracing(可达性分析)的过程。
三、 从新标记(CMS remark):会Stop The -World。为了修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间通常比初始标记阶段稍长些,但远比并发标记的时间短。
四、 并发清除(CMS concurrent sweep):回收内存。
复制代码

耗时最长的并发标记和并发清除过程收集器线程均可以与用户线程一块儿工做,因此时并发执行的。

缺点:

  • 并发阶段,虽然不会致使用户线程暂停,但会占用一部分线程(CPU资源),致使应用变慢,吞吐量下降。默认启动收集线程数是(CPU数量+3)/4。即当CPU在4个以上时,并发回收时垃圾收集线程很多于25%的CPU资源,而且随着CPU数量的增长而降低。可是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。
  • 没法清除浮动垃圾。并发清除阶段,用户线程还在运行,还会产生新垃圾。这些垃圾不会在这次GC中被标记,只能等到下次GC被回收。
  • 标记-清除 算法会产生大量不连续内存,致使分配大对象时内存不够,提早触发Full GC。

9.七、G1

  • 在JDK1.7提供的先进垃圾收集器。
  • 既适合新生代,也适合老年代。
  • 空间整合:使用 标记-整理 算法,不产生碎片空间。
  • 整个Java堆被分为多个大小相同的的块(region)。新生代和老年代再也不是物理隔离的,而是一部分region块组成的集合。
  • 默认把堆平均分红2048个region,最小1M,最大32M,必须是2的幂次方,能够经过-XX:G1HeapRegionSize参数指定。region分为4种:
E:eden区,新生代
S:survivor区,新生代
O:old区,老年代
H:humongous区,用来放大对象。当新建对象大小超过region大小一半时,直接在新的一个或多个连续region中分配,并标记为H
复制代码
  • 可预测的停顿时间:估算每一个region内的垃圾可回收的空间以及回收须要的时间(经验值),记录在一个优先列表中。收集时,优先回收价值最大的region,而不是在整个堆进行全区域回收。这样提升了回收效率,得名:Garbage-First。 G1中有2种GC:

  • young GC:新生代eden区没有足够可用空间时触发。存活的对象移到survivor区或晋升old区。 mixed GC:当old区对象不少时,老年代对象空间占堆总空间的比值达到阈值(-XX:InitiatingHeapOccupancyPercent默认45%)会触发,它除了回收年轻代,也回收 部分 老年代(回收价值高的部分region)。

mixed GC回收步骤:

一、初始标记(Initial Marking):只是标记一下GC Roots能直接关联到的对象,而且修改TAMS(Next Top at Mark 二、Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中建立新对象。这阶段须要停顿线程(STW),但耗时很短,共用YGC的停顿,因此通常伴随着YGC发生。
三、并发标记(Concurrent Marking):进行可达性分析,找出存活对象,耗时长,但可与用户线程并发执行。
四、最终标记(Final Marking):修正并发标记阶段用户线程运行致使的变更记录。会STW,但能够并行执行,时间不会很长。
五、筛选回收(Live Data Counting and Evacuation):根据每一个region的回收价值和回收成本排序,根据用户配置的GC停顿时间开始回收。
复制代码
  • 当对象分配过快,mixed GC来不及回收,G1会退化,触发Full GC,它使用单线程的Serial收集器来回收,整个过程STW,要尽可能避免这种状况。
  • 当内存不多的时候(存活对象占用大量空间),没有足够空间来复制对象,会致使回收失败。这时会保留被移动过的对象和没移动的对象,只调整引用。失败发生后,收集器认为存活对象被移动了,有足够空间让应用程序使用,因而用户线程继续工做,等待下一次触发GC。若是内存不够,就会触发Full GC。

9.八、ZGC

在JDK 11当中,加入了实验性质的ZGC。它的回收耗时平均不到2毫秒。它是一款低停顿高并发的收集器。

ZGC几乎在全部地方并发执行的,除了初始标记的是STW的。因此停顿时间几乎就耗费在初始标记上,这部分的实际是很是少的。那么其余阶段是怎么作到能够并发执行的呢?

ZGC主要新增了两项技术,

  • 着色指针Colored Pointer,
  • 读屏障Load Barrier。

ZGC 是一个并发、基于区域(region)、增量式压缩的收集器。Stop-The-World 阶段只会在根对象扫描(root scanning)阶段发生,这样的话 GC 暂停时间并不会随着堆和存活对象的数量而增长。

处理阶段:

  • 标记(Marking);
  • 重定位(Relocation)/压缩(Compaction);
  • 从新分配集的选择(Relocation set selection);
  • 引用处理(Reference processing);
  • 弱引用的清理(WeakRefs Cleaning);
  • 字符串常量池(String Table)和符号表(Symbol Table)的清理;
  • 类卸载(Class unloading)

着色指针Colored Pointer

ZGC利用指针的64位中的几位表示Finalizable、Remapped、Marked一、Marked0(ZGC仅支持64位平台),以标记该指向内存的存储状态。
至关于在对象的指针上标注了对象的信息。注意,这里的指针至关于Java术语当中的引用。

在这个被指向的内存发生变化的时候(内存在Compact被移动时),颜色就会发生变化。
复制代码

读屏障Load Barrier

因为着色指针的存在,在程序运行时访问对象的时候,能够轻易知道对象在内存的存储状态(经过指针访问对象),
若请求读的内存在被着色了,那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有必定的耗费,从而达到与用户线程并发的效果。
复制代码

与标记对象的传统算法相比,ZGC在指针上作标记,在访问指针时加入Load Barrier(读屏障),好比当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有几率被减速,而不存在为了保持应用与GC一致而粗暴总体的Stop The World。

拓展

Java——七种垃圾收集器+JDK11最新ZGC

相关文章
相关标签/搜索