文章

JVM 垃圾回收

垃圾对象判断算法

引用计数法

对象被几个对象引用,它的引用计数就为几,这种算法存在循环引用问题。

可达性分析(JVM使用这个算法)

以 “GC Roots 对象” 为起点,往下搜索引用的对象,将找到的对象都标记为非垃圾对象,则其余未标记的对象都是需要回收的垃圾对象。

GC Roots 根节点有:静态变量、虚拟机栈局部变量、本地方法栈的变量等

垃圾回收过程

新对象首先分配在 Eden 区,当 Eden 区空间不足时,会触发 Minor GC,通过可达性算法标记 Eden 区和 From Survivor 区中的存活对象,将这些存活的对象复制到 To Survivor 区,随后清空 Eden 区和 From Survivor 区。新生代中的对象每熬过一次 Minor GC,其分代年龄就会加1。根据某些规则,年轻代的对象可以晋升到老年代。老年代用于存放长期存活的对象,当老年代空间不足时,会触发 Major GC 或 Full GC,Full GC 涉及整个堆内存的回收,STW(Stop the World) 时间会比较长。

Minor GC 时对象晋升到老年代的几种方式:

  1. 当年龄到达一定阈值后,对象晋升到老年代(MaxTenuringThreshold,JDK 1.8 中默认为 15,但默认值会根据不同的垃圾回收器而有所变化,如 CMS 默认为 6)

  2. To Survivor 区空间不够存放存活的对象,部分对象直接进入老年代

  3. 根据动态年龄判断规则,如果 Survivor 区中一批对象总大小大于这块 Survivor 空间的 50%,按照年龄从大到小的顺序将对象晋升老年代直至 Survivor 可用空间超过 50%(TargetSurvivorRatio,如年龄1+年龄2+…+年龄n的对象总大小大于50%,则将年龄n及以上的对象放入老年代)

  4. 大对象直接进入老年代(PretenureSizeThreshold,这个参数只在 Serial 和 ParNew 垃圾回收器下有效)

垃圾回收算法

1. 复制

将内存划分为大小相同的两块,每次使用其中一块,垃圾回收时,将存活的对象从当前使用的一块内存复制到未使用的一块内存,然后清理使用的内存(年轻代中使用了这个算法)

缺点:

  1. 浪费空间

  2. 存活率较高时,复制效率较低

年轻代空间相对较小,且其中的对象大多数都不会继续存活,因此年轻代使用了该算法。

2. 标记整理

标记所有存活的对象,将它们移动到内存的一端,清空边界另一端的内存(老年代使用了这个算法)

缺点:算法复杂度较高

3. 标记清除

标记所有存活的对象,清除其他未被标记的对象

缺点:产生大量不连续的内存碎片

垃圾回收器

垃圾回收器会使用上面的垃圾回收算法,但是由于不同的垃圾回收器侧重不同,故其实现细节不同

串行垃圾回收器

串行垃圾回收器时最古老的垃圾回收器,它是单线程回收器,只会使用一个线程去完成垃圾回收工作,且它在回收垃圾的时候会暂停所有的应用程序线程(Stop The World)。

其年轻代回收器(Serial,-XX:UseSerialGC)使用复制算法,老年代回收器(Serial Old,-XX:UseSerialOldGC)使用标记整理算法

串行垃圾回收器实现简单,适用于占用内存不太大的应用。

并行垃圾回收器

并行垃圾回收器使用多线程进行垃圾回收,它关注的是吞吐量(CPU利用率),而CMS关注的是减少应用程序线程的停顿时间。

缺点:STW 时间比较长,因此诞生了 CMS 来降低 STW 时间

Parallel 垃圾回收器

默认垃圾回收线程数和CPU核数一致,也可通过 -XX:ParallelGCThreads 参数修改GC线程数,但不推荐。

其年轻代回收器(Parallel Scavenge,-XX:UseParallelGC)使用复制算法,老年代回收器(Parallel Old,-XX:UseParallelOldGC)使用标记整理算法

ParNew 垃圾回收器

ParNew 类似 Parallel 垃圾回收器,区别是它可以跟 CMS 搭配使用

通过 -XX:UseParNewGC 参数使用 ParNew 垃圾回收器

很多互联网公司使用 ParNew + CMS

并发垃圾回收器

CMS 垃圾回收器

CMS(Concurrent Mark Sweep)是一款旨在最小化回收停顿时间的老年代垃圾回收器,通过名字中的 Mark Sweep 可知它使用的是标记清除算法,它回收垃圾包括以下 5 个过程:

  1. 初始标记:暂停应用程序线程,标记 GC Roots 直接引用的对象,耗时很短

  2. 并发标记:从 GC Roots 直接引用的对象遍历整个对象图,耗时很长但不需要停止应用程序线程,存在错标和漏标问题

  3. 重新标记:暂停应用程序线程,使用 “写屏障+增量更新算法” 解决并发标记阶段的漏标问题,耗时比初始标记稍长一些

  4. 并发清理:清理未标记的对象(这个阶段新创建的对象会被标记为黑色,不做处理)

  5. 并发重置:重置本次 GC 过程中的标记

优点:低停顿

缺点:

  1. 使用标记清除算法,会产生很多内存碎片(使用 -XX:UseCMSCompactAtFullCollection 可以让 JVM 执行完标记清除后做内存整理)

  2. 无法处理浮动垃圾(在并发标记和并发清理阶段新产生的垃圾,需要等到下一次 GC 来处理)

  3. 可能存在当前的垃圾回收任务还没执行完成,又启动了新的垃圾回收任务,导致 concurrent mode failure,此时会 STW,改用 Serial Old 来回收垃圾(一般在并发标记和并发清理阶段出现,并发执行的应用程序线程不断创建新对象,由于老年代空间不足触发了 Full GC,导致开启新的垃圾回收任务)

CMS 相关参数:

  1. -XX:+UseConcMarkSweepGC:使用 CMS 垃圾回收器

  2. -XX:ConcGCThreads:并发的 GC 线程数

  3. -XX:+UseCMSCompactAtFullCollection:Full GC 之后做内存整理

  4. -XX:CMSFullGCBeforeCompaction:多少次 Full GC 后做内存整理(默认为 0,表示每次 Full GC 都做内存整理,Full GC 频繁的应用应调大该值)

  5. -XX:CMSInitiatingOccupancyFraction:老年代使用内存达到这个比例时触发 Full GC(默认为 92,百分比)

  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用 -XX:CMSInitiatingOccupancyFraction 设定的老年代回收阈值(JVM 默认只在第一次遵循设定的阈值触发回收,后续会自动调整回收阈值)

  7. -XX:+CMCScavengeBeforeRemark:在 CMS GC 前执行一次 Minor GC,减少老年代对年轻代的跨代引用,降低 CMS 标记时的开销(CMS GC 耗时的 80% 在标记阶段)

  8. -XX:+CMSParallelInitialMarkEnabled:CMS 使用多线程执行初始标记,目的是缩短 STW(默认开启)

  9. -XX:+CMSParallelRemarkEnabled:CMS 使用多线程执行重新标记,目的是缩短 STW(默认开启)

三色标记算法

三色标记算法将对象标记成黑、灰、白三种颜色:

  • 黑色:该对象已经被标记过,且该对象下的属性也全部都被标记过(存活对象

  • 灰色:该对象已经被标记过,但该对象下的属性没有全部被标记完(GC 需要从该对象出发去寻找垃圾

  • 白色:该对象没有被标记过(垃圾对象

漏标问题:

漏标会导致程序需要的对象被当成垃圾回收掉,有两种解决方案:

  1. 增量更新(Increment Update)当黑色对象增加新的白色对象引用时,记录这个新增的引用,并发标记结束后,以新增引用的黑色对象为根,重新标记一次CMS 使用该方案)【通过写屏障实现】

  2. 原始快照(SATB, Snapshot At The Beginning)当灰色对象删除白色对象引用时,记录这个要删除的引用,并发标记结束后,以删除引用的灰色对象为根(此时能读取到已删除的白色对象引用),重新标记一次G1 使用该方案,已删除的白色对象引用会标记为黑色,可能成为浮动垃圾,下次 GC 再回收)【通过写屏障实现】

写屏障:它不同于内存屏障,是 JVM 代码层面的实现,类似于 AOP,在变量赋值这种写操作前后调用 xxx_barrier() 方法做一些额外操作

G1 垃圾回收器

G1 是一款针对多处理器和大内存机器的垃圾回收器,原则是收集尽可能多的垃圾(Garbage First),特点是高吞吐和可自定义GC停顿时间的特征。

通过 -XX:MaxGCPauseMillis 参数指定期望的GC停顿时间,默认 200ms

G1 内存布局

G1 将 Java 堆划分为很多个大小相等的 Region,一般为 2048 个。

如堆内存为 4096M,则每个 Region 大小为 2M(4096 / 2048),也可用 -XX:G1HeapRegionSize 参数指定 Region 大小(不推荐)。

G1 保留了分代概念,它们都是 Region 的集合(不一定连续)。

默认年轻代占堆内存的比例为 5%,可以通过 -XX:G1NewSizePercent 参数指定年轻代初始占比,JVM 在运行过程中会动态给年轻代分配更多的 Region,但最多不超过堆内存的 60%,可以通过 -XX:G1MaxNewSizePercent 参数调整最大占比。

年轻代的Eden和Survivor比例也是 8 : 1 : 1。

G1 Minor GC 对象晋升老年代的规则不变,但是对大对象的处理不同,它有专门存放大对象的Humongous Region,而不是放到老年代。

当一个对象超过一个Region大小的50%时,这个对象被认为是大对象,会被放入一个或多个连续的Humongous Region。

Full GC除了回收年轻代和老年代,也会回收Humongous区。

G1 使用的是复制算法,不会有CMS的内存碎片问题。(从整体上看使用的是标记整理算法)

G1 垃圾回收过程

G1 垃圾回收过程跟CMS差别不大:

  1. 初始标记:暂停应用程序线程,标记 GC Roots 直接引用的对象,耗时很短

  2. 并发标记:从 GC Roots 直接引用的对象遍历整个对象图,耗时很长但不需要停止应用程序线程,存在错标和漏标问题

  3. 重新标记:暂停应用程序线程,使用 “写屏障+SATB原始快照算法” 解决并发标记阶段的漏标问题,耗时比初始标记稍长一些

  4. 清理/复制:暂停应用程序线程,计算Region的回收效益并排序一个优先列表(单位时间可回收的对象大小),根据用户的期望GC停顿时间来执行回收任务,垃圾对象很多的时候只会回收排在优先列表前面的部分Region(如用户期望最大停顿150ms,本次GC总共有1000个Region可回收,通过计算得出回收效益前800个Region的回收总耗时在150ms左右,则本次只回收这800个Region,剩下的200个Region下次GC再回收,存在浮动垃圾问题)

G1 垃圾回收方式
  1. Minor GC:回收年轻代(Eden区放满并不一定触发,如果Eden区回收时间远小于 -XX:MaxGCPauseMillis 参数设定值,则给年轻代分配新的Region,直到某次Eden区放满后计算得出的回收时间接近 -XX:MaxGCPauseMillis 参数设定值,才触发 Minor GC/ Young GC)

  2. Mixed GC:回收年轻代、部分老年代和大对象区,老年代使用占比达到 -XX:InitiatingHeapOccupancyPercent(默认45%)阈值时触发

  3. Full GC:回收所有堆内存,会停止应用程序线程来单线程执行,非常耗时,应尽量避免触发

G1 参数
  1. -XX:+UseG1GC:使用 G1 垃圾回收器

  2. -XX:ParallelGCThread:垃圾回收线程数

  3. -XX:G1HeapRegionSize:单个 Region 大小(1~32MB,必须是2的幂)

  4. -XX:MaxGCPauseMillis:期望GC暂停时间(默认200ms)

  5. -XX:G1NewSizePercent:年轻代初始内存占比(默认5%)

  6. -XX:G1MaxNewSizePercent:年轻代最大内存占比(默认60%)

  7. -XX:TargetSurvivorRatio:Survivor区满足动态年龄判断的容量占比(默认50%)

  8. -XX:MaxTenuringThreshold:晋升分代年龄

  9. -XX:InitiatingHeapOccupancyPercent:触发Mixed GC的老年代使用内存占比阈值(默认45%)

  10. -XX:G1MixedGCLiveThresholdPercent:Region中存活对象占比低于该阈值才会回收该Region(默认85%)

  11. -XX:G1MixedGCCountTarget:在一次GC中做几次独占清理,最后独占清理阶段会执行一会清理,暂停回收执行应用程序线程,一会又开始回收,循环个几次(默认8次)

  12. -XX:G1HeapWastePercent:Mixed GC过程中,新空闲出来的Region占比该阈值时(默认5%),停止Mixed GC

ZGC 垃圾回收器

ZGC 是一款适用于超大内存的、比G1性能更加优秀的、面向未来的垃圾回收器(在 JDK 11 实验引入,在 JDK 15 正式使用,只能用于64位系统)

ZGC 重要特征:

  1. 暂停时间不超过10ms

  2. 最大支持16TB堆内存

  3. 对应用程序的吞吐量影响小于15%

  4. 奠定了未来GC的基石

内存多重映射

ZGC一次GC中地址视图的切换过程:

##### 染色指针

ZGC出现之前,GC信息保存在对象头的MarkWord中,而ZGC使用染色指针将GC信息保存在对象引用中,是寄存器访问,速度比内存访问更快。

染色指针是一种将少量信息存储在指针上的技术,在 64 位 JVM 中,对象地址是64位,高16位暂时不用来寻址,剩下48位的高4位存储4个标志位,最后44位用来寻址,最大支持16TB地址空间,即ZGC最大可以管理16TB内存:

64位对象地址:

  1. 16位:预留给以后使用

  2. 1位   :Finalizable,表示这个对象只能通过finalizer才能访问(与并发引用处理有关)

  3. 1位   :Remapped,设置此位的值后,表示对象未指向 relocation set 中(relocation set表示需要GCRegion集合)

  4. 1位   :Marked1,GC标记存活对象

  5. 1位   :Marked0,GC标记存活对象

  6. 44位:对象的地址(所以它可以支持2^42=4T内存)

染色指针不支持指针压缩

读屏障

在GC的并发转移阶段,GC线程在转移对象的同时,应用程序线程也在访问对象,应用程序线程有可能访问到已经转移但是地址未及时更新的对象,如果不做处理将发生错误。在ZGC中,应用程序线程访问对象是,会被读屏障拦截,如果发现对象已经转移,但是对象地址还是旧的,会通过转发表将对象访问转发到新复制的对象上,同时修正该对象引用的值,使其直接指向新对象。

ZGC内存布局

Large:4MB及以上

ZGC垃圾回收过程

ZGC只有三个STW阶段:初始标记重新标记初始转移

其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;

重新标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。

即ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。

与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

如何选择垃圾回收器

  1. 内存很小(如100m):使用串行垃圾回收器

  2. 内存4G以下:可用 Parallel

  3. 内存4G~8G:可用 ParNew + CMS

  4. 内存8G~64G:可用 G1

  5. 内存64G以上:可用 ZGC

  6. 没有停顿时间要求,可用串行;停顿时间允许超过1s,可用并行;停顿时间要求尽量小,使用并发

使用 jvisualvm 观察 GC 过程

  1. 下载:https://visualvm.github.io/

  2. 安装 Visual GC 插件:Tools ➡️ Plugins ➡️ 找到 Visual GC 插件安装

  3. 使用:Local中找到要观察的程序 ➡️ 点击 Visual GC 选项,如下图所示:

JVM 内存参数

  1. 堆:-Xms、-Xmx

  2. 堆的新生代:-Xmn

  3. 方法区:-XX:MetaspaceSize、-XX:MaxMetaspaceSize(推荐设置,如果不设置,上限为本地内存可用大小)

  4. 栈:-Xss

使用举例:`java -Xms2048m -Xmx2048m -Xmn1024m -Xss512k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -jar demo.jar`

License:  CC BY 4.0