《深入理解java虚拟机》第3版笔记3 第3章 垃圾收集器与内存分配策略

  1. 可达性分析算法

    1. 在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
    • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。

    • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

    • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。

    • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

    • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。 ·所有被同步锁(synchronized关键字)持有的对象。

    • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

      除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。

    1. Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软 引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强 度依次逐渐减弱。
    • 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。
    • 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内 存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。
    • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只 能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只 被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。
    • 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供 了PhantomReference类来实现虚引用。
    1. 要回收一个对象,至少要经历两次标记过程:如果对象在进行可达性分析后发现没 有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是 否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用 过,那么虚拟机将这两种情况都视为“没有必要执行”。

    如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的 队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize() 方法。这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。

    public class FinalizeEscapeGC {
        public static FinalizeEscapeGC SAVE_HOOK = null;
    
        public void isAlive() {
            System.out.println("yes, i am still alive :)");
        }
    
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("finalize method executed!");
            FinalizeEscapeGC.SAVE_HOOK = this;
        }
    
        public static void main(String[] args) throws Throwable {
            SAVE_HOOK = new FinalizeEscapeGC();
    //对象第一次成功拯救自己
            SAVE_HOOK = null;
            System.gc();
    // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
            Thread.sleep(500);
            if (SAVE_HOOK != null) {
                SAVE_HOOK.isAlive();
            } else {
                System.out.println("no, i am dead :(");
            }
    // 下面这段代码与上面的完全相同,但是这次自救却失败了
            SAVE_HOOK = null;
            System.gc();
    // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
            Thread.sleep(500);
            if (SAVE_HOOK != null) {
                SAVE_HOOK.isAlive();
            } else {
                System.out.println("no, i am dead :(");
            }
        }
    }
    
    
    1. 回收方法区

    JDK 11时期的ZGC收集器就不支持类卸载

    • 判定一个类型是否属于“不再被使用的类”需要同时满足下面三个条件:

      该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。

      加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP的重加载等,否则通常是很难达成的。

      该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

      Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是 和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX: +TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在 Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版[1]的虚拟机支持。

    1. 垃圾收集算法

    从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”(Reference Counting GC)和“追踪式垃圾收集”(Tracing GC)两大类,这两类也常被称作“直接垃圾收集”和“间接 垃圾收集”。

    • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

      新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。

      老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单 独收集老年代的行为。

      混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为。

    • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

    1. 标记-清除算法

    2. 标记-复制算法

    3. 标记-整理算法

      HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的

      让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经 大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。CMS就是这样做的

    4. HotSpot的算法细节实现

      • 根节点枚举

        所有收集器在根节点枚举这一步骤时都是必须暂停用户线程(Stop The World),包括CMS、G1、 ZGC等收集器

        Java虚拟机使用的都是准确式垃圾收集,所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有 执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。在HotSpot 的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。一旦类加载动作完成的时候, HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译(见第11章)过程中,也 会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信 息了,并不需要真正一个不漏地从方法区等GC Roots开始查找。

      • 安全点

      OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举。只是在“特定的位置”记录 了这些信息,这些位置被称为安全点(Safepoint)。

      两种中断方案:抢先式中断 (Preemptive Suspension)和主动式中断(Voluntary Suspension),现在虚拟机都采用主动式中断。

      当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一 个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最 近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他 需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新 对象。

      • 安全区域

        用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走 到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。对于 这种情况,就必须引入安全区域(Safe Region)来解决。

        当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时 间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全 区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的 阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以 离开安全区域的信号为止。

      • 记忆集与卡表

      所有涉及部分区域收集(Partial GC)行为的 垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,为了避免把整个其他区域加进GC Roots扫描范围,就需要记忆集(Remembered Set)的数据结构。

      记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。

      实现方案:

      • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
      • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
      • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

      第三种“卡精度”所指的是用一种称为“卡表”(Card Table)的方式去实现记忆集

      卡表最简单的形式可以只是一个字节数组[2],而HotSpot虚拟机确实也是这样做的。以下这行代 码是HotSpot默认的卡表标记逻辑

      CARD_TABLE [this address >> 9] = 0;

      字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个 内存块被称作“卡页”(Card Page)。一般来说,卡页大小都是以2的N次幂的字节数,通过上面代码可 以看出HotSpot中使用的卡页是2的9次幂,即512字节(地址右移9位,相当于用地址除以512)。那如 果卡表标识内存区域的起始地址是0x0000的话,数组CARD_TABLE的第0、1、2号元素,分别对应了 地址范围为0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡页内存块.

      一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代 指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃 圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它 们加入GC Roots中一并扫描。

    5. 写屏障

      在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。类似AOP

      写屏障的开销外,卡表在高并发场景下还面临着“伪共享”(False Sharing)问题.也就是缓存行。

      在JDK 7之后,HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark,用来决定是否开启 卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损 耗,是否打开要根据应用实际运行情况来进行测试权衡。

    6. 并发的可达性分析

      三色标记

      • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是 白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
      • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代 表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对 象不可能直接(不经过灰色对象)指向某个白色对象。
      • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
    7. 经典垃圾收集器

      《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

      图中展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配 使用[3],图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。

      在JDK 8时将Serial+CMS、 ParNew+Serial Old这两个组合声明为废弃,并在JDK 9中完全取消了这些组合的支持

      • Serial收集器

        单线程工作的收集器,强 调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。

        Serial 采用复制算法,Serial Old采用标记整理算法

        《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

      • ParNew收集器

      实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之 外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规 则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。

      除了Serial收集器外,目前只有它能与CMS 收集器配合工作。

      《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

      涉及“并发”和“并行”概念的收集器,并发,并行的概念

      并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线 程在协同工作,通常默认此时用户线程是处于等待状态。

      并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾 收集器线程与用户线程都在运行。由

      CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作的原因:

      • 一个面向低延迟一个面向高吞吐量的目标不一致

      • 技术上的原因是Parallel Scavenge收集器及 后面提到的G1收集器等都没有使用HotSpot中原本设计的垃圾收集器的分代框架,而选择另外独立实 现。

      • Parallel Scavenge收集器

        一款新生代收集器,也是基于标记-复制算法实现的收集器。

        Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能 地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐 量(Throughput)

      • Serial Old收集器

        Serial收集器的老年代版本。

        与Parallel Scavenge收集器搭配使用,也可以作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用

        Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,并非 直接调用Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现几乎是一样的,所以在官 方的许多资料中都是直接以Serial Old代替PS MarkSweep进行讲解。

      • Parallel Old收集器

        Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实 现。

        关注吞吐量或处理器资源稀缺时,可以考虑Parallel Scavenge加Parallel Old收集器这个组合。

        《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

      • CMS收集器

      CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现。

      整个过程分为四个步骤,包括:

      1)初始标记(CMS initial mark)

      2)并发标记(CMS concurrent mark)

      3)重新标记(CMS remark)

      4)并发清除(CMS concurrent sweep)

      《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

      缺点:

      1)CMS收集器对处理器资源非常敏感。

      2)无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。

      3)CMS是一款基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。有触发触发Full GC的情况。

      • Garbage First收集器

        把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以 根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的 Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的 旧对象都能获取很好的收集效果。Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。

        G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作 为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免 在整个Java堆中进行全区域的垃圾收集。

        内存回收的速度赶不上内存分配的速度, G1收集器也要*冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。

        G1收集器的 运作过程大致可划分为以下四个步骤:

        1)初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要 停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。

        2)并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。

        3)最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。

        4)筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以*选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。

        《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

        G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现。这种特性有利于程序长时间运行,在程序为大 对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。

        缺点:

        ​ 在用户程序运行过程 中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载 (Overload)都要比CMS要高。

  2. 低延迟垃圾收集器

    衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟 (Latency),三者共同构成了一个“不可能三角[1]”。

    《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

    Shenandoah和ZGC,几乎整个工作过程全 部都是并发的,只有初始标记、最终标记这些阶段有短暂的停顿,这部分停顿的时间基本上是固定 的,与堆的容量、堆中对象的数量没有正比例关系。

  • Shenandoah收集器

    2014年RedHat把Shenandoah贡献 给了OpenJDK(第一款不由Oracle(包括以前的Sun)公 司的虚拟机团队所领导开发的HotSpot垃圾收集器).

    目标 是实现一种能在任何堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的垃圾收集器

    Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降 低了伪共享问题(见3.4.4节)的发生概率。

    《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

    Shenandoah收集器的工作过程大致可以划分为以下九个阶段:

    1)·初始标记(Initial Marking):与G1一样,首先标记与GC Roots直接关联的对象,这个阶段仍 是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关。

    2)并发标记(Concurrent Marking):与G1一样,遍历对象图,标记出全部可达的对象,这个阶段 是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。

    3)最终标记(Final Marking):与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值 最高的Region,将这些Region构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停 顿。

    4)并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到 的Region(这类Region被称为Immediate Garbage Region)。

    5)并发回收(Concurrent Evacuation):并发回收阶段是Shenandoah与之前HotSpot中其他收集器的 核心差异。在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之 中。复制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进 行的话,就变得复杂起来了。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象 进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对 象的地址,这是很难一瞬间全部改变过来的。对于并发回收阶段遇到的这些困难,Shenandoah将会通 过读屏障和被称为“*s Pointers”的转发指针来解决(讲解完Shenandoah整个工作过程之后笔者还要 再回头介绍它)。并发回收阶段运行的时间长短取决于回收集的大小。

    6)初始引用更新(Initial Update Reference):并发回收阶段复制对象结束后,还需要把堆中所有指 向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未 做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收 集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的 停顿。 7)

    并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作,这个阶段是与用户 线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它 不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为 新值即可。

    8)最终引用更新(Final Update Reference):解决了堆中的引用更新后,还要修正存在于GC Roots 中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。

    9)并发清理(Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有的Region已 再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收 这些Region的内存空间,供以后新对象分配使用。

    三个最重要的并发阶段(并发标记、并发回收、并发引用更新)

    《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

    《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

  1. ZGC收集器

    ZGC的内存布局。与Shenandoah和G1一样,ZGC也采用基于Region的堆内存布局,但与它们不同的是,ZGC的Region(在一些官方资料中将它称为Page或者ZPage,本章为行文一致继续称 为Region)具有动态性——动态创建和销毁,以及动态的区域容量大小。在x64硬件平台下,ZGC的 Region可以具有如图3-19所示的大、中、小三类容量:

    小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。

    中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对 象。

    大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置 4MB或以上的大对象。

    《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

    ZGC的运作过程大致可划分为以下四个大的阶 段。全部四个阶段都是可以并发执行的,仅是两个阶段中间会存在短暂的停顿小阶段:

    《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

    1)并发标记(Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的 阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫这些)的 短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、Shenandoah不同的是,ZGC 的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志 位。

    2)并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出 本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。重分配集与G1收集器 的回收集(Collection Set)还是有区别的,ZGC划分Region的目的并非为了像G1那样做收益优先的增 量回收。相反,ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的 维护成本。因此,ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面 的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对 全堆的。此外,在JDK 12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。

    3)并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分 配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集器能仅从引用上就明 确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次 访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象 上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(SelfHealing)能力。这样做的好处是只有第一次访问旧对象会陷入转发,也就是只慢一次,对比 Shenandoah的*s转发指针,那是每次对象访问都必须付出的固定开销,简单地说就是每次都慢, 因此ZGC对用户程序的运行时负载要比Shenandoah来得更低一些。还有另外一个直接的好处是由于染 色指针的存在,一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于 新对象的分配(但是转发表还得留着不能释放掉),哪怕堆中还有很多指向这个对象的未更新指针也 没有关系,这些旧指针一旦被使用,它们都是可以自愈的。

    4)并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所 有引用,这一点从目标角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的并发重映射并不 是一个必须要“迫切”去完成的任务,因为前面说过,即使是旧引用,它也是可以自愈的,最多只是第 一次使用时多一次转发和修正操作。重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束 后可以释放转发表这样的附带收益),所以说这并不是很“迫切”。因此,ZGC很巧妙地把并发重映射 阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所 有对象的,这样合并就节省了一次遍历对象图[9]的开销。一旦所有指针都被修正之后,原来记录新旧 对象关系的转发表就可以释放掉了。

    低延迟为首要目标的ZGC已经达到了以高吞吐量为目标Parallel Scavenge 的99%,直接超越了G1。如果将吞吐量测试设定为面向SLA(Service Level Agreements)应用 的“Critical Throughput”的话[16],ZGC的表现甚至还反超了Parallel Scavenge收集器。

    《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

    《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

  2. 虚拟机及垃圾收集器日志

    在JDK 9以前,HotSpot并没 有提供统一的日志处理框架,虚拟机各个功能模块的日志开关分布在不同的参数上,日志级别、循环 日志大小、输出格式、重定向等设置在不同功能上都要单独解决。

    JDK 9开始,HotSpot所有功能的日志都收归到了“-Xlog”参数上

    -Xlog[:[selector][:[output][:[decorators][:output-options]]]]

    命令行中最关键的参数是选择器(Selector),它由标签(Tag)和日志级别(Level)共同组成。 标签可理解为虚拟机中某个功能模块的名字,它告诉日志框架用户希望得到虚拟机哪些功能的日志输 出。垃圾收集器的标签名称为“gc”,由此可见,垃圾收集器日志只是HotSpot众多功能日志的其中一 项,全部支持的功能模块标签名如下所示:

    !《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

    日志级别从低到高,共有Trace,Debug,Info,Warning,Error,Off六种级别,日志级别决定了输 出信息的详细程度,默认级别为Info,HotSpot的日志规则与Log4j、SLF4j这类Java日志框架大体上是 一致的。另外,还可以使用修饰器(Decorator)来要求每行日志输出都附加上额外的内容,支持附加 在日志行上的信息包括:

    ·time:当前日期和时间。

    ·uptime:虚拟机启动到现在经过的时间,以秒为单位。

    ·timemillis:当前时间的毫秒数,相当于System.currentTimeMillis()的输出。

    ·uptimemillis:虚拟机启动到现在经过的毫秒数。

    ·timenanos:当前时间的纳秒数,相当于System.nanoTime()的输出。

    ·uptimenanos:虚拟机启动到现在经过的纳秒数。

    ·pid:进程ID。

    ·tid:线程ID。

    ·level:日志级别。

    ·tags:日志输出的标签集。

  • JDK 9统一日志框架前、后是如何获得垃圾收集器过程的相关信 息

    1)查看GC基本信息,在JDK 9之前使用-XX:+PrintGC,JDK 9后使用-Xlog:gc:

    2)查看GC详细信息,在JDK 9之前使用-XX:+PrintGCDetails,在JDK 9之后使用-X-log:gc, 用通配符将GC标签下所有细分过程都打印出来,如果把日志级别调整到Debug或者Trace,还将获得更多细节信息:

    3)查看GC前后的堆、方法区可用容量变化,在JDK 9之前使用-XX:+PrintHeapAtGC,JDK 9之 后使用-Xlog:gc+heap=debug:

    4)查看GC过程中用户线程并发时间以及停顿的时间,在JDK 9之前使用-XX:+PrintGCApplicationConcurrentTime以及-XX:+PrintGCApplicationStoppedTime,JDK 9之后使用-Xlog: safepoint:

    5)查看收集器Ergonomics机制(自动设置堆空间各分代区域大小、收集目标等内容,从Parallel收 集器开始支持)自动调节的相关信息。在JDK 9之前使用-XX:+PrintAdaptive-SizePolicy,JDK 9之后 使用-Xlog:gc+ergo*=trace:

    6)查看熬过收集后剩余对象的年龄分布信息,在JDK 9前使用-XX:+PrintTenuring-Distribution, JDK 9之后使用-Xlog:gc+age=trace:

    !《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

  1. 垃圾收集器参数总结

    垃圾收集相关的常用参数

    《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略

    《深入理解java虚拟机》第3版笔记3
第3章 垃圾收集器与内存分配策略