java 的 Full GC 贯穿了 java 垃圾回收的方方面面的知识,涉及 java 内存结构、内存分配原理、垃圾回收算法、内存调优策略等等,涵盖了 java 语言底层设计的核心内容。Full GC 会造成线程的阻塞,通俗讲就是 “stop the world”,频繁发生会导致系统不稳定,甚至不可用,作为 java 程序员不是降低其发生的频率而是要避免发生 Full 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 发生。