相关概念认识

  • 引用计数算法(Reference Counting)
  • 标记清除算法(Mark and Sweep)
  • 可达性分析算法
  • 在Java技术体系里面,固定可作为GC Roots的对象包括以下几种
    • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
    • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
    • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
    • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
    • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
    • 所有被同步锁(synchronized关键字)持有的对象。
    • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  • Java 引用
    • 强引用(StronglyRe-ference)
    • 软引用(Soft Reference)
    • 弱引用(Weak Reference)
    • 虚引用(Phantom Reference)
  • 衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency)
  • Brooks Pointers 转发指针
  • Colored Pointer 染色指针技术

一、垃圾收集器

1.1 串行收集器

介绍

串行收集器是所有垃圾回收器中最古老的一种,也是 JDK 中最基本的垃圾回收器之一。
串行回收器是指使用单线程进行垃圾回收的回收器。每次回收时,串行回收器只有一个工作线程,对于并行能力较弱的计算机来说,串行回收器的专注性和独占性往往有更好的性能表现。 串行回收器可在新生代和老年代使用,根据作用于不同的堆空间,分为新生代串行回收器和老年代串行回收器。

Serial
  • 在JDk1.3之前是新生代收集的唯一选择
  • 单线程,只会使用一个CPU去完成
  • 垃圾收集时,必须暂停其他工作线程,直到它收集结束。“Stop The World”
  • 虚拟机后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉
  • 到现在为止,是虚拟机运行在Client模式下的默认新生代收集器
  • 通过JVM参数 -XX:+UseSerialGC可以使用串行垃圾回收器。

Serial Old
  • 是Serial收集器的老年代版本,是单线程收集器,采用“标记-整理算法”
  • 主要意义也是在于给Client模式下的虚拟机使用
  • 虚拟机启动参数
    • -XX:+UseSerialGC 新生代、老年代都使用串行回收器
    • -XX:+UseParNewGC 新生代使用 ParNew 收集器,老年代使用串行收集器
    • -XX:+UseParallelGC 新生代使用 ParallelGC收集器,老年代使用串行收集器

Serial与Serial Old工作过程如图:

Java虚拟机相关图示-Serial与Serial Old工作过程.png

1.2 并行收集器

介绍

井行回收器在串行回收器的基础上做了改进,它使用多个线程同时进行垃圾回收, 对于并行能力强的计算机,可以有效缩短垃圾回收所需的实际时间

新生代 ParNew 回收器

ParNew 回收器是一个工作在新生代的垃圾收集器。它只是简单地将串行回收器多线程化,它的回收策略、算法以及参数和新生代串行回收器一样。

  • ParNew 回收器相关虚拟机启动参数
    • -XX:+UseParNewGC : 新生代使用 ParNew 回收器,老年代使用串行回收器。
    • -XX:+UseConcMarkSweepGC 新生代使用 ParNew 回收器,老年代使用 CMS
    • -XX:ParallelGCThreads 参数指定ParNew 回收器的线程数。一般,最好与CPU 数量相当,避免过多的线程数 影响垃圾收集性能。在默认情况下,当CPU 数量小于个时, ParallelGCThreads 的值等于 CPU数量 ,当 CPU 数量大于8个时, ParallelGCThreads值等于 3+((5*CPU数)/8 )。

新生代Parallel Scavenge收集器

新生代 ParallelGC回收器也是使用标记-复制算法的收集器。从表面上看,它和 ParNew 回收器一样,都是多线程、独占式的收集器 但是, ParallelGC 回收器有 个重要的特点:它非常关注系统的吞吐量

  • 启用参数
    • -XX:+UseParallelGC 新生代使用ParallelGC回收器,老年代使用串行回收器
    • -XX:+UseParallelOldGC 新生代使用ParallelGC回收器,老年代使用 ParallelOldGC回收器
  • 用于于控制系统的吞吐量参数
    • -XX:MaxGCPauseMillis 设置最大垃圾收集停顿时间(值大于0)。设置值越小停顿的时间越小,但会导致GC频繁,且增加GC总时间,从而降低吞吐量。
    • -XX GCTimeRatio 设置吞吐量大小(范围0~100)。
    • -XX:+UseAdaptiveSizePolicy 可以打开自适应 GC 策略。

老年代 ParallelOldGC 回收器

老年代ParallelOldGC 回收器也是一种多线程并发的收集器。和新生代 ParallelGC回收器一样,它也是一种关注吞吐量的收集器。
ParallelOldGC 回收器使用标记压缩算法,它在 JDK6中才可以使用。

  • 启动参数
    • -XX:+UseParallelOldGC 新生代使用ParallelGC回收器,老年代使用 ParallelOldGC回收器

1.3 CMS收集器(Concurrent Mark Sweep)

工作流程示意图

image.png

根据标记清除算法,初始标记、并发标记和重新标记都是为了标记出需要回收的对象。井发清理则是在标记完成后,正式回收垃圾对象。并发重置是指在垃圾回收完成后,重新初始化CMS 数据结构和数据,为下一次垃圾回收做好准备。并发标记和并发清理和并发重置都是可以和应用程序线程一起执行

  • 启动参数
    • -XX:+UseConcMarkSweepGC启用CMS
    • -XX:-CMSPrecleaningEnabled, 关闭预清理
    • -XX:ConcGCThreads 或 -XX:ParallelCMSThreads  设置并发线程数量。CMS的默认并发线程数是(ParallelGCThreads+3 )/4。 ParallelGCThreads 表示 GC 并行时使用的线程数 ,如果新生代使用 ParNew ,那么 ParallelGCThreads也就是新生代 GC 的线程数量。
    • -XX:CMSinitiatingOccupancyFraction 回收阀值,默认68, 即当老年代空间使用率达到 68%时,就会触发GC
    • -XX:+UseCMSCompactAtFullCollection(JDK8,已弃用)开启碎片整理
    • -XX:CMSFullGCsBeforeCompaction(JDK8,已弃用) 指定CMS回收多少次后,才进行碎片整理

1.4 G1 收集器

G1( Garbage-First )是在 JDK 1.7 中正式使用的全新的垃圾回收器,从长期目标来看,它是为了取代 CMS 回收器。

  • 特点

    • 井行性: G1在回收期间,可以由多个 GC 线程同时工作,有效利用多核计算能力。
    • 并发性: G1拥有与应用程序交互执行的能力,部分工作可以和应用程序同时执行,因此一般来说,不会在整个回收期间完全阻塞应用程序。
    • 分代 GC: G1 依然是一个分代收集器,但是和之前回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,它们或者工作在年轻代,或者工作在老年代。
    • 空间整理: G1在回收过程中,会进行适当的对象移动,不像 CMS ,只是简单地标记清理对象,在若干次 GC 后, CMS 必须进行一次碎片整理。而 G1不同,它每次回收都会有效地复制对象,减少空间碎片。
    • 可预见性:由于分区的原因, GI1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿也能得到较好的控制。
  • G1回收过程

    • 1.新生代GC
    • 2.并发标记周期
    • 3.混合收集
      1. 如果需要,可能会进行 Full GC
  • G1新生代GC图示

image.png
G1新生代GC 主要回收eden 和survivor。 一旦eden 被沾满则会触发GC。

  • G1 的并发标记周期

    • 初始标记: 标记从根节点直接可达的对象 这个阶段会伴随一次新生代 GC ,它是会产生全局停顿的,应用程序线程在这个阶段必须停止执行
    • 根区域扫描:这个过程是可以和应用程序并发执行的是根区域扫描不能和新生代 GC 同时执行 (因为根区域扫描依赖 survivor 区的对象,而新生代 GC 会修改这个区域),因此如果恰巧在此时需要进行新生代 GC, GC 就需要等待根区域扫描结束后才能进行,如果发生这种情况,这次新生代 GC 的时间就会延长
    • 井发标记::和 CMS 类似,并发标记将会扫描并查找整个堆的存活对象,并做好标记。这是一个并发的过程,并且这个过程可以被一次新生代GC打断
    • 重新标记:和CMS一样,重新标记也是会产生应用程序**停顿**的。由于在并发标记过程中,应用程序依然在运行,因此标记结果可能需要进行修正,所以在此对上一次的标记结果进行补充。在G1 中,这个过程使用 SATB (Snapshot At-The-Beginning )算法完成,G1会在标记之初为存活对象创建一个快照,这个快照有助于加速重新标记的速度。
    • 独占清理:这个阶段是会引起停顿的, 它将计算各个区域的存活对象和 GC 回收比例并进行排序,识别可供混合回收的区域。在这个阶段,还会更新记忆集(Remebered Set)该阶段给出了需要被混合回收的区域并进行了标记,在混合回收阶段,需要这些信息。
    • 井发清理阶段:这里会识别并清理完全空闲的区域,它是井发的清理,不会引起停顿
  • G1回收周期图示

image.png

  • G1 启动参数
    • -XX:+UseG1GC 启用G1回收器
    • -XX:MaxGCPauseMillis 最大停顿时间
    • -XX:ParallelGCThreads GC并发线程数
    • -XX:InitiatingHeapOccupancyPercent 参数可以指定当整个堆使用率达到多少时,触发并发标记周期的执行。默认值是 45
    • -XX: DisableExplicitGC 禁用手动调用System.gc()
    • -XX:MaxTenuringThreshold=15 新生代对象最大年龄,超过还没被回收则进入老年代

1.5 Shenandoah收集器

  • 工作过程 (并发标记、并发回收、并发引用更新)
    • 初始标记(Initial Marking):与G1一样,首先标记与GC Roots直接关联的对象,这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只与GCRoots的数量相关。
    • 并发标记(Concurrent Marking):与G1一样,遍历对象图,标记出全部可达的对象,这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
    • 最终标记(Final Marking):与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停顿
    • 并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate GarbageRegion)。
    • 并发回收(Concurrent Evacuation):并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异。在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中
    • 初始引用更新(Initial Update Reference):并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。
    • 并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作,这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。
    • 最终引用更新(Final Update Reference):解决了堆中的引用更新后,还要修正存在于GC Roots中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。
    • 并发清理(Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,这些Region都变成Immediate GarbageRegions了,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用。
  • 工作过程图示

image.png

1.6 ZGC 收集器

  • 运作过程
    • 并发标记(Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫这些)的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、Shenandoah不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位
    • 并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)
    • 并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。
    • 并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用
  • 图示

image.png

二、实战:内存分配与回收策略


参考

  • 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》