Java GC 笔记

Jul 14, 2020


读了这篇文章感觉对 Java GC 的理解透彻多了,所以就决定做个笔记

JVM 内存区域

  • 虚拟机栈、本地方法栈、程序计数器、Java 8 之后的本地内存不需要进行 GC
  • 堆是 GC 发生的区域

垃圾识别方法

  1. 引用计数法:对象被引用一次,则在它的对象头加一次引用次数,如果没有引用(引用次数为 0), 则对象可回收。但其无法解决循环引用的问题,所以不采用。
  2. 可达性算法:从一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点,这样通过 GC Root 串成的一条线就叫引用链,直到所有的结点都遍历完毕。如果对象不在任意一个以 GC Root 为起点的引用链中,则会被判断为「垃圾」,会被 GC 回收。

哪些对象可以作为 GC Root 呢?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

垃圾回收方法

  1. 标记清除法:先根据可达性算法标记出相应的可回收对象,再对可回收的对象进行回收。会形成较多的内存碎片,基本不用。
  2. 标记整理法:前两步和标记清除法一样,但它在标记清除法的基础上添加了一个整理的过程,即将所有的存活对象都往一端移动,再清理掉另一端的所有区域,这样就解决了内存碎片的问题。问题在于:每次垃圾清除都要频繁地移动存活的对象,效率十分低下。
  3. 复制算法:将堆均分为 A 和 B;先在 A 分配对象,B 不分配;清理时将 A 存活的对象复制到 B,然后清空 A;下次将 B 存活的对象复制到 A,清空 B。问题有二:一是可用空间只有一半;二是大量复制对象,性能低下。

分代收集算法

分代收集算法整合了以上算法的优点,最大程度避免了它们的缺点,所以是现代虚拟机采用的首选算法。与其说它是算法,倒不是说它是一种策略,因为它是把上述几种算法整合在了一起。 分代收集算法的基础在于:大部分对象都很短命,会在很短的时间内被回收(IBM 专业研究表明,一般来说,98% 的对象都是朝生夕死的,经过一次 Minor GC 后就会被回收)。 分代收集算法根据对象存活周期的不同将堆分成新生代和老生代(Java8以前还有个永久代),默认比例为 1 : 2;新生代又分为 Eden 区, from Survivor 区(简称S0),to Survivor 区(简称 S1),三者的比例为 8: 1 : 1。这样就可以根据新老生代的特点选择最合适的垃圾回收算法。 我们把新生代发生的 GC 称为 Young GC(也叫 Minor GC),老年代发生的 GC 称为 Old GC(也称为 Full GC)。

  1. 堆分为新生代和老生代(老年代),默认比例 1:2
  2. 新生代分为 Eden 区、from Survivor 区(S0)、to Survivor 区(S1),比例为 8:1:1
  3. 新生代的 GC 称为 Young GC(Minor GC),使用复制算法,因为在 Eden 区分配的对象大部分在 Minor GC 后都消亡了,只剩下极少部分存活对象,可以最大限度地降低复制算法造成的对象频繁拷贝带来的开销
  4. 老年代发生的 GC 称为 Old GC(Full GC),使用标记整理法
  5. S0 或 S1 中的存活对象每次 Minor GC 都可能被回收
  6. 对象的年龄即发生 Minor GC 的次数

YGC 的过程

  1. 对象一般分配在 Eden 区,当 Eden 区将满时,触发 Minor GC
  2. 经过 Minor GC 后只有少部分对象会存活,它们会被移到 S0 区,同时对象年龄加一,最后把 Eden 区对象全部清理以释放出空间
  3. 下一次 Minor GC 时,会把 Eden 区的存活对象和 S0 中的存活对象一起移到 S1,同时对象年龄加一,最后清空 Eden 和 S0 的空间
  4. 若再触发下一次 Minor GC,则重复上一步,只不过此时变成了从 Eden、S1 区将存活对象复制到 S0 区。每次垃圾回收,S0、S1 角色互换,都是从 Eden、S0(或S1) 将存活对象移动到 S1(或S0)

对象晋升

  • 当对象的年龄达到了设定的阈值,则会从S0(或S1)晋升到老年代
  • 当某个对象分配需要大量的连续内存时,此时对象的创建不会分配在 Eden 区,会直接分配在老年代
  • 在 S0(或S1) 区相同年龄的对象大小之和大于 S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代

Stop The World

如果老年代满了,会触发 Full GC,Full GC 会同时回收新生代和老年代(即对整个堆进行GC),它会导致 Stop The World(简称 STW),造成挺大的性能开销。
什么是 STW ?所谓的 STW,即在 GC(minor GC 或 Full GC)期间,只有垃圾回收器线程在工作,其他工作线程则被挂起。

Safe Point

由于 Full GC(或 Minor GC)会影响性能,所以我们要在一个合适的时间点发起 GC,这个时间点被称为 Safe Point。
这个时间点的选定既不能太少以让 GC 时间太长导致程序过长时间卡顿,也不能过于频繁以至于过分增大运行时的负荷。
一般当线程在这个时间点上状态是可以确定的,如确定 GC Root 的信息等,可以使 JVM 开始安全地 GC。
Safe Point 主要指的是以下特定位置:

  • 循环的末尾
  • 方法返回前
  • 调用方法的 call 之后
  • 抛出异常的位置

垃圾收集器种类

  • 在新生代工作的垃圾回收器:Serial,ParNew,ParallelScavenge
  • 在老年代工作的垃圾回收器:CMS,Serial Old,Parallel Old
  • 同时在新老生代工作的垃圾回收器:G1

图片中的垃圾收集器如果存在连线,则代表它们之间可以配合使用

新生代收集器

  1. Serial 收集器是工作在新生代的、单线程的垃圾收集器,对于运行在 Client 模式下的虚拟机,Serial 收集器是新生代的默认收集器
  2. ParNew 收集器是 Serial 收集器的多线程版本,主要工作在 Server 模式,是许多运行在 Server 模式下的虚拟机的首选新生代收集器
  3. Parallel Scavenge 收集器也是一个使用复制算法、多线程、工作于新生代的垃圾收集器,和 ParNew 收集器不同的是,Parallel Scavenge 的目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)),所以更适合做后台运算等不需要太多用户交互的任务

老年代收集器

  1. Serial Old 是工作于老年代的单线程收集器,主要给 Client 模式下的虚拟机使用。如果在 Server 模式下,它还有两大用途:一是在 JDK 1.5 及之前的版本中与 Parallel Scavenge 配合使用;另一种是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用
  2. Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理法,这两者的组合由于都是多线程收集器,真正实现了「吞吐量优先」的目标
  3. CMS 收集器是以实现最短 STW 时间为目标的收集器,如果应用很重视服务的响应速度,希望给用户最好的体验,则 CMS 收集器是个很不错的选择!CMS 虽然工作于老年代,但采用的是标记清除法

G1(Garbage First) 收集器

G1 收集器是面向服务端的垃圾收集器,被称为驾驭一切的垃圾回收器,主要有以下几个特点

  • 像 CMS 收集器一样,能与应用程序线程并发执行
  • 整理空闲空间更快
  • 需要 GC 停顿时间更好预测
  • 不会像 CMS 那样牺牲大量的吞吐性能
  • 不需要更大的 Java Heap

为什么 G1 能建立可预测的停顿模型呢?主要原因在于 G1 对堆空间的分配与传统的垃圾收集器不一样。G1 各代的存储地址不是连续的,每一代都使用了 n 个不连续的大小相同的 Region,每个 Region 占有一块连续的虚拟内存地址,如图所示:

除了和传统的新老生代、幸存区的空间区别,Region还多了一个 H,它代表 Humongous,这表示这些 Region 存储的是巨大对象(humongous object,H-obj),即大小大于等于 region 一半的对象,这样超大对象就直接分配到了老年代,防止了反复拷贝移动。那么 G1 分配成这样有啥好处呢?
传统的收集器如果发生 Full GC 是对整个堆进行全区域的垃圾收集,而分配成各个 Region 的话,方便 G1 跟踪各个 Region 里垃圾堆积的价值大小(回收所获得的空间大小及回收所需经验值),这样根据价值大小维护一个优先列表,根据允许的收集时间,优先收集回收价值最大的 Region,也就避免了整个老年代的回收,也就减少了 STW 造成的停顿时间。同时由于只收集部分 Region,就做到了 STW 时间的可控。

总结

在生产环境中我们要根据不同的场景来选择垃圾收集器组合
如果是运行在桌面环境处于 Client 模式的,则用 Serial + Serial Old 收集器绰绰有余
如果需要响应时间快,用户体验好的,则用 ParNew + CMS 的搭配模式
即使是号称是「驾驭一切」的 G1,也需要根据吞吐量等要求适当调整相应的 JVM 参数
没有最牛的技术,只有最合适的使用场景