情景一 空间震荡
现象
服务刚启动的时候GC次数比较多,由于只设置了-Xms,JVM自动分配一定大小的heap或者metaSpace大小,一旦空间不够用,就进行GC并且向系统申请新的内存,并且计算新的内存大小
如下图所示,JVM会检测commited size是否低于或者高于低水位和高水位,一旦低于或者超过,就会动态调整堆的大小,JVM 通过 -XX:MinHeapFreeRatio 和 -XX:MaxHeapFreeRatio 来控制扩容和缩容的比例。
分析
定位问题
观察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自定义类加载器等技术点上。
场景四:过早晋升*
现象
分配速率接近晋升速率,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器或者增大系统内存