Java Full GC 触发条件、原因追踪排查及避免或注意事项详解

java 的 Full GC 贯穿了 java 垃圾回收的方方面面的知识,涉及 java 内存结构、内存分配原理、垃圾回收算法、内存调优策略等等,涵盖了 java 语言底层设计的核心内容。Full GC 会造成线程的阻塞,通俗讲就是 “stop the world”,频繁发生会导致系统不稳定,甚至不可用,作为 java 程序员不是降低其发生的频率而是要避免发生 Full GC。
1GC 基本概念1.1年轻代 GC1.2老年代 GC2Full GC2.1Full GC 触发条件2.2Full GC 原因2.3Full GC 排查追踪2.4Full GC 避免及注意事项
GC 基本概念
常用的 HotSpot JVM 把内存划分为 Eden、Survivor 和 Tenured/Old 空间,如下图所示:
其中 Eden + Survivor 称之为年轻代(新生代),Survivor 又被等分为 2个 Survivor 区(分为 from 和 to 角色),Tenured/Old 则称为老年代。
年轻代 GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
一般情况下,新创建的对象都会被分配到 Eden 区(一些大对象特殊处理,直接在老年代分配),这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到 Survivor 区。对象在 Survivor 区中每熬过一次 Minor GC,年龄就会增加 1岁,当它的年龄增加到一定程度(可以通过 -XX:MaxTenuringThreshold 来设置,默认为 15)时,就会被移动到年老代中。
年轻代中的对象基本(80%左右)在几轮 Minor GC 后都会被回收,所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。基于上述的描述,可以深入理解为什么 Eden 区和 2个 Survivor 区的空间默认比例是 8:1:1。
GC 刚开始,对象只会存在于 Eden 区和名为 “From” 的 Survivor 区,另一个 Survivor 区的 “To” 是空的。接着进行 GC,Eden 区中所有存活的对象都会被复制到 “To”,而在 “From” 区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到阈值的对象会被移动到年老代中,没有达到阈值的对象会被复制到 “To” 区域。经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候,“From” 和 “To” 会交换它们的角色,也就是新的 “To” 就是上次 GC 前的 “From”,新的 “From” 就是上次 GC 前的 “To”。无论如何,角色名为 “To” 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到 “To” 区被填满,“To” 区被填满之后,会将所有对象移动到年老代中。
老年代 GC
接着上述的年轻代 GC,对老年代 GC 称为 Major GC。
其实针对 Major GC 没有正式的定义,它有点复杂,一方面,很多 Major GC 都是由 Minor GC 触发的,所以很多情况下将这两个概念分开是不可能的,另一方面,很多现代的垃圾回收会部分的执行老年代(Tenured space)清理。
老年代空间的主要由新生代转入的对象、创建的大对象以及大数组对象。
在老年代发生垃圾回收时,由于老年代中的对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-整理算法(标记-整理算法)进行垃圾回收。
Major GC 的速度一般会比 Minor GC 慢 10倍以上。
Full GC
Full GC 触发条件
老年代中如果没有足够的内存空间去容纳新进的对象时,就会引发一次 Full GC;如果在执行完 Full GC 之后,还是没有办法给这些对象分配内存,那么就凉凉了,会抛出如下 OOM 错误:
java.lang.OutOfMemoryError: Java heap space
Full GC 原因
full gc 的原因其实非常的多,将近 30 多种,具体查看如下代码中 return 返回的字符串描述:
#include "precompiled.hpp"
#include "gc/shared/gcCause.hpp"
const char* GCCause::to_string(GCCause::Cause cause) {
switch (cause) {
case _java_lang_system_gc:
return "System.gc()";
case _full_gc_alot:
return "FullGCAlot";
case _scavenge_alot:
return "ScavengeAlot";
case _allocation_profiler:
return "Allocation Profiler";
case _jvmti_force_gc:
return "JvmtiEnv ForceGarbageCollection";
case _gc_locker:
return "GCLocker Initiated GC";
case _heap_inspection:
return "Heap Inspection Initiated GC";
case _heap_dump:
return "Heap Dump Initiated GC";
case _wb_young_gc:
return "WhiteBox Initiated Young GC";
case _wb_conc_mark:
return "WhiteBox Initiated Concurrent Mark";
case _wb_full_gc:
return "WhiteBox Initiated Full GC";
case _update_allocation_context_stats_inc:
case _update_allocation_context_stats_full:
return "Update Allocation Context Stats";
case _no_gc:
return "No GC";
case _allocation_failure:
return "Allocation Failure";
case _tenured_generation_full:
return "Tenured Generation Full";
case _metadata_GC_threshold:
return "Metadata GC Threshold";
case _metadata_GC_clear_soft_refs:
return "Metadata GC Clear Soft References";
case _cms_generation_full:
return "CMS Generation Full";
case _cms_initial_mark:
return "CMS Initial Mark";
case _cms_final_remark:
return "CMS Final Remark";
case _cms_concurrent_mark:
return "CMS Concurrent Mark";
case _old_generation_expanded_on_last_scavenge:
return "Old Generation Expanded On Last Scavenge";
case _old_generation_too_full_to_scavenge:
return "Old Generation Too Full To Scavenge";
case _adaptive_size_policy:
return "Ergonomics";
case _g1_inc_collection_pause:
return "G1 Evacuation Pause";
case _g1_humongous_allocation:
return "G1 Humongous Allocation";
case _dcmd_gc_run:
return "Diagnostic Command";
case _last_gc_cause:
return "ILLEGAL VALUE - last gc cause - ILLEGAL VALUE";
default:
return "unknown GCCause";
}
ShouldNotReachHere();
}
这里主要介绍常见的原因,如下列表:
Full GC (Ergonomics):使用 Parallel Scavenge 垃圾回收器,若晋升到老年代的平均大小大于老年代剩余的空间大小,则会触发该类 Full GC,如下打印:
2022-08-09T00:00:02.161+0800: 11046.680: [Full GC (Ergonomics) [PSYoungGen: 9518K->0K(1730048K)] [ParOldGen: 3652109K->1779475K(3495424K)] 3661627K->1779475K(5225472K), [Metaspace: 164114K->162637K(1204224K)], 3.1009201 secs] [Times: user=10.15 sys=0.28, real=3.10 secs]
Full GC (Metadata GC Threshold):它是指 Metaspace 扩容触发了 Full GC 的初始化阈值,如果未通过 -XX:MetaspaceSize 设置,则默认大约是 21 M;在 GC 后,Metaspace 会被动态调整,若本次 GC 释放了大量空间,那么就适当降低该值,如果释放的空间较小则适当提高该值,当然它的值不会大于 -XX:MaxMetaspaceSize;若超过最大阈值,则会出现如下 OOM 错误,程序崩溃:
java.lang.OutOfMemoryError: Metaspace
Full GC (System.gc()):System.gc() 方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也增加了间歇性停顿的次数。
强烈建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过 -XX:+DisableExplicitGC 来禁止 RMI 调用 System.gc()。
Full GC 排查追踪
java 程序启动时,最好都配置 gc 日志的监控打印参数 -Xloggc,指定输出 gc 信息日志文件路径,具体示例如下:
-Xloggc:/xxx/xxxx/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
上述示例是生产环境建议的实践配置,它会在指定目录输出类似如下信息:
2022-08-08T23:59:34.443+0800: 11018.963: [GC (Allocation Failure) [PSYoungGen: 1716775K->6161K(1730560K)] 5177705K->3469633K(5225984K), 0.0214261 secs] [Times: user=0.13 sys=0.00, real=0.02 secs]
2022-08-09T00:00:01.990+0800: 11046.510: [GC (Allocation Failure) [PSYoungGen: 1674838K->9518K(1730048K)] 5138310K->3661627K(5411328K), 0.1692477 secs] [Times: user=0.71 sys=0.22, real=0.17 secs]
2022-08-09T00:00:02.161+0800: 11046.680: [Full GC (Ergonomics) [PSYoungGen: 9518K->0K(1730048K)] [ParOldGen: 3652109K->1779475K(3495424K)] 3661627K->1779475K(5225472K), [Metaspace: 164114K->162637K(1204224K)], 3.1009201 secs] [Times: user=10.15 sys=0.28, real=3.10 secs]
2022-08-09T00:00:13.126+0800: 11057.646: [GC (Allocation Failure) [PSYoungGen: 1713152K->16874K(1656320K)] 3492627K->1991145K(5151744K), 0.0492017 secs] [Times: user=0.36 sys=0.00, real=0.05 secs]
2022-08-09T00:00:35.881+0800: 11080.401: [GC (Allocation Failure) [PSYoungGen: 1656298K->17351K(1611264K)] 3630569K->2006252K(5106688K), 0.0195847 secs] [Times: user=0.12 sys=0.00, real=0.02 secs]
排查 GC 的打印,在日常打印基础上添加下列参数:
-XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 -XX:+PrintHeapAtGC -XX:+PrintTLAB -XX:+PrintReferenceGC -XX:+PrintTenuringDistribution
Full GC 避免及注意事项
针对 full gc 的对应策略总结如下:
合理的新生代、老年代的空间比例,默认即可,不要轻易修改,除非对自身程序内存情况有把握;
尽量初始化堆最大值 -Xmx 设置时,同时设置 -Xms 初始化内存值,其值最好与最大值相同,以避免在每次 GC 后调整堆的大小,进而可能的 Full GC 发生。