某天早上,毛老师在群里问「cat 上怎么看 gc」。
![](https://img.haomeiwen.com/i2405011/ae1e29fc56e41640.png)
看到有 GC 的问题,立马做出小鸡搓手状。
之后毛老师发来一张图。
![](https://img.haomeiwen.com/i2405011/da91633fbc103eaa.png)
图片展示了老年代内存占用情况。
第一个大陡坡是应用发布,老年代内存占比下降,很正常。
第二个小陡坡,老年代内存占用突然下降,应该是发生了老年代 GC。
但奇怪的是,此时老年代内存占用并不高,发生 GC 并不是正常现象。
于是,毛老师查看了 GC log。
![](https://img.haomeiwen.com/i2405011/e2a9844e7290c08c.png)
从 GC log 中可以看出,老年代发生了一次 CMS GC。
但此时老年代内存使用占比 = 234011K / 2621440k ≈ 9%。
而 CMS 触发的条件是:
老年代内存使用占比达到 CMSInitiatingOccupancyFraction,默认为 92%,
毛老师设置的是 75%。
-XX:CMSInitiatingOccupancyFraction = 75
于是排除老年代占用过高的可能。
接着分析内存状况。
![](https://img.haomeiwen.com/i2405011/e2a39cad29db2b4d.png)
毛老师发现在老年代发生 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 的问题算是解决了,但同时引发了以下几点思考:
- Metaspace 分配和扩容有什么规律?
- JDK 1.8 中的 Metaspace 和 JDK 1.7 中的 Perm 区有什么区别?
- 老年代回收设置成非 CMS 时,Metaspace 占用到达 -XX:MetaspaceSize 会引发什么 GC?
- 如何制造 Metasapce 内存占用上升?
关于这个问题一和问题二,阿飞Javaer 已经解释的比较清楚。
对于 Metaspce,其初始大小并不等于设置的 -XX:MetaspaceSize 参数。
随着类的加载,Metaspce 会不断进行扩容,直到达到 -XX:MetaspaceSize 触发 GC。
而至于如何设置 Metaspace 的初始大小,目前的确没有办法。
在 openjdk 的 bug 列表中,找到一个 关于 Metaspace 初始大小的 bug,并且尚未解决。
![](https://img.haomeiwen.com/i2405011/2b2c42103c47ed73.png)
对于问题二, 阿飞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 空间占用上升。
![](https://img.haomeiwen.com/i2405011/c8e34065657356b4.png)
从堆 Dump 中也可以发现,有很多被 CGlib 所代理的 SayHello 类对象。
![](https://img.haomeiwen.com/i2405011/6907955d9a017c78.png)
代理类对应的 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。
![](https://img.haomeiwen.com/i2405011/0a31f6987954a381.png)
从 jstat 输出可以看到,在触发一次 Full GC 之后,Metaspace 依旧发生了扩容,但未发生 Full GC。
jstat FGC 次数一直都是 1。
此外,使用 GClib 动态生成类,Metaspace 继续扩容,到一定程度,触发了 Full GC。
但触发 FGC 时,Metaspace 占比并没用明显的规律。
![](https://img.haomeiwen.com/i2405011/f333e96b3f646ebf.png)
尝试了几次,由于 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 扩容或缩容。
参考资料
- JVM参数MetaspaceSize的误解 https://www.jianshu.com/p/b448c21d2e71
- JVM源码分析之垃圾收集的执行过程 https://www.jianshu.com/p/04eff13f3707
- JVM源码分析之Metaspace解密 http://lovestblog.cn/blog/2016/10/29/metaspace/
网友评论