前言
之前 碰到了一个 flink 基于 ChildFirstClassLoader 来进行任务隔离 导致的内存泄漏的问题
然后 使用 demo 复现了一下 问题
之后 想探索一下 java language 中 类型的卸载相关
并且会 拓展一些其他的知识
以下测试用例基于 jdk8, 部分截图基于 jdk9
测试用例
Test29MultiLoaderContextInvoker 驱动类
/**
* Test29MultiLoaderContextInvoker
*
* @author Jerry.X.He <970655147@qq.com>
* @version 1.0
* @date 2021-12-19 09:31
*/
public class Test29MultiLoaderContextInvoker {
// Test29MultiLoaderContextInvoker
// -Xmx10M -XX:+UseSerialGC -XX:+TraceClassLoading
// -Xmx10M -XX:+UseSerialGC -XX:+TraceClassUnloading
public static void main(String[] args) throws Exception {
ClassLoader appClassloader = Test29MultiLoaderContextInvoker.class.getClassLoader();
String[] alwaysParentPatterns = new String[]{};
URL[] classpathes = new URL[]{
new File("/Users/jerry/IdeaProjects/HelloWorld/target/classes").toURI().toURL()
};
Consumer<Throwable> throwableConsumer = (ex) -> {
ex.printStackTrace();
};
int loopCount = 20;
for (int i = 0; i < loopCount; i++) {
ChildFirstClassLoader classLoader = new ChildFirstClassLoader(classpathes, appClassloader, alwaysParentPatterns, throwableConsumer);
Class mainClass = classLoader.loadClass("com.hx.test12.Test29MultiLoaderContextMain");
Method mainMethod = mainClass.getDeclaredMethod("main", int.class, String[].class);
mainMethod.invoke(null, i, args);
}
}
}
Test29MultiLoaderContextMain 业务类
/**
* Test29MultiLoaderContextInvoker
*
* @author Jerry.X.He <970655147@qq.com>
* @version 1.0
* @date 2021-12-19 09:31
*/
public class Test29MultiLoaderContextMain {
// hold 1M
public static byte[] dummyBytes = new byte[1 * 1024 * 1024];
// Test29MultiLoaderContextInvoker
public static void main(int idx, String[] args) throws Exception {
System.out.println(String.format(" Test29MultiLoaderContextMain.main be invoked, idx : %s, args : %s ", idx, args));
new Thread(() -> {
try {
System.out.println(String.format(" bizThread - %s is started ", idx));
Thread.sleep(10_000);
System.out.println(String.format(" bizThread - %s is end ", idx));
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
执行结果如下
可以看到 应该是第五次启动了之后, 发生了一次 full gc, 卸载了一部分类型, 然后 后面还是发生了 OOM, 一个 Test29MultiLoaderContextMain.class 会占用 1M 的空间
6 个占用了 6M, 然后 第七次 invoke 的时候发生了 OOM
假设我们去掉 Thread.sleep(10_000)
可以看到的是 20次 invoke 均成功执行了, 这次执行里面 我们发现有对 Test29MultiLoaderContextMain.class 的卸载
jls 中类型的卸载
12.7. Unloading of Classes and Interfacesd
可以按到的是 类型卸载的前提是 加载它的 classloader 被gc了
对应于我们这里, 要想卸载 Test29MultiLoaderContextMain.class 就得保证 加载他的 ChilldFirstClassLoader 被卸载了
HotSpotVM 中的类型卸载
以下运行时截图 基于 jdk9, 调试基于 SerialGC
这里是 输出 "unloading class com.hx.test12.Test29MultiLoaderContextMain" 打印的地方
堆栈信息提上来一点, 看到这里有一个 data->is_alive(is_alive_closure) 的判断, 判断的就是 对应的 classloader 是否被 gc 标记(还存活)
当然 这里仅仅是一些标记操作, 真真的 clean 操作是在 gc 之后, 清理掉这些 classloaderData, InstanceKlass 什么的
java.lang.ClassLoader 的创建和回收
我们这里讨论的是 我们自定义的业务的 ClassLoader
创建是在 new ChildFirstClassloader 的时候
回收则是 gc 来处理的
不可到达 就被回收了
classLoaderData 的创建和回收
它的创建是 在 resolve InstanceKlass 的时候, 一个 classloader 对应于一个 classLoaderData, 基于 new 分配
classLoaderData 的清理是在标记完之后, gc 之后, 进行 delete 的
InstanceKlass 的创建和回收
注意这里的 new 后面的参数, loader_data, size
klass 是重写了 operator new, 实际分配的空间是属于 loader_data.metaspace.class_vsm, 是属于我们常说到的 Metaspace[元空间]
之所以 要提到这个, 是因为 空间的回收和这个是有关系的
InstanceKlass 的回收, 是 classLoaderData 回收的一部分
回收 metaspace 之前, 如下, 一切 都很正常
delete metaspace 之后, 可以看到 相关字段都更新为了 dummy word, 可以大致判断出的是 InstanceKlass 被回收了
具体填充 dummy word 的地方在这里, 回收 chunk 的地方
java.lang.Class 的创建和回收
是在创建 InstanceKlass, 初始化的时候 创建的 java.lang.Class
回收 则是 gc 来处理的
不可到达, 就被 回收了
以上几种 分配/回收 模式
从上面可以看到, 有几种内存的 分配, 回收的方式
1. 基于 c++ 原生的 new, delete
2. 基于内存池, 上面的 metaspace, heap 均属于
完