JAVA RMI GDC每小时触发Full GC

JAVA RMI GDC每小时触发Full GC

orzlee
2024-05-06 / 0 评论 / 25 阅读 / 正在检测是否收录...

java-rmi.jpg

前言

最近倒腾Java,发现JVM并行收集器在还有大量堆内存的时候就开始触发老年代的回收,通过GC日志发现每小时都会执行GC和Full GC回收,回收是通过System.gc()调用。

那每小时都调用一次Full GC,我添加启动参数-Xms11g -Xmx11g分配11G堆内存只是为了好看吗?每次Full GC耗时在600ms,并行收集器老年代内存使用还不足5%,真是发神经了!

过程就不做记录了,无聊又蛋疼。以下是在使用了RMI或者其他NIO对象的情况。

RMI DGC每小时触发一次Full GC

原因:DGC代码中调用System.gc()导致并行收集器触发Full GC。

解决办法:

  1. 修改DGC调用System.gc()执行时间,默认一小时,单位ms。启动参数添加:
     -Dsun.rmi.dgc.client.gcInterval=3600000
     -Dsun.rmi.dgc.server.gcInterval=3600000
  2. 禁止程序中的System.gc(),添加启动参数:
     -XX:+DisableExplicitGC
  3. 使用支持并发的收集器(CMS、G1、ZGC或Shenandoah),并且添加启动参数:
     -XX:+ExplicitGCInvokesConcurrent

    方法1,治标不治本,延长时间触发Full GC会导致STW时间更长,可以配合并行收集器使用-XX:+ExplicitGCInvokesConcurrent来控制频率。

方法3,启用-XX:+ExplicitGCInvokesConcurrent参数可以尝试在垃圾收集时使用并发收集来减少STW时间,从而提高应用程序的响应性。

不建议使用-XX:+DisableExplicitGC来限制System.gc()

因为从Java 7开始,Java提供了基于NIO的RMI实现,称为NIO-based RMI。这种实现利用了NIO的非阻塞I/O模型,能够更高效地处理大量的并发连接,减少了线程资源的消耗,并提高了性能和可伸缩性。通过使用NIO,NIO-based RMI能够更好地适应高并发和大规模的网络通信场景。

DirectByteBuffer是一种在堆外内存中分配的ByteBuffer,它与NIO密切相关,因为它可以直接映射到操作系统的内存中,从而避免了在Java堆和本地内存之间进行复制。在NIO编程中,使用DirectByteBuffer可以提高I/O操作的性能和效率,特别是在处理大量数据时。在基于NIO的RMI实现中,使用NioServerSocketChannel和NioEventLoopGroup通常涉及到DirectByteBuffer的使用。

虽然 DirectByteBuffer 对象本身是由 JVM 管理的,它们存储在 Java 堆内存中,但是 DirectByteBuffer 对象所持有的实际数据存储在堆外内存中。 JVM 通过 DirectByteBuffer 对象来管理对堆外内存的访问和操作。当 DirectByteBuffer 对象被垃圾回收时,它所持有的堆外内存也会随之被释放。

因此,触发 Full GC 通常是为了回收已经失去引用的 DirectByteBuffer 对象,进而释放掉它们所占用的堆外内存。 Full GC 在回收过程中会扫描整个堆内存,包括其中的对象和引用。当发现 DirectByteBuffer 对象已经没有引用指向时,JVM 就会将其标记为可回收, 待下次 Full GC 执行时进行回收。这样,间接地通过 Full GC 回收了 DirectByteBuffer 对象,也就释放了相应的堆外内存。

如果禁用了System.gc(),那么不会及时的清理 DirectByteBuffer 或者其他 NIO 对象导致堆外内存也不会释放。虽然最后也可能因为JVM堆内内存不足触发Full GC 来释放,但没必要冒险。参阅

监控程序调用System.gc()堆栈

可以使用 async-profiler 跟踪System.gc调用者:

  1. 预先开始分析:

     profiler.sh start -e java.lang.System.gc <pid>
  2. 发生一种或多种System.gc情况后,停止分析并打印堆栈跟踪:

     --- Execution profile ---
     Total samples       : 6
    
     Frame buffer usage  : 0.0007%
    
     --- 4 calls (66.67%), 4 samples
     [ 0] java.lang.System.gc
     [ 1] java.nio.Bits.reserveMemory
     [ 2] java.nio.DirectByteBuffer.<init>
     [ 3] java.nio.ByteBuffer.allocateDirect
     [ 4] Allocate.main
    
     --- 2 calls (33.33%), 2 samples
     [ 0] java.lang.System.gc
     [ 1] sun.misc.GC$Daemon.run
  3. 在上面的示例中,System.gc从两个地方调用了 6 次。这两种情况都是 JDK 内部强制进行垃圾回收的典型情况。第一个来自java.nio.Bits.reserveMemory.当没有足够的可用内存来分配新的直接 ByteBuffer 时(由于-XX:MaxDirectMemorySize限制), JDK 会强制进行 Full GC 回收无法访问的直接 ByteBuffer。 第二个来自 GC Daemon 线程。这由 Java RMI 运行时定期调用。例如,如果您使用 JMX 远程,则每小时自动启用一次定期 GC。 这可以 通过-Dsun.rmi.dgc.client.gcInterval系统属性进行调整。

结语

起初我很不明白为什么DGC代码里面会有System.gc(),我一直以为堆外内存不归JVM管理,那调用System.gc()又有什么用呢?又是问ChatGPT又是各种搜索,才发现虽然JVM不管理堆外内存,但是管理使用堆外内存的对象,System.gc()是为了及时释放掉不再使用的堆外内存持有对象,堆内释放了,操作系统就会释放堆外内存了!

0
取消
扫码打赏
支付金额随意哦!

评论 (0)

取消