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 时对象晋升到老年代的几种方式:
当年龄到达一定阈值后,对象晋升到老年代(MaxTenuringThreshold,JDK 1.8 中默认为 15,但默认值会根据不同的垃圾回收器而有所变化,如 CMS 默认为 6)
To Survivor 区空间不够存放存活的对象,部分对象直接进入老年代
根据动态年龄判断规则,如果 Survivor 区中一批对象总大小大于这块 Survivor 空间的 50%,按照年龄从大到小的顺序将对象晋升老年代直至 Survivor 可用空间超过 50%(TargetSurvivorRatio,如年龄1+年龄2+…+年龄n的对象总大小大于50%,则将年龄n及以上的对象放入老年代)
大对象直接进入老年代(PretenureSizeThreshold,这个参数只在 Serial 和 ParNew 垃圾回收器下有效)
垃圾回收算法
1. 复制
将内存划分为大小相同的两块,每次使用其中一块,垃圾回收时,将存活的对象从当前使用的一块内存复制到未使用的一块内存,然后清理使用的内存(年轻代中使用了这个算法)
缺点:
浪费空间
存活率较高时,复制效率较低
年轻代空间相对较小,且其中的对象大多数都不会继续存活,因此年轻代使用了该算法。
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 个过程:
初始标记:暂停应用程序线程,标记 GC Roots 直接引用的对象,耗时很短
并发标记:从 GC Roots 直接引用的对象遍历整个对象图,耗时很长但不需要停止应用程序线程,存在错标和漏标问题
重新标记:暂停应用程序线程,使用 “写屏障+增量更新算法” 解决并发标记阶段的漏标问题,耗时比初始标记稍长一些
并发清理:清理未标记的对象(这个阶段新创建的对象会被标记为黑色,不做处理)
并发重置:重置本次 GC 过程中的标记
优点:低停顿
缺点:
使用标记清除算法,会产生很多内存碎片(使用 -XX:UseCMSCompactAtFullCollection 可以让 JVM 执行完标记清除后做内存整理)
无法处理浮动垃圾(在并发标记和并发清理阶段新产生的垃圾,需要等到下一次 GC 来处理)
可能存在当前的垃圾回收任务还没执行完成,又启动了新的垃圾回收任务,导致 concurrent mode failure,此时会 STW,改用 Serial Old 来回收垃圾(一般在并发标记和并发清理阶段出现,并发执行的应用程序线程不断创建新对象,由于老年代空间不足触发了 Full GC,导致开启新的垃圾回收任务)
CMS 相关参数:
-XX:+UseConcMarkSweepGC:使用 CMS 垃圾回收器
-XX:ConcGCThreads:并发的 GC 线程数
-XX:+UseCMSCompactAtFullCollection:Full GC 之后做内存整理
-XX:CMSFullGCBeforeCompaction:多少次 Full GC 后做内存整理(默认为 0,表示每次 Full GC 都做内存整理,Full GC 频繁的应用应调大该值)
-XX:CMSInitiatingOccupancyFraction:老年代使用内存达到这个比例时触发 Full GC(默认为 92,百分比)
-XX:+UseCMSInitiatingOccupancyOnly:只使用 -XX:CMSInitiatingOccupancyFraction 设定的老年代回收阈值(JVM 默认只在第一次遵循设定的阈值触发回收,后续会自动调整回收阈值)
-XX:+CMCScavengeBeforeRemark:在 CMS GC 前执行一次 Minor GC,减少老年代对年轻代的跨代引用,降低 CMS 标记时的开销(CMS GC 耗时的 80% 在标记阶段)
-XX:+CMSParallelInitialMarkEnabled:CMS 使用多线程执行初始标记,目的是缩短 STW(默认开启)
-XX:+CMSParallelRemarkEnabled:CMS 使用多线程执行重新标记,目的是缩短 STW(默认开启)
三色标记算法
三色标记算法将对象标记成黑、灰、白三种颜色:
黑色:该对象已经被标记过,且该对象下的属性也全部都被标记过(存活对象)
灰色:该对象已经被标记过,但该对象下的属性没有全部被标记完(GC 需要从该对象出发去寻找垃圾)
白色:该对象没有被标记过(垃圾对象)
漏标问题:
漏标会导致程序需要的对象被当成垃圾回收掉,有两种解决方案:
增量更新(Increment Update):当黑色对象增加新的白色对象引用时,记录这个新增的引用,并发标记结束后,以新增引用的黑色对象为根,重新标记一次(CMS 使用该方案)【通过写屏障实现】
原始快照(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差别不大:
初始标记:暂停应用程序线程,标记 GC Roots 直接引用的对象,耗时很短
并发标记:从 GC Roots 直接引用的对象遍历整个对象图,耗时很长但不需要停止应用程序线程,存在错标和漏标问题
重新标记:暂停应用程序线程,使用 “写屏障+SATB原始快照算法” 解决并发标记阶段的漏标问题,耗时比初始标记稍长一些
清理/复制:暂停应用程序线程,计算Region的回收效益并排序一个优先列表(单位时间可回收的对象大小),根据用户的期望GC停顿时间来执行回收任务,垃圾对象很多的时候只会回收排在优先列表前面的部分Region(如用户期望最大停顿150ms,本次GC总共有1000个Region可回收,通过计算得出回收效益前800个Region的回收总耗时在150ms左右,则本次只回收这800个Region,剩下的200个Region下次GC再回收,存在浮动垃圾问题)
G1 垃圾回收方式
Minor GC:回收年轻代(Eden区放满并不一定触发,如果Eden区回收时间远小于 -XX:MaxGCPauseMillis 参数设定值,则给年轻代分配新的Region,直到某次Eden区放满后计算得出的回收时间接近 -XX:MaxGCPauseMillis 参数设定值,才触发 Minor GC/ Young GC)
Mixed GC:回收年轻代、部分老年代和大对象区,老年代使用占比达到 -XX:InitiatingHeapOccupancyPercent(默认45%)阈值时触发
Full GC:回收所有堆内存,会停止应用程序线程来单线程执行,非常耗时,应尽量避免触发
G1 参数
-XX:+UseG1GC:使用 G1 垃圾回收器
-XX:ParallelGCThread:垃圾回收线程数
-XX:G1HeapRegionSize:单个 Region 大小(1~32MB,必须是2的幂)
-XX:MaxGCPauseMillis:期望GC暂停时间(默认200ms)
-XX:G1NewSizePercent:年轻代初始内存占比(默认5%)
-XX:G1MaxNewSizePercent:年轻代最大内存占比(默认60%)
-XX:TargetSurvivorRatio:Survivor区满足动态年龄判断的容量占比(默认50%)
-XX:MaxTenuringThreshold:晋升分代年龄
-XX:InitiatingHeapOccupancyPercent:触发Mixed GC的老年代使用内存占比阈值(默认45%)
-XX:G1MixedGCLiveThresholdPercent:Region中存活对象占比低于该阈值才会回收该Region(默认85%)
-XX:G1MixedGCCountTarget:在一次GC中做几次独占清理,最后独占清理阶段会执行一会清理,暂停回收执行应用程序线程,一会又开始回收,循环个几次(默认8次)
-XX:G1HeapWastePercent:Mixed GC过程中,新空闲出来的Region占比该阈值时(默认5%),停止Mixed GC
ZGC 垃圾回收器
ZGC 是一款适用于超大内存的、比G1性能更加优秀的、面向未来的垃圾回收器(在 JDK 11 实验引入,在 JDK 15 正式使用,只能用于64位系统)
ZGC 重要特征:
暂停时间不超过10ms
最大支持16TB堆内存
对应用程序的吞吐量影响小于15%
奠定了未来GC的基石
内存多重映射
ZGC一次GC中地址视图的切换过程:
##### 染色指针
ZGC出现之前,GC信息保存在对象头的MarkWord中,而ZGC使用染色指针将GC信息保存在对象引用中,是寄存器访问,速度比内存访问更快。
染色指针是一种将少量信息存储在指针上的技术,在 64 位 JVM 中,对象地址是64位,高16位暂时不用来寻址,剩下48位的高4位存储4个标志位,最后44位用来寻址,最大支持16TB地址空间,即ZGC最大可以管理16TB内存:
64位对象地址:
16位:预留给以后使用
1位 :Finalizable,表示这个对象只能通过finalizer才能访问(与并发引用处理有关)
1位 :Remapped,设置此位的值后,表示对象未指向 relocation set 中(relocation set表示需要GC的Region集合)
1位 :Marked1,GC标记存活对象
1位 :Marked0,GC标记存活对象
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的,且停顿时间随存活对象的大小增加而增加。
如何选择垃圾回收器
内存很小(如100m):使用串行垃圾回收器
内存4G以下:可用 Parallel
内存4G~8G:可用 ParNew + CMS
内存8G~64G:可用 G1
内存64G以上:可用 ZGC
没有停顿时间要求,可用串行;停顿时间允许超过1s,可用并行;停顿时间要求尽量小,使用并发
使用 jvisualvm 观察 GC 过程
下载:https://visualvm.github.io/
安装 Visual GC 插件:Tools ➡️ Plugins ➡️ 找到 Visual GC 插件安装
使用:Local中找到要观察的程序 ➡️ 点击 Visual GC 选项,如下图所示:
JVM 内存参数
堆:-Xms、-Xmx
堆的新生代:-Xmn
方法区:-XX:MetaspaceSize、-XX:MaxMetaspaceSize(推荐设置,如果不设置,上限为本地内存可用大小)
栈:-Xss
使用举例:`java -Xms2048m -Xmx2048m -Xmn1024m -Xss512k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -jar demo.jar`