记一次元空间内存溢出问题

发现问题

在工作中遇到了一个元空间内存溢出问题,问题出在一个用户输入Java文件,后台负责编译并执行Java文件的功能上,因为用户能随时对Java文件进行修改,所以我们每次执行这个文件的时候都会重新编译,new URLClassLoader来加载这个类,这样的话每次都是加载最新的Class,如果用同一个ClassLoader对象去加载同一个类,是不会重复去加载的。每调用一次这个执行接口,元空间就会增加一个class对象,随着调用次数增多,元空间就慢慢被沾满,这些Class对象却不能被卸载掉,为啥呢?按说Class对象只要满足3个条件就能被卸载:

  1. Class对象的所有实例都没有直接引用
  2. 加载class的ClassLoader也没有直接引用
  3. 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

执行一段时间可以看到会去卸载类:
记一次元空间内存溢出问题_第1张图片
思路没有问题,继续根据接源代码完善测试类, 里面一共有加载三个类,一个入参,一个出参,一个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));
    }
}

这个时候再去执行,发现没多久就内存溢出了:
记一次元空间内存溢出问题_第2张图片
问题就出在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);

你可能感兴趣的:(技术博客,java,内存泄漏)