大量加载类可能导致元空间OOM。首先回顾下类卸载需要满足的条件,要同时满足以下3个条件:

  1. 该类的实例全部被回收
  2. 加载该类的ClassLoader被回收
  3. 该Class对象没在任何地方引用,也无法使用反射来访问。

添加类加载日志,限制元空间大小,验证类是否卸载:-XX:+TraceClassLoading -XX:+TraceClassUnloading -XX:MaxMetaspaceSize=30m

下面这段代码能正常卸载BaseValue类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) {
    for (int i = 0; i < 1000; i++) {
        testLoader();
    }
}

// 
@SneakyThrows
public static void testLoader() {
    GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
    String hello = "package com.example.cachedemo\n" +
        "\n" +
        "import cn.hutool.core.util.IdUtil\n" +
        "\n" +
        "class BaseValue {\n" +
        "    def map = [\"abc.a\": \"1\", \"abc.b\": \"2\", \"abc.c\": IdUtil.getSnowflake().nextIdStr()]\n" +
        "}";
    Class aClass = groovyClassLoader.parseClass(hello);
    try {
        GroovyObject object = (GroovyObject) aClass.getConstructor().newInstance();
        Map<String, Object> map = (Map<String, Object>)object.getProperty("map");
    } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
        throw new RuntimeException(e);
    }
}

下面这个也能正常卸载,是因为aClass的类加载器实际是groovy.lang.GroovyClassLoader$InnerLoader,每次parseClass都使用一个新的groovy.lang.GroovyClassLoader$InnerLoader。所以testLoader结束后类可回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void main(String[] args) {
    for (int i = 0; i < 1000; i++) {
        testLoader();
    }
    // 数量是1, 暂且不清楚是为什么。
    System.out.println(groovyClassLoader.getLoadedClasses().length);
}


public static GroovyClassLoader groovyClassLoader = new GroovyClassLoader();

@SneakyThrows
public static void testLoader() {
    String hello = "package com.example.cachedemo\n" +
        "\n" +
        "import cn.hutool.core.util.IdUtil\n" +
        "\n" +
        "class BaseValue {\n" +
        "    def map = [\"abc.a\": \"1\", \"abc.b\": \"2\", \"abc.c\": IdUtil.getSnowflake().nextIdStr()]\n" +
        "}";
    Class aClass = groovyClassLoader.parseClass(hello);
    try {
        GroovyObject object = (GroovyObject) aClass.getConstructor().newInstance();
        Map<String, Object> map = (Map<String, Object>)object.getProperty("map");
    } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
        throw new RuntimeException(e);
    }
}

不能正常卸载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

public static void main(String[] args) {
    for (int i = 0; i < 1000; i++) {
        testLoader();
    }
    // 数量是1
    System.out.println(groovyClassLoader.getLoadedClasses().length);
}

public static List l = new ArrayList();
@SneakyThrows
public static void testLoader() {

    String hello = "package com.example.cachedemo\n" +
        "\n" +
        "import cn.hutool.core.util.IdUtil\n" +
        "\n" +
        "class BaseValue {\n" +
        "    def map = [\"abc.a\": \"1\", \"abc.b\": \"2\", \"abc.c\": IdUtil.getSnowflake().nextIdStr()]\n" +
        "}";
    Class aClass = groovyClassLoader.parseClass(hello);
    l.add(aClass);
    try {
        GroovyObject object = (GroovyObject) aClass.getConstructor().newInstance();
        Map<String, Object> map = (Map<String, Object>)object.getProperty("map");
    } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
        throw new RuntimeException(e);
    }
}