Java教程:9种常见的CMS GC问题详解(六)

开课吧开课吧锤锤2021-03-23 10:33

    Java编程语言是一种简单、面向对象、分布式、解释型、健壮安全、与系统无关、可移植、高性能、多线程和动态的语言。如今Java已经广泛应用于各个领域的编程开发。

Java

    4.2场景二:显式GC的去与留

    4.2.1现象

    除了扩容缩容会触发CMSGC之外,还有Old区达到回收阈值、MetaSpace空间不足、Young区晋升失败、大对象担保失败等几种触发条件,如果这些情况都没有发生却触发了GC?这种情况有可能是代码中手动调用了System.gc方法,此时可以找到GC日志中的GCCause确认下。那么这种GC到底有没有问题,翻看网上的一些资料,有人说可以添加-XX:+DisableExplicitGC参数来避免这种GC,也有人说不能加这个参数,加了就会影响NativeMemory的回收。先说结论,笔者这里建议保留System.gc,那为什么要保留?我们一起来分析下。

    4.2.2原因

    找到System.gc在Hotspot中的源码,可以发现增加-XX:+DisableExplicitGC参数后,这个方法变成了一个空方法,如果没有加的话便会调用Universe::heap()::collect方法,继续跟进到这个方法中,发现System.gc会引发一次STW的FullGC,对整个堆做收集。

JVM_ENTRY_NO_ENV(void, JVM_GC(void))
  JVMWrapper("JVM_GC");
  if (!DisableExplicitGC) {
    Universe::heap()->collect(GCCause::_java_lang_system_gc);
  }
JVM_END
void GenCollectedHeap::collect(GCCause::Cause cause) {
  if (cause == GCCause::_wb_young_gc) {
    // Young collection for the WhiteBox API.
    collect(cause, YoungGen);
  } else {
#ifdef ASSERT
  if (cause == GCCause::_scavenge_alot) {
    // Young collection only.
    collect(cause, YoungGen);
  } else {
    // Stop-the-world full collection.
    collect(cause, OldGen);
  }
#else
    // Stop-the-world full collection.
    collect(cause, OldGen);
#endif
  }
}

    保留System.gc

    此处补充一个知识点,CMSGC共分为Background和Foreground两种模式,前者就是我们常规理解中的并发收集,可以不影响正常的业务线程运行,但ForegroundCollector却有很大的差异,他会进行一次压缩式GC。此压缩式GC使用的是跟SerialOldGC一样的Lisp2算法,其使用Mark-Compact来做FullGC,一般称之为MSC(Mark-Sweep-Compact),它收集的范围是Java堆的Young区和Old区以及MetaSpace。由上面的算法章节中我们知道compact的代价是巨大的,那么使用ForegroundCollector时将会带来非常长的STW。如果在应用程序中System.gc被频繁调用,那就非常危险了。

    去掉System.gc

    如果禁用掉的话就会带来另外一个内存泄漏问题,此时就需要说一下DirectByteBuffer,它有着零拷贝等特点,被Netty等各种NIO框架使用,会使用到堆外内存。堆内存由JVM自己管理,堆外内存必须要手动释放,DirectByteBuffer没有Finalizer,它的NativeMemory的清理工作是通过sun.misc.Cleaner自动完成的,是一种基于PhantomReference的清理工具,比普通的Finalizer轻量些。

    为DirectByteBuffer分配空间过程中会显式调用System.gc,希望通过FullGC来强迫已经无用的DirectByteBuffer对象释放掉它们关联的NativeMemory,下面为代码实现:

// These methods should be called whenever direct memory is allocated or
// freed.  They allow the user to control the amount of direct memory
// which a process may access.  All sizes are specified in bytes.
static void reserveMemory(long size) {

    synchronized (Bits.class) {
        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }
        if (size <= maxMemory - reservedMemory) {
            reservedMemory += size;
            return;
        }
    }

    System.gc();
    try {
        Thread.sleep(100);
    } catch (InterruptedException x) {
        // Restore interrupt status
        Thread.currentThread().interrupt();
    }
    synchronized (Bits.class) {
        if (reservedMemory + size > maxMemory)
            throw new OutOfMemoryError("Direct buffer memory");
        reservedMemory += size;
    }

}

    HotSpotVM只会在OldGC的时候才会对Old中的对象做ReferenceProcessing,而在YoungGC时只会对Young里的对象做ReferenceProcessing。Young中的DirectByteBuffer对象会在YoungGC时被处理,也就是说,做CMSGC的话会对Old做ReferenceProcessing,进而能触发Cleaner对已死的DirectByteBuffer对象做清理工作。但如果很长一段时间里没做过GC或者只做了YoungGC的话则不会在Old触发Cleaner的工作,那么就可能让本来已经死亡,但已经晋升到Old的DirectByteBuffer关联的NativeMemory得不到及时释放。这几个实现特征使得依赖于System.gc触发GC来保证DirectByteMemory的清理工作能及时完成。如果打开了-XX:+DisableExplicitGC,清理工作就可能得不到及时完成,于是就有发生DirectMemory的OOM。

    4.2.3策略

    通过上面的分析看到,无论是保留还是去掉都会有一定的风险点,不过目前互联网中的RPC通信会大量使用NIO,所以笔者在这里建议保留。此外JVM还提供了-XX:+ExplicitGCInvokesConcurrent和-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses参数来将System.gc的触发类型从Foreground改为Background,同时Background也会做ReferenceProcessing,这样的话就能大幅降低了STW开销,同时也不会发生NIODirectMemoryOOM。

    4.2.4小结

    不止CMS,在G1或ZGC中开启ExplicitGCInvokesConcurrent模式,都会采用高性能的并发收集方式进行收集,不过还是建议在代码规范方面也要做好约束,规范好System.gc的使用。

    P.S.HotSpot对System.gc有特别处理,最主要的地方体现在一次System.gc是否与普通GC一样会触发GC的统计/阈值数据的更新,HotSpot里的许多GC算法都带有自适应的功能,会根据先前收集的效率来决定接下来的GC中使用的参数,但System.gc默认不更新这些统计数据,避免用户强行GC对这些自适应功能的干扰(可以参考-XX:+UseAdaptiveSizePolicyWithSystemGC参数,默认是false)。

    以上内容由开课吧老师、新宇、湘铭、祥璞提供,更多Java教程尽在开课吧广场Java教程频道。

有用
分享
全部评论快来秀出你的观点
登录 后可发表观点…
发表
暂无评论,快来抢沙发!
高并发编程训练营