52 如何 尽可能的减少 自定义ClassLoader 造成的影响

Posted 蓝风9

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了52 如何 尽可能的减少 自定义ClassLoader 造成的影响相关的知识,希望对你有一定的参考价值。

前言 

// 呵呵 很快又该总结 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 的卸载

以上是关于52 如何 尽可能的减少 自定义ClassLoader 造成的影响的主要内容,如果未能解决你的问题,请参考以下文章

C# 滚动自定义图形以减少闪烁的更好方法

如何减少iOS导航栏自定义视图的左右间隙

std::map 上的自定义 omp 减少

Android Fabric - 以自定义间隔发送捕获的异常

项目代码规范

Swift 减少元组数组自定义数据模型