发现问题
在工作中遇到了一个元空间内存溢出问题,问题出在一个用户输入Java文件,后台负责编译并执行Java文件的功能上,因为用户能随时对Java文件进行修改,所以我们每次执行这个文件的时候都会重新编译,new URLClassLoader来加载这个类,这样的话每次都是加载最新的Class,如果用同一个ClassLoader对象去加载同一个类,是不会重复去加载的。每调用一次这个执行接口,元空间就会增加一个class对象,随着调用次数增多,元空间就慢慢被沾满,这些Class对象却不能被卸载掉,为啥呢?按说Class对象只要满足3个条件就能被卸载:
- Class对象的所有实例都没有直接引用
- 加载class的ClassLoader也没有直接引用
- class对象没有被任何地方引用
寻找原因
看条件好像都符合,没有办法就写个简单的类测试一下。
public class UrlClassLoaderTest {
public static void main(String[] args) throws Exception {
while (true) {
loadClass();
}
}
private static void loadClass() throws Exception {
URLClassLoader loader = new URLClassLoader(new URL[]{ new File("E://test/").toURI().toURL()}, Thread.currentThread().getContextClassLoader());
Class aClass = loader.loadClass("com.example.demo.TestImpl");
TestInterface testInterface = (TestInterface) aClass.newInstance();
testInterface.call(null);
}
}
执行的时候加上参数:
-verbose:class -XX:MaxMetaspaceSize=30M
执行一段时间可以看到会去卸载类:
思路没有问题,继续根据接源代码完善测试类, 里面一共有加载三个类,一个入参,一个出参,一个service,接口传进来json,用fastjson转换成入参对象,模拟一下:
public class UrlClassLoaderTest {
public static void main(String[] args) throws Exception {
while (true) {
loadClass();
}
}
private static void loadClass() throws Exception {
URLClassLoader loader = new URLClassLoader(new URL[]{new File("E://test/").toURI().toURL()}, Thread.currentThread().getContextClassLoader());
Class aClass = loader.loadClass("com.example.demo.TestImpl");
Class bClass = loader.loadClass("com.example.demo.TestParam");
TestInterface testInterface = (TestInterface) aClass.newInstance();
JSONObject jsonObject = new JSONObject();
testInterface.call(jsonObject.toJavaObject(bClass));
}
}
这个时候再去执行,发现没多久就内存溢出了:
问题就出在jsonObject.toJavaObject方法上,进源代码查看,发现这个方法里面居然把Class对象保存起来了,toJavaObject这个里面调用了TypeUtils.castToJavaBean,传了 ParserConfig.getGlobalInstance()这个全局对象
public <T> T toJavaObject(Class<T> clazz) {
if (clazz == Map.class || clazz == JSONObject.class || clazz == JSON.class) {
return (T) this;
}
if (clazz == Object.class && !containsKey(JSON.DEFAULT_TYPE_KEY)) {
return (T) this;
}
return TypeUtils.castToJavaBean(this, clazz, ParserConfig.getGlobalInstance());
}
然后TypeUtils.castToJavaBean这个方法里面有调用config.get(clazz),先根据class去获取ObjectDeserializer,如果不存在就会新创建,然后把class作为key保存到config的map里面,这样就造成了类的卸载不符合第3个条件。
ObjectDeserializer deserializer = config.get(clazz);
if(deserializer != null){
String json = JSON.toJSONString(object);
return JSON.parseObject(json, clazz);
}
解决办法
调用jsonObject.toJavaObject(bClass,new ParserConfig(),1)这个方法,这样用的就是私有的config,随着方法的结束,这个config也再没有引用,可以被回收。另外Object转json也有缓存class对象,如果想要class对象不被缓存,调用JSONObject.toJSONString(new Object(), serializeConfig);