垃圾收集器概述¶
有哪些常见的垃圾收集器?
他们的工作原理是什么?
不同场景,如何选择垃圾收集器?
常见 GC¶
查看当前 GC¶
JDK 默认 GC¶
java -XX:+PrintCommandLineFlags -version
输出(JDK 7/8):
-XX:+UseParallelGC
Java 进程 GC¶
jcmd 48617 VM.flags
jinfo -flags 48617
输出(JDK 9-22):
-XX:+UseG1GC
串行收集器¶
启用方式: -XX:+UseSerialGC
Serial : 新生代,复制算法
SerialOld: 老年代,标记整理算法
串行收集器是最早的垃圾收集器,在内存很小的单处理器上很有用。
并行收集器¶
启用方式:
-XX:+UseParallelGC
(Parallel Scavenge + SerialOld)JDK 7-8 默认值-XX:+UseParallelOldGC
(Parallel Scavenge + Parallel Old)
ParNew : Serial 的多线程版,使用复制算法回收新生代垃圾
Parallel Scavenge : 也可以看作 Serial 的多线程版,使用复制算法回收新生代垃圾,但是可以通过参数控制吞吐率
Parallel Old: ParNew 的老年代版本,使用标记整理算法回收老年代垃圾
CMS(Concurrent Mark Sweep)¶
启用方式: -XX:+UseConcMarkSweepGC
(ParNew + CMS + SerialOld)
CMS 是使用标记清除算法进行老年代垃圾回收的,因为耗时最长的并发标记和并发清除都是和用户线程并发执行的,所以可以减少Stop the World
的时间,但是标记清除算法很容易产生很多内存碎片,容易触发 Full GC,这也是为什么还需要搭配 SerialOld 的原因。
备注
遗憾的是,还没有来得及将 CMS 作为默认垃圾回收器,G1 就出来了,JDK 7/8 默认使用 Parallel Scavenge + SerialOld, JDK 9-22 都将 G1 作为默认回收器,虽然后面又出了ZGC,但 ZGC 为了将停顿时间降低到 1ms 内,但目前普适性最高的还是 G1。
初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
并发清除: 不需要停顿。
G1¶
启用方式: -XX:+UseG1GC
G1 是目前主推的收集器,JDK 9-22 都将其作为默认收集器,它能够以高概率满足暂停时间目标。
堆被分为新生代和老年代,之前介绍的回收器回收范围只能是新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
初始标记
并发标记
最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
筛选回收: 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
ZGC¶
启用方式: XX:+UseZGC -XX:+ZGenerational
备注
新版本的 ZGC 是基于分代实现的,所以需要使用 XX:+UseZGC -XX:+ZGenerational
启用。
GC 周期¶
ZGC 提供低于一毫秒的最大暂停时间,主要是因为 ZGC 只是在停顿时进行标记操作。
染色指针¶
读屏障¶
之前的GC都是采用写屏障(Write Barrier),而ZGC采用的是读屏障。
读屏障(Load Barriers)类似于 Spring AOP 的前置通知。
在ZGC中,当读取处于重分配集的对象时,会被读屏障拦截,通过转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为叫做指针的「自愈能力」。
这样就算GC把对象移动了,读屏障也会发现并修正指针,于是应用代码就永远都会持有更新后的有效指针,而且不需要STW,类似JDK里的CAS自旋,读取的值发现已经失效了,需要重新读取。
好处是:第一次访问旧对象访问会变慢,但也只会有一次变慢,当「自愈」完成后,后续访问就不会变慢了。
正是因为Load Barriers的存在,所以会导致配置ZGC的应用的吞吐量会变低。不过这点开销是值得的。