CMS可能发生的问题(一)

情景一 空间震荡

现象

服务刚启动的时候GC次数比较多,由于只设置了-Xms,JVM自动分配一定大小的heap或者metaSpace大小,一旦空间不够用,就进行GC并且向系统申请新的内存,并且计算新的内存大小

如下图所示,JVM会检测commited size是否低于或者高于低水位和高水位,一旦低于或者超过,就会动态调整堆的大小,JVM 通过 -XX:MinHeapFreeRatio 和 -XX:MaxHeapFreeRatio 来控制扩容和缩容的比例。
IMAGE

分析

image.png

定位问题

观察CMS GC触发的时间点Old/MetaSpace的commited是否是一个固定的值,如果不是固定的值,说明发生了heap空间调整

策略

将成对出现的JVM参数设置成固定的数值,防止动态扩容缩容造成的java虚拟机堆不稳定(前提是在系统内存不太紧缺的情况下)

-Xms:
-Xmx:
-XX:MinNewSize=
-XX:MaxNewSize=
-XX:MetaSpaceSize=
-XX:MaxMetaSpaceSize=
复制代码

情景二 System.gc()

现象

除了扩容缩容会导致CMS GC之外,Old区域达到阈值、MetaSpace空间不足、Young区晋升失败、大对象担保失败也会触发GC,这种很有可能是代码中调用了System.GC(),此时可以在GC Cause中找到System.GC()的情况

分析

System.GC()参与了DirectByteBuffer的内存分配过程,DirectByteBuffer有着零拷贝等特点,被各种NIO框架广泛使用。此区域不归JVM管理,需要借助sun.misc.Cleaner来完成。其在为DirectByteBuffer分配内存的过程中需要显式调用java.nio.Bits中的System.gc()

这个System.gc()的作用在于将堆中对于DirectByteBuffer中的对象引用消除掉,如果没有System.GC()只能等待young区域和Old区域自行垃圾回收对失去引用的DirectByteBuffer对象进行清除,一旦长时间不发生Old GC就会导致DirectByteBuffer区域中的对象无法消除,最终引发DirectByteBuffer溢出

// 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;
    }
}
复制代码

System.GC()会触发一次foregroundGC,CMS分为Background和Foreground两种模式,前置就是常规理论中的并发收集,而foreground会进行一次压缩式的GC,此GC使用的是和Serial Old一样的Lisp2算法,其使用Full Compact来做Full GC,一般称之为MSC,其收集的范围是Young区和Old以及MetaSpace,System.GC()会触发foreground 模式的CMS GC

策略

折中Full GC和DirectByteBuffer,开启-XX:+ExplicitGCInvokesConcurrent和-XX:+ExplicitInvokesConcurrentAndUnloadsClasses,使得System.GC()触发的GC从foreground变为background,这样既不影响堆中引用对DirectByteBuffer区域的对象回收问题,也不会每次DirectByteBuffer清理空间都要触发一次Full GC

-javaagent:"C:\Program Files\Java\xrebel.jar"
-XX:+UseConcMarkSweepGC
-XX:+PrintGCDetails
-XX:+PrintHeapAtGC
-Xloggc:D://gceasy//waterGC.log
-XX:+ExplicitGCInvokesConcurrent
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
复制代码

场景三 MetaSpace区域OOM

现象

JVM在启动后或者某个时间点开始,MetaSpace的使用大小在持续增长,同时每次GC也无法释放,调大MetaSpace空间也无法彻底解决问题

原因

MetaSpace主要由Klass MetaSpace和NoKlass MetaSpace两大部分组成

  • Klass MetaSpace:

用来存储Klass,Class文件在JVM中的运行时数据结构,这部分默认放在Cpmpressed Class Poniter Space,是一块连续的内存区域,紧接着Heap,这个区域不是必须有的

  • NoKlass MetaSpace:

专门用来存储Klass之外的其他内容,比如Method,ConstantPool等,可以由多块不连续的内存组成,虽然叫做NoKlass MetaSpace,但是其实可以存储Klass的内容,因此Klass MetaSpace不是必须的

Meta对象无法被释放

  • MetaSpace内存管理:

类和其元数据的生命周期与其对应的类加载器相同,只要类的类加载器没有被卸载。MetaSpace中的类元数据就也是存活的,不能被回收。每个加载器有单独的存储空间。

  • MetaSpace弹性伸缩:

我们会将 -XX:MetaSpaceSize 和 -XX:MaxMetaSpaceSize 两个值设置为固定的,但是这样也会导致在空间不够的时候无法扩容,然后频繁地触发 GC,最终 OOM。所以关键原因就是 ClassLoader 不停地在内存中 load 了新的 Class ,一般这种问题都发生在动态类加载等情况上。

策略

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/jvmlogs
复制代码

使用jprofiler分析dump文件,观察classes的直方图,查看是哪个包下的Class数量异常

//java自带工具命令
jcmd <PID> GC.class_stats|awk '{print$13}'|sed  's/\(.*\)\.\(.*\)/\1/g'|sort |uniq -c|sort -nrk1
复制代码

定位和解决问题是比较简单了,经常会出现问题的几个点是Orika的classMap、JSON的ASMSerializer、Groovy动态加载类等,问题基本集中在反射或者字节码加强、CGLIB动态代理、OSGI自定义类加载器等技术点上。

场景四:过早晋升*

现象

image
分配速率接近晋升速率,YoungGC频繁并且每次YoungGC都会有大量的对象被放到Old区,并且Old区每经过一次GC就会有大量的对象被回收

这种场景主要发生在分代的收集器上面,专业的术语称为“Premature Promotion”。90% 的对象朝生夕死,只有在 Young 区经历过几次 GC 的洗礼后才会晋升到 Old 区,每经历一次 GC 对象的 GC Age 就会增长 1,最大通过 -XX:MaxTenuringThreshold 来控制。

原因

  • Young区域过小:

由于young区域过小,young区域的GC次数就会明显增多,造成对象的GC分代年龄不正常的增大,使得本该回收的对象参与了晋升,这部分对象又会造成Old区域填充的速度变快,使得Old GC区域需要进行频繁的GC

  • MaxTenuringThrshold 动态变化造成对象无法回收:

Hotspot 会使用动态计算的方式来调整晋升的阈值,如同前面所叙述的,JVM hotspot会对TernuringThreshold的动态年龄进行调整。-XX:MaxTenuringThreshold=6 CMS的默认最大TenuringThreshold为6,如果hotspot累加0到n的对象比例超过了(TargetSurvivorRatio /100)就要调整n的值,此时young区域有大量GC分代为n的对象,TernuringTreshold就会被设置成n,造成更多的对象晋升到old区域

策略

确定Old区域的最大占用空间后,分配给Old区域3倍左右的空间,使用-Xmn将堆中剩余的空间全部分配给Young区域,一旦发生这种GC问题,这种方式的调整会有十分明显的效果,如果还是不行就需要更换GC器或者增大系统内存

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享