前言
// 呵呵 很快又该总结 2022 了, 希望这一年也能总结出很多收获
接着 java.lang.Class/java.lang.ClassLoader/InstanceKlass/ClassloaderData 的卸载
可以先看一下 这一篇文章, 明确一下 上下文
这里 主要说的是 如果我们的场景中存在自定义的 classloader 的情况下
应该 怎么尽可能少的减小 自定义classloader 的存在造成潜在的 内存泄漏的风险
第一是 classloader 的数量的控制, 其次是 一些代码层面的一些控制, 需要明确
另外 在文章 java.lang.Class/java.lang.ClassLoader/InstanceKlass/ClassloaderData 的卸载 中, 还有一些 可以看到的疑问, 没有提到的, 这里一起看一下
以下测试用例基于 jdk8, 部分截图基于 jdk9
测试用例
测试用例, 这里暂时使用第一个版本的测试用例, 我们再迭代
/**
* 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);
}
}
}
/**
* 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();
}
}
ChildFirstClassLoader 的引用有哪些 ?
如下, 首先要查询的是 classloader 加载了那些东西, 这里可以看到的是 只是加载了 com.hx.test12.Test29MultiLoaderContextMain
但是 实际不然 其实还有 com.hx.test12.Test29MultiLoaderContextMain 里面的这一个内部类
com.hx.test12.Test29MultiLoaderContextMain 的类型是没有实例的
com.hx.test12.Test29MultiLoaderContextMain$$Lambda$3/1032000752 类型存在一个实例, 传递给了 Thread-$N
其次是当前 Thread 上下文的一些引用
这里的 target 就是我们上面提到的 com.hx.test12.Test29MultiLoaderContextMain$$Lambda$3/1032000752 的实例
我们这里关心的是 inheritedAccessControlContext 中的 context 中的第一个 ProtectionDomain 元素, 可以看到的是 引用了一个 ChildFirstClassLoader
总结一下 我们明显能够看到的有两条路径
1. Thread-$N 关联的 target 是一个 com.hx.test12.Test29MultiLoaderContextMain$$Lambda$3/1032000752 的实例, 其 classloader 为 ChildFirstClassLoader
2. Thread-$N 关联的 inheritedAccessControlContext 中的 context[0] 会关联到 ChildFirstClassLoader
AccessController. inheritedAccessControlContext 是怎么来的?
上面我们可以看到 com.hx.test12.Test29MultiLoaderContextMain$$Lambda$3/1032000752 的实例也会关联 ChildFirstClassLoader, 那么我们这里是需要取消他对 ChildFirstClassLoader 的关联, 这个还是挺好处理的
那么另外一个呢? Thread.inheritedAccessControlContext 呢?
首先我们要看 它的初始化的地方 调用的是 AccessController.getStackAccessControlContext
AccessController.getStackAccessControlContext 是一个 native 方法
实现大致是如下, 从遍历当前线程的堆栈, 分别获取 调用的方法对应的 java.lang.Class 的 ProtectionDomain
分别是 Test29MultiLoaderContextMain 的 ProtectionDomain 和 Test29MultiLoaderContextInvoker 的 ProtectionDomain
呵呵 既然知道了构造的方式, 那么才有 处理的方式
输出一下 其中的两个元素, 大致是
java.security.ProtectionDomain
{0x00000007bf795450} - klass: 'java/security/ProtectionDomain'
- ---- fields (total size 5 words):
- private 'hasAllPerm' 'Z' @12 false
- private final 'staticPermissions' 'Z' @13 false
- private 'codesource' 'Ljava/security/CodeSource;' @16 a 'java/security/CodeSource'{0x00000007bf78d5c0} (f7ef1ab8 f7ef168a)
- private 'classloader' 'Ljava/lang/ClassLoader;' @20 a 'org/apache/flink/util/ChildFirstClassLoader'{0x00000007bf78b450} (f7ef168a f7ef2a91)
- private 'principals' '[Ljava/security/Principal;' @24 a 'java/security/Principal'[0] {0x00000007bf795488} (f7ef2a91 f7ef1af6)
- private 'permissions' 'Ljava/security/PermissionCollection;' @28 a 'java/security/Permissions'{0x00000007bf78d7b0} (f7ef1af6 f7ef2a8f)
- final 'key' 'Ljava/security/ProtectionDomain$Key;' @32 a 'java/security/ProtectionDomain$Key'{0x00000007bf795478} (f7ef2a8f 0)
java.security.ProtectionDomain
{0x00000007bf8bb638} - klass: 'java/security/ProtectionDomain'
- ---- fields (total size 5 words):
- private 'hasAllPerm' 'Z' @12 false
- private final 'staticPermissions' 'Z' @13 false
- private 'codesource' 'Ljava/security/CodeSource;' @16 a 'java/security/CodeSource'{0x00000007bf8c1518} (f7f182a3 f7f378cb)
- private 'classloader' 'Ljava/lang/ClassLoader;' @20 a 'jdk/internal/loader/ClassLoaders$AppClassLoader'{0x00000007bf9bc658} (f7f378cb f7f182a8)
- private 'principals' '[Ljava/security/Principal;' @24 a 'java/security/Principal'[0] {0x00000007bf8c1540} (f7f182a8 f7f182aa)
- private 'permissions' 'Ljava/security/PermissionCollection;' @28 a 'java/security/Permissions'{0x00000007bf8c1550} (f7f182aa f7f182ae)
- final 'key' 'Ljava/security/ProtectionDomain$Key;' @32 a 'java/security/ProtectionDomain$Key'{0x00000007bf8c1570} (f7f182ae 0)
去掉 ChildFirstClassLoader 的两个引用
调整 Test29MultiLoaderContextInvoker 如下
第一个是 新建 MyRunnable 的地方是 Test29MultiLoaderContextInvoker 中, 对应的 classloader 为 AppClassLoader
第二个是 新建 Thread 的地方也放在了 Test29MultiLoaderContextInvoker, 这样 Thread-$N 的 inheritedAccessControlContext 的 context 中就只会有一个 ProtectionDomain 关联的 classloader 为 AppClassLoader
/**
* 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");
Thread runnable = new Thread(new MyRunnable());
Method mainMethod = mainClass.getDeclaredMethod("main", int.class, Thread.class, String[].class);
mainMethod.invoke(null, i, runnable, args);
}
}
/**
* MyRunnable
*
* @author Jerry.X.He
* @version 1.0
* @date 2021-12-26 14:55
*/
private static class MyRunnable implements Runnable {
@Override
public void run() {
try {
int idx = 0;
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();
}
}
}
}
调整 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, Thread runnable, String[] args) throws Exception {
System.out.println(String.format(" Test29MultiLoaderContextMain.main be invoked, idx : %s, args : %s ", idx, args));
runnable.start();
}
}
看一下 是否符合我们上面的期望
然后 重新执行, 日志如下, 可以看到, 在执行 20 个循环的过程中存在 Test29MultiLoaderContextMain 的卸载, 这个就是 我们期望达到的结果
Test29MultiLoaderContextMain.main be invoked, idx : 0, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 1, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 2, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 3, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 4, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0098828]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0098028]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0097828]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0097028]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c007e828]
Test29MultiLoaderContextMain.main be invoked, idx : 5, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 6, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 7, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 8, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 9, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 10, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c007e828]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0097028]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0097828]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0098028]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0098828]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0099028]
Test29MultiLoaderContextMain.main be invoked, idx : 11, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 12, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 13, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 14, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 15, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 16, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0098828]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0098028]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0097828]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0097028]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c007e828]
[Unloading class com.hx.test12.Test29MultiLoaderContextMain 0x00000007c0099828]
Test29MultiLoaderContextMain.main be invoked, idx : 17, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 18, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
Test29MultiLoaderContextMain.main be invoked, idx : 19, args : [Ljava.lang.String;@2bea5ab4
bizThread - 0 is started
// after 10s
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
bizThread - 0 is end
最终的结论如下
1. 控制 自定义 ClassLoader 的数量, 避免比较麻烦的细节控制
2. 自定义 ClassLoader 加载的 class 需要尽可能的单纯, 少去 access 其他的上下文, 把控的成本很高, 有一定的经验要求
完
参考
java.lang.Class/java.lang.ClassLoader/InstanceKlass/ClassloaderData 的卸载