前言
本文记录了排查java.lang.OutOfMemoryError: Metaspace
问题的处理过程,解决方案并不是通过调整-XX:MaxMetaspaceSize
来解决问题,经过排查最终定位到的问题是JVM参数中配置-XX:SoftRefLRUPolicyMSPerMB=0
引起的。@空歌白石
问题
近期在生产上有个应用频繁发生java.lang.OutOfMemoryError: Metaspace
的报错,每个一到两天就会有机器触发OOM的告警,详细的堆栈如下:
java.util.concurrent.CompletionException: java.lang.OutOfMemoryError: Metaspace
at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:331)
at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:346)
at java.base/java.util.concurrent.CompletableFuture$UniApply.tryFire(CompletableFuture.java:632)
at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
at java.base/java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:2088)
// 空歌白石:省略部分业务堆栈
at com.google.common.util.concurrent.Futures$6.run(Futures.java:1764)
at com.google.common.util.concurrent.MoreExecutors$DirectExecutor.execute(MoreExecutors.java:456)
at com.google.common.util.concurrent.AbstractFuture.executeListener(AbstractFuture.java:817)
at com.google.common.util.concurrent.AbstractFuture.complete(AbstractFuture.java:753)
at com.google.common.util.concurrent.AbstractFuture.setException(AbstractFuture.java:634)
at com.google.common.util.concurrent.SettableFuture.setException(SettableFuture.java:53)
// 空歌白石:省略部分业务堆栈
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
Caused by: java.lang.OutOfMemoryError: Metaspace
复制代码
JMX的MetaSpace监控如下图:
分析
首先大家应该都知道,JVM的MetaSpace存储JVM的元信息,而且是在堆外存储。主要涉及以下内容:
- JVM中类的元数据在Java堆中的存储区域
- Java类对应的HotSpot虚拟机中的内部表示也存储在这里
- 类的层级信息,字段,名字
- 方法的编译信息及字节码
- 变量
- 常量池和符号解析
JAVA永久代的演化
- JDK7开始,字符串常量和符号引用等就被移出永久代,字符串字面量迁移至Java堆 /符号引用转移到了native heap。
- JDK8,永久代被彻底地移出了JVM,取而代之的是元空间MetaSpace,把类的元数据放到本地化的堆内存native heap中,这块区域就叫Metaspace。
理论上以上信息占用的内存空间在服务启动后就会比较稳定,并不会出现上文中提到的一直增长的,甚至导致OOM的情况出现。
class数量不断增加
分析到这里,已经有理由相信,是在程序运行中某段代码在不断的生成新得class,导致了metaSpace的空间一直上升。那么哪些情况下会引起class的动态增加呢?可能包含以下情况:
- 由于反射类加载,动态代理生成的类加载
- 动态或自定义的ClassLoader,在运行时不断的加载新的Class
不论哪种情况,一定是Metaspace的大小和加载类的数据有关系,加载的类越多metaspace占用的内存也就越大。
jmap
有了以上分析,接下来要做的就是如何查看当前JVM加载了哪些class呢?答案是可以借助于jdk自带的工具jmap来分析。
获取Java进程PID
使用以下命令获取到Java进程的PID。
ps -ef | grep 'java'
打印类信息
sudo -u ${loginUser} /usr/java/jdk11/bin/jmap -clstats ${pid} > /tmp/jamp-class-state.txt
- loginUser:用当前登录用户替换
- pid:上文中获取的PID
对比两次class差异
正常情况下JVM的metasp并不会明显增加,为了获取哪些class在不断增长,我们可以间隔一定时间后,再次执行jmap命令,重新获取class的状态,对比前后两次class的差异,其中的差异就是新增的class。
可以从对比图中看出,jdk.internal.reflect.GeneratedSerializationConstructorAccessor
这个类在不断的增长。接下来的事情就是将问题定位到具体的代码中,那么如何实现呢?
arthas
为了回答上文的问题,如何定位具体的代码调用链,可以借助于arthas完成。相信大家应该都有用过,这里不过多的介绍如何使用了,未使用过的同学可以查看官方文档学习下。
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
复制代码
有几个常用的命令可以简单介绍下:
thread -n 10 -i 1000
: 列出1秒内最忙的10个线程栈profiler start -d 10 --file /tmp/flame-graph.svg
:生产3秒内的火焰图trace demo.MathGame run -n 1
: 查看指定函数内部各模块调用时间,其中-n
指定捕捉次数
这里有点需要注意,一般生产环境是不允许访问外网的,需要将arthas的jar包自己通过正常的发布渠道添加的机器中。
我是通过分析火焰图来判断堆栈的。火焰图如下:
通过指定筛选条件,可以订位到具体的受影响class,进而可以获取到具体的代码逻辑。
代码分析
基于以上定位的代码,可以看出诱发jdk.internal.reflect.GeneratedSerializationConstructorAccessor
类不断增加的原因是和我们使用的RPC框架有关。
其中一段核心代码如下,客户端通过clientClass.getDeclaredConstructor
获取实例。
DerivedClient client = (DerivedClient) _clientCache.get(clientKey);
if (client == null)
synchronized (_clientCache) {
// 空歌白石:省略部分代码
Constructor<DerivedClient> ctor = clientClass.getDeclaredConstructor(paramTypes);
ctor.setAccessible(true);
client = ctor.newInstance(paramValues);
// 空歌白石:省略部分代码
}
return client;
复制代码
具体的底层都是用到了Reflection
相关的代码,ReflectionData
是Class
类的内部静态类被缓存起来,里面的属性就是反射操作时需要用的属性Field,方法Method和构造函数等。
private static class ReflectionData<T> {
volatile Field[] declaredFields;
volatile Field[] publicFields;
volatile Method[] declaredMethods;
volatile Method[] publicMethods;
volatile Constructor<T>[] declaredConstructors;
volatile Constructor<T>[] publicConstructors;
// Intermediate results for getFields and getMethods
volatile Field[] declaredPublicFields;
volatile Method[] declaredPublicMethods;
volatile Class<?>[] interfaces;
// Cached names
String simpleName;
String canonicalName;
static final String NULL_SENTINEL = new String();
// Value of classRedefinedCount when we created this ReflectionData instance
final int redefinedCount;
ReflectionData(int redefinedCount) {
this.redefinedCount = redefinedCount;
}
}
复制代码
Class
中的reflectionData()
方法负责延迟创建和缓存ReflectionData
。
private ReflectionData<T> reflectionData() {
SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
int classRedefinedCount = this.classRedefinedCount;
ReflectionData<T> rd;
// 空歌白石:判断缓存
if (reflectionData != null &&
(rd = reflectionData.get()) != null &&
rd.redefinedCount == classRedefinedCount) {
return rd;
}
// else no SoftReference or cleared SoftReference or stale ReflectionData
// -> create and replace new instance
return newReflectionData(reflectionData, classRedefinedCount);
}
复制代码
看到这里并没有什么问题,继续看源码,发现ReflectionData
是被SoftReference
包装的。
// 空歌白石:reflectionData属性定义
private transient volatile SoftReference<ReflectionData<T>> reflectionData;
复制代码
根源分析
ReflectionData
是被SoftReference
软引用修饰的,如果是软引用的话在内存空间不足时就可能会被回收掉,如果回收掉那下次再使用的话只能重新通过反射获取。
而SoftReference
是否被回收又和JVM的配置项SoftRefLRUPolicyMSPerMB
参数的值有关系。还记得上文中我们查看JVM参数时的截图吗?其中蓝色的部分就是关于SoftRefLRUPolicyMSPerMB
的配置,显然,我们配置的是0。
大家可能会问,SoftRefLRUPolicyMSPerMB
表示的含义是什么呢?可以这样理解SoftRefLRUPolicyMSPerMB
表示每MB堆空闲空间的 Soft Reference 保持存活的毫秒数
,JDK1.7默认值为1000
,也就是1秒。超过时间会被回收。
参数调整
根据以上分析,我们将生产上JVM参数进行了调整,可以有两种方案,一种直接删除SoftRefLRUPolicyMSPerMB
的配置,一种是将SoftRefLRUPolicyMSPerMB=1000
,两种效果是一样。最后将服务重新发布后,观察一天metaSpace的变化情况,发现MetaSpace已经不再像之前问题出现时经过一天的时间Metaspace不断的上升进而引起OOM了。
为什么不调整MaxMetaspaceSize
可能有同学一开始会问,为何不调整MaxMetaspaceSize
呢?MaxMetaspaceSize
表示最大元空间(Metaspace)内存大小
,我们是基于以下方面考量,首先,我们的服务中并没有热加载或使用自定义ClassLoader的地方,起码业务代码可以明确是没有的,在服务启动时占用的metaspace并不高,在现有的MaxMetaspaceSize
配置为256MB
情况下已经足够应用使用了。因此,我们从始至终都没有通过调整MaxMetaspaceSize
的方式来排查问题。
总结
线上任何服务的告警都应该得到重视,并深入分析其中的原因,通过对问题的深入分析和探究是提升自己的技术能力最有效的方法。