有不少观点认为调用 System.gc()
是一个不好的习惯,当我看到 JDK 8 中 FileChannelImpl
的 map
方法实现时,其中对 System.gc()
的调用让我感到诧异。在 FileChannelImpl
中存在如下代码 FileChannelImpl.java at jdk8-b120:
1 | try { |
可以看到在调用 map0
方法抛出 OutOfMemoryError
时会调用 System.gc()
方法尝试回收内存,而 JDK 源码的质量一向是比较高的,为何作者在此处调用了 System.gc()
呢?会对异常恢复有帮助吗?于是询问了该问题,主要与堆外内存有关,参考回答如下:
The
FileChannelImpl
’s case is different.map0
may fail due to insufficient native memory or, in case of 32 bit systems, when running out of address space. In these cases, the heap memory manager did not produce theOutOfMemoryError
and it is possible that the garbage collector didn’t run. But to reclaim native memory or address space, the associatedByteBuffer
instances must get garbage collected, so their cleaner can run. This is a rare corner case where callingSystem.gc();
makes sense.It’s still fragile, as
System.gc();
is not guaranteed to collect all objects or to run the garbage collector at all. JEP 383 is supposed to solve this, by providing better control over the lifetime of native allocations.
即尝试回收未使用的 ByteBuffer
实例所占用的物理内存,这是一个需要调用 System.gc()
的极端罕见的场景。类似地,Bits
类也调用了 System.gc()
,该次调用也与直接内存有关,源码位于 Bits.java at jdk8-b120:
1 | // These methods should be called whenever direct memory is allocated or |
即当可供申请的内存空间不足以满足本次申请时,会调用 System.gc()
尝试回收内存后再次进行申请。JDK 8 中还有一处调用了 System.gc()
,源码位于 GC.java at jdk8-b120:
1 | public void run() { |
该次调用的原因可以参考注释,此处不再赘述。
Spark
类似地,在 Spark 的源码中,可以发现,其中 ContextCleaner
类将使用 periodicGCService
定期的调用 System.gc()
触发 Full GC 进行垃圾回收,其默认的间隔时间为半小时,源码位于 ContextCleaner.scala at v3.2.1:
1 | private val periodicGCService: ScheduledExecutorService = |
Reference
Is the System.gc() call in sun.nio.ch.FileChannelImpl a bad case? - Stack Overflow
Impact of setting -XX:+DisableExplicitGC when NIO direct buffers are used - Stack Overflow
When does System.gc() do something? - Stack Overflow
[SPARK-8414] Ensure context cleaner periodic cleanups · apache/spark@1ce4adf