转载请注明原文连接: https://www.jianshu.com/p/468...
某天早上,毛老师在群里问「cat 上怎么看 gc」。java
看到有 GC 的问题,立马作出小鸡搓手状。算法
以后毛老师发来一张图。app
图片展现了老年代内存占用状况。工具
第一个大陡坡是应用发布,老年代内存占比降低,很正常。源码分析
第二个小陡坡,老年代内存占用忽然降低,应该是发生了老年代 GC。测试
但奇怪的是,此时老年代内存占用并不高,发生 GC 并非正常现象。this
因而,毛老师查看了 GC log。spa
从 GC log 中能够看出,老年代发生了一次 CMS GC。.net
但此时老年代内存使用占比 = 234011K / 2621440k ≈ 9%。代理
而 CMS 触发的条件是:
老年代内存使用占比达到 CMSInitiatingOccupancyFraction,默认为 92%,
毛老师设置的是 75%。
-XX:CMSInitiatingOccupancyFraction = 75
因而排除老年代占用太高的可能。
接着分析内存情况。
毛老师发如今老年代发生 GC 时,Metaspace 的内存占用也一块儿降低。
因而怀疑是 Metaspace 占用达到了设置的参数 MetaspaceSize,发生了 GC。
查看 JVM 参数设置,MetaspaceSize 参数被设置为128m。
-XX:MetaspaceSize = 128m -XX:MaxMetaspaceSize = 256m
问题的缘由被集中在 Metaspace 上。
毛老师查看另一个监控工具,发生小陡坡的纵坐标的确接近 128m。
此时,引起出另外一个问题:
Metaspace 发生 GC,为什么会引发老年代 GC。
因而,想到以前看过 阿飞Javaer 的文章 《JVM参数MetaspaceSize的误解》。
其中有几个关键点:
Metaspace 在空间不足时,会进行扩容,并逐渐达到设置的 MetaspaceSize。Metaspace 扩容到 -XX:MetaspaceSize 参数指定的量,就会发生 FGC。
若是配置了 -XX:MetaspaceSize,那么触发 FGC 的阈值就是配置的值。
若是 Old 区配置 CMS 垃圾回收,那么扩容引发的 FGC 也会使用 CMS 算法进行回收。
其中的关键点是:
若是老年代设置了 CMS,则 Metasapce 扩容引发的 FGC 会转变成一次 CMS。
查看毛老师配置的 JVM 参数,果真设置了 CMS GC。
-XX:+UseConcMarkSweepGC
因而,解决问题的方法是调整 -XX:MetaspaceSize = 256m。
从监控来看,设置 -XX:MaxMetaspaceSize = 256m 已经足够。
由于后期并不会引起 CMS GC。
GC 的问题算是解决了,但同时引起了如下几点思考:
关于这个问题一和问题二,阿飞Javaer 已经解释的比较清楚。
对于 Metaspce,其初始大小并不等于设置的 -XX:MetaspaceSize 参数。
随着类的加载,Metaspce 会不断进行扩容,直到达到 -XX:MetaspaceSize 触发 GC。
而至于如何设置 Metaspace 的初始大小,目前的确没有办法。
在 openjdk 的 bug 列表中,找到一个 关于 Metaspace 初始大小的 bug,而且还没有解决。
对于问题二, 阿飞Javaer 在文章中也进行了说明。
Perm 的话,咱们经过配置 -XX:PermSize 以及 -XX:MaxPermSize 来控制这块内存的大小。JVM 在启动的时候会根据 -XX:PermSize 初始化分配一块连续的内存块。
这样的话,若是 -XX:PermSize 设置过大,就是一种赤果果的浪费。
关于 Metaspace,JVM 还提供了其他一些设置参数。
能够经过如下命令查看。
java -XX:+PrintFlagsFinal -version | grep Metaspace
关于 Metaspace 更多的内容,能够参考笨神的文章:《JVM源码分析之Metaspace解密》。
问题三
Metaspace 占用到达 -XX:MetaspaceSize 会引起什么?
已经知道,当老年代回收设置成 CMS GC 时,会触发一次 CMS GC。
那么若是不设置为 CMS GC,又会发生什么呢?
使用如下配置进行一个小尝试,而后查看 GC log。
-Xmx2048m -Xms2048m -Xmn1024m -XX:MetaspaceSize=40m -XX:MaxMetaspaceSize=128m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:d:/heap_trace.txt
该配置并未设置 CMS GC,JDK 1.8 默认的老年代回收算法为 ParOldGen。
本文测试的应用在启动完成后,占用 Metaspace 空间约为 63m,可经过 jstat 命令查看。
因而,设置 -XX:MetaspaceSize = 40m,指望发生一次 GC。
从 GC log 中,能够找到如下关键日志。
[GC (Metadata GC Threshold) [PSYoungGen: 360403K->47455K(917504K)] 360531K->47591K(1966080K), 0.0343563 secs] [Times: user=0.08 sys=0.00, real=0.04 secs] [Full GC (Metadata GC Threshold) [PSYoungGen: 47455K->0K(917504K)] [ParOldGen: 136K->46676K(1048576K)] 47591K->46676K(1966080K), [Metaspace: 40381K->40381K(1085440K)], 0.1712704 secs] [Times: user=0.42 sys=0.02, real=0.17 secs]
能够看出,因为 Metasapce 到达 -XX:MetaspaceSize = 40m 时候,触发了一次 YGC 和一次 Full GC。
通常而言,咱们对 Full GC 的重视度比对 YGC 高不少。
因此通常都会直描述,当 Metasapce 到达 -XX:MetaspaceSize 时会触发一次 Full GC。
问题四
如何人工模拟 Metaspace 内存占用上升?
Metaspace 是 JDK 1.8 以后引入的一个区域。
有一点能够确定的,Metaspace 会保存类的描述信息。
JVM 须要根据 Metaspace 中的信息,才能找到堆中类 java.lang.Class 所对应的对象。(有点绕)
既然 Metaspace 中会保存类描述信息,能够经过新建类来增长 Metaspace 的占用。
因而想到,使用 CGlib 动态代理,生成被代理类的子类。
简单的 SayHello 类。
public class SayHello { public void say() { System.out.println("hello everyone"); } }
简单的代理类,使用 CGlib 生成子类。
public class CglibProxy implements MethodInterceptor { public Object getProxy(Class clazz) { Enhancer enhancer = new Enhancer(); // 设置须要建立子类的类 enhancer.setSuperclass(clazz); enhancer.setCallback(this); enhancer.setUseCache(false); // 经过字节码技术动态建立子类实例 return enhancer.create(); } // 实现MethodInterceptor接口方法 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("前置代理"); // 经过代理类调用父类中的方法 Object result = proxy.invokeSuper(obj, args); System.out.println("后置代理"); return result; } }
简单新建一个 Controller 用于测试生成 10000 个 SayHello 子类。
@RequestMapping(value = "/getProxy", method = RequestMethod.GET) @ResponseBody public void getProxy() { CglibProxy proxy = new CglibProxy(); for (int i = 0; i < 10000; i++) { //经过生成子类的方式建立代理类 SayHello proxyTmp = (SayHello) proxy.getProxy(SayHello.class); proxyTmp.say(); } }
应用启动完毕后,请求 /getProxy 接口,发现 Metaspace 空间占用上升。
从堆 Dump 中也能够发现,有不少被 CGlib 所代理的 SayHello 类对象。
代理类对应的 java.lang.Class 对象分配在堆内,类的描述信息在 Metaspace 中。
堆中有多个 Class 对象,能够推断出 Metasapce 须要装下不少类描述信息。
最后,当 Metaspace 使用空间超过设置的 -XX:MaxMetaspaceSize=128m 时,就会发生 OOM。
Exception in thread "http-nio-8080-exec-6" java.lang.OutOfMemoryError: Metaspace
从 GC log 中能够看到,JVM 会在 Metaspace 占用满以后,尝试 Full GC。
但会出现如下字样。
Full GC (Last ditch collection)
此外,还有一个问题。
当 Metaspace 内存占用未达到 -XX:MetaspaceSize 时,Metaspace 只扩容,不会引发 Full GC。
当 Metaspace 内存占用达到 -XX:MetaspaceSize 时,会发生 Full GC。
在发生第一次 Full GC 以后,Metaspace 依然会扩容。
那么,第二次触发 Full GC 的条件是?
有文章说,在触发第一次F Full GC 后,以后 Metaspace 的每次扩容,都会引发 Full GC。
但观察本文测试的 GC log 和 jstat 命令查看 Metasapce 扩容情况,能够看出:
在第一次 Full GC 以后,以后 Metaspace 的扩容,并不必定会引发 Full GC。
从 jstat 输出能够看到,在触发一次 Full GC 以后,Metaspace 依旧发生了扩容,但未发生 Full GC。
jstat FGC 次数一直都是 1。
此外,使用 GClib 动态生成类,Metaspace 继续扩容,到必定程度,触发了 Full GC。
但触发 FGC 时,Metaspace 占比并没用明显的规律。
尝试了几回,因为 jstat 设置了 1s 钟输出一次,因此每次触发 Full GC 时候,MC 的数据都不同,但基本是相同。
猜想在第一次 Full GC 以后,以后再次触发 Full GC 的阈值是有必定的计算公式的。
但具体如何计算,估计是须要深刻源码了。
此外能够看到,每次 Metaspace 扩容时,都伴随着一次 YGC 或者 Full GC,不知道是不是巧合。
接着看到 占小狼 的文章 《JVM源码分析之垃圾收集的执行过程》。
文章有一句话:
从上述分析中能够发现,gc操做的入口都位于GenCollectedHeap::do_collection方法中。
不一样的参数执行不一样类型的gc。
打开 openjdk 8 中的 GenCollectedHeap 类,查看 do_collection 方法。
能够看到,在 do_collection 方法中,有这个一段代码。
if (complete) { // Delete metaspaces for unloaded class loaders and clean up loader_data graph ClassLoaderDataGraph::purge(); MetaspaceAux::verify_metrics(); // Resize the metaspace capacity after full collections MetaspaceGC::compute_new_size(); update_full_collections_completed(); }
其中最主要的是 MetaspaceGC::compute_new_size();
。
得出,YGC 和 Full GC 的确会从新计算 Metaspace 的大小。
至因而否进行扩容和缩容,则须要根据 compute_new_size()
方法的计算结果而定。
得出,Metasapce 扩容致使 GC 这个说法,实际上是不许确的。
正确的过程是:新建类致使 Metaspace 容量不够,触发 GC,GC 完成后从新计算 Metaspace 新容量,决定是否对 Metaspace 扩容或缩容。