本文基于 Java 17-ea,但是相关设计在 Java 11 之后是大致一样的
我们经常在面试中询问 System.gc()
究竟会不会立刻触发 Full GC,网上也有很多人给出了答案,但是这些答案都有些过时了。本文基于最新的 Java 的下一个即将发布的 LTS 版本 Java 17(ea)的源代码,深入解析 System.gc() 背后的故事。
为什么需要System.gc()
1. 使用并管理堆外内存的框架,需要 Full GC 的机制触发堆外内存回收
JVM 的内存,不止堆内存,还有其他很多块,通过 Native Memory Tracking 可以看到:
1 | ini复制代码Native Memory Tracking: |
- Java Heap: 堆内存,即
-Xmx
限制的最大堆大小的内存。 - Class:加载的类与方法信息,其实就是 metaspace,包含两部分: 一是 metadata,被
-XX:MaxMetaspaceSize
限制最大大小,另外是 class space,被-XX:CompressedClassSpaceSize
限制最大大小 - Thread:线程与线程栈占用内存,每个线程栈占用大小受
-Xss
限制,但是总大小没有限制。 - Code:JIT 即时编译后(C1 C2 编译器优化)的代码占用内存,受
-XX:ReservedCodeCacheSize
限制 - GC:垃圾回收占用内存,例如垃圾回收需要的 CardTable,标记数,区域划分记录,还有标记 GC Root 等等,都需要内存。这个不受限制,一般不会很大的。
- Compiler:C1 C2 编译器本身的代码和标记占用的内存,这个不受限制,一般不会很大的
- Internal:命令行解析,JVMTI 使用的内存,这个不受限制,一般不会很大的
- Symbol: 常量池占用的大小,字符串常量池受
-XX:StringTableSize
个数限制,总内存大小不受限制 - Native Memory Tracking:内存采集本身占用的内存大小,如果没有打开采集(那就看不到这个了,哈哈),就不会占用,这个不受限制,一般不会很大的
- Arena Chunk:所有通过 arena 方式分配的内存,这个不受限制,一般不会很大的
- Tracing:所有采集占用的内存,如果开启了 JFR 则主要是 JFR 占用的内存。这个不受限制,一般不会很大的
- Logging,Arguments,Module,Synchronizer,Safepoint,Other,这些一般我们不会关心。
除了 Native Memory Tracking 记录的内存使用,还有两种内存 Native Memory Tracking 没有记录,那就是:
- Direct Buffer:直接内存
- MMap Buffer:文件映射内存
针对除了堆内存以外,其他的内存,有些也是需要 GC 的。例如:MetaSpace,CodeCache,Direct Buffer,MMap Buffer 等等。早期在 Java 8 之前的 JVM,对于这些内存回收的机制并不完善,很多情况下都需要 FullGC 扫描整个堆才能确定这些区域中哪些内存可以回收。
有一些框架,大量使用并管理了这些堆外空间。例如 netty 使用了 Direct Buffer,Kafka 和 RocketMQ 使用了 Direct Buffer 和 MMap Buffer。他们都是提前从系统申请好一块内存,之后管理起来并使用。在空间不足时,继续向系统申请,并且也会有缩容。例如 netty,在使用的 Direct Buffer 达到-XX:MaxDirectMemorySize
的限制之后,则会先尝试将不可达的Reference对象加入Reference链表中,依赖Reference的内部守护线程触发可以被回收DirectByteBuffer关联的Cleaner的run()方法。如果内存还是不足, 则执行System.gc()
,期望触发full gc
,来回收堆内存中的DirectByteBuffer
对象来触发堆外内存回收,如果还是超过限制,则抛出java.lang.OutOfMemoryError
.
2. 使用了 WeakReference, SoftReference 的程序,需要相应的 GC 回收。
对于 WeakReference,只要发生 GC,无论是 Young GC 还是 FullGC 就会被回收。SoftReference 只有在 FullGC 的时候才会被回收。当我们程序想主动对于这些引用进行回收的时候,需要能触发 GC 的方法,这就用到了System.gc()
。
3. 测试,学习 JVM 机制的时候
有些时候,我们为了测试,学习 JVM 的某些机制,需要让 JVM 做一次 GC 之后开始,这也会用到System.gc()
。但是其实有更好的方法,后面你会看到。
System.gc() 背后的原理
System.gc()
实际上调用的是RunTime.getRunTime().gc()
:
1 | csharp复制代码public static void gc() { |
这个方法是一个 native 方法:
1 | csharp复制代码public native void gc(); |
对应 JVM 源码:
1 | scss复制代码JVM_ENTRY_NO_ENV(void, JVM_GC(void)) |
首先,根据 DisableExplicitGC 这个 JVM 启动参数的状态,确定是否会 GC,如果需要 GC,不同 GC 会有不同的处理。
1. G1 GC 的处理
如果是 System.gc()
触发的 GC,G1 GC 会根据 ExplicitGCInvokesConcurrent 这个 JVM 参数决定是默认 GC (轻量 GC,YoungGC)还是 FullGC。
参考代码g1CollectedHeap.cpp
:
1 | scss复制代码//是否应该并行 GC,也就是较为轻量的 GC,对于 GCCause::_java_lang_system_gc,这里就是判断 ExplicitGCInvokesConcurrent 这个 JVM 是否为 true |
2. ZGC 的处理
直接不处理,不支持通过 System.gc()
触发 GC。
参考源码:zDriver.cpp
1 | php复制代码void ZDriver::collect(GCCause::Cause cause) { |
3. Shenandoah GC 的处理
Shenandoah 的处理和 G1 GC 的类似,先判断是不是用户明确触发的 GC,然后通过 DisableExplicitGC 这个 JVM 参数判断是否可以 GC(其实这个是多余的,可以去掉,因为外层JVM_ENTRY_NO_ENV(void, JVM_GC(void))
已经处理这个状态位了)。如果可以,则请求 GC,阻塞等待 GC 请求被处理。然后根据 ExplicitGCInvokesConcurrent 这个 JVM 参数决定是默认 GC (轻量并行 GC,YoungGC)还是 FullGC。
参考源码shenandoahControlThread.cpp
1 | ruby复制代码void ShenandoahControlThread::request_gc(GCCause::Cause cause) { |
请求 GC 的代码流程是:
1 | scss复制代码void ShenandoahControlThread::handle_requested_gc(GCCause::Cause cause) { |
对于GCCause::_java_lang_system_gc
,GC 的执行流程大概是:
1 | scss复制代码bool explicit_gc_requested = _gc_requested.is_set() && is_explicit_gc(_requested_gc_cause); |
System.gc() 相关的 JVM 参数
1. DisableExplicitGC
说明:是否禁用显式 GC,默认是不禁用的。对于 Shenandoah GC,显式 GC 包括:GCCause::_java_lang_system_gc
,GCCause::_dcmd_gc_run
,GCCause::_jvmti_force_gc
,GCCause::_heap_inspection
,GCCause::_heap_dump
,对于其他 GC,仅仅限制GCCause::_java_lang_system_gc
默认:false
举例:如果想禁用显式 GC:-XX:+DisableExplicitGC
2. ExplicitGCInvokesConcurrent
说明:对于显式 GC,是执行轻量并行 GC (YoungGC)还是 FullGC,如果为 true 则是执行轻量并行 GC (YoungGC),false 则是执行 FullGC
默认:false
举例:启用的话指定:-XX:+ExplicitGCInvokesConcurrent
其实,在设计上有人提出(参考链接)想将 ExplicitGCInvokesConcurrent 改为 true。但是目前并不是所有的 GC 都可以在轻量并行 GC 对 Java 所有内存区域进行回收,有些时候必须通过 FullGC。所以,目前这个参数还是默认为 false
3. 已过期的 ExplicitGCInvokesConcurrentAndUnloads 和使用 ClassUnloadingWithConcurrentMark 替代
如果显式 GC采用轻量并行 GC,那么无法执行 Class Unloading(类卸载),如果启用了类卸载功能,可能会有异常。所以通过这个状态位来标记在显式 GC时,即使采用轻量并行 GC,也要扫描进行类卸载。 ExplicitGCInvokesConcurrentAndUnloads
目前已经过期了,用ClassUnloadingWithConcurrentMark
替代
如何灵活可控的主动触发各种 GC?
答案是通过 WhiteBox API。但是这个不要在生产上面执行,仅仅用来测试 JVM 还有学习 JVM 使用。WhiteBox API 是 HotSpot VM 自带的白盒测试工具,将内部的很多核心机制的 API 暴露出来,用于白盒测试 JVM,压测 JVM 特性,以及辅助学习理解 JVM 并调优参数。WhiteBox API 是 Java 7 引入的,目前 Java 8 LTS 以及 Java 11 LTS(其实是 Java 9+ 以后的所有版本,这里只关心 LTS 版本,Java 9 引入了模块化所以 WhiteBox API 有所变化)都是有的。但是默认这个 API 并没有编译在 JDK 之中,但是他的实现是编译在了 JDK 里面了。所以如果想用这个 API,需要用户自己编译需要的 API,并加入 Java 的 BootClassPath 并启用 WhiteBox API。下面我们来用 WhiteBox API 来主动触发各种 GC。
1. 编译 WhiteBox API
将https://github.com/openjdk/jdk/tree/master/test/lib
路径下的sun
目录取出,编译成一个 jar 包,名字假设是 whitebox.jar
2. 编写测试程序
将 whitebox.jar
添加到你的项目依赖,之后写代码
1 | scss复制代码public static void main(String[] args) throws Exception { |
3. 启动程序查看效果
使用启动参数 -Xbootclasspath/a:/home/project/whitebox.jar -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xlog:gc
启动程序。其中前三个 Flag 表示启用 WhiteBox API,最后一个表示打印 GC info 级别的日志到控制台。
我的输出:
1 | ini复制代码[0.036s][info][gc] Using G1 |
微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer
本文转载自: 掘金