类加载

作者: hehehehe | 来源:发表于2020-11-19 22:30 被阅读0次

https://www.cnblogs.com/throwable/p/12272269.html

public class Sample {
    public void say() {
        System.out.println("Hello Doge!");
    }
}

如果使用字节码工具修改say()方法的内容为System.out.println("Hello Throwable!");,并且使用自定义的ClassLoader重新加载一个同类名的Sample类,那么通过new关键字实例化出来的Sample对象调用say()到底打印"Hello Doge!"还是"Hello Throwable!"?

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.24.0-GA</version>
</dependency>
public class Demo {

    public void say() {
        System.out.println("Hello Doge!");
    }
}

// 一次性使用的自定义类加载器
public class CustomClassLoader extends ClassLoader {

    private final byte[] data;

    public CustomClassLoader(byte[] data) {
        this.data = data;
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (!Demo.class.getName().equals(name)) {
            return super.loadClass(name);
        }
        return defineClass(name, data, 0, data.length);
    }
}

public class Main {

    public static void main(String[] args) throws Exception {

        String name = Demo.class.getName();
        CtClass ctClass = ClassPool.getDefault().getCtClass(name);
        CtMethod method = ctClass.getMethod("say", "()V");
        method.setBody("{System.out.println(\"Hello Throwable!\");}");
        byte[] bytes = ctClass.toBytecode();
        CustomClassLoader classLoader = new CustomClassLoader(bytes);
        // 新的Demo类,只能反射调用,因为类路径中的Demo类已经被应用类加载器加载
        Class<?> newDemoClass = classLoader.loadClass(name);
        // 类路径中的Demo类
        Demo demo = new Demo();
        demo.say();
        // 新的Demo类
        newDemoClass.getDeclaredMethod("say").invoke(newDemoClass.newInstance());
        // 比较
        System.out.println(newDemoClass.equals(Demo.class));
    }
}
  • new关键字只能使用在当前类路径下的类的实例化,而这些类都是由应用类加载器加载,如果上面的例子中newDemoClass.newInstance()强制转换为Demo类型会报错。
  • 通过自定义类加载器加载的和当前类路径相同名全类名的类只能通过反射去使用,而且即使全类名相同,由于类加载器隔离,它们其实是不相同的类。
如何避免类重新加载导致内存溢出

JDK没有提供方法去卸载一个已经加载的类,也就是类的生命周期是由JVM管理的,因此要解决类重新加载导致内存溢出的问题归根结底就是解决重新加载的类被回收的问题。由于创建出来是的java.lang.Class对象,如果需要回收它,则要考虑下面几点:

1、java.lang.Class对象反射创建的实例需要被回收。
2、java.lang.Class对象不能被任何地方强引用。
3、加载java.lang.Class对象的ClassLoder已经被回收。

public class Demo {
    // 这里故意建立一个数组占用大量内存
    private int[] array = new int[1000];

    public void say() {
        System.out.println("Hello Doge!");
    }
}

public class Main {

    private static final Map<ClassLoader, List<Class<?>>> CACHE = new HashMap<>();

    public static void main(String[] args) throws Exception {
        String name = Demo.class.getName();
        CtClass ctClass = ClassPool.getDefault().getCtClass(name);
        CtMethod method = ctClass.getMethod("say", "()V");
        method.setBody("{System.out.println(\"Hello Throwable!\");}");
        for (int i = 0; i < 100000; i++) {
            byte[] bytes = ctClass.toBytecode();
            CustomClassLoader classLoader = new CustomClassLoader(bytes);
            // 新的Demo类,只能反射调用,因为类路径中的Demo类已经被应用类加载器加载
            Class<?> newDemoClass = classLoader.loadClass(name);
            add(classLoader, newDemoClass);
        }
        // 清理类加载器和它加载过的类
        clear();
        System.gc();
        Thread.sleep(Integer.MAX_VALUE);
    }

    private static void add(ClassLoader classLoader, Class<?> clazz) {
        if (CACHE.containsKey(classLoader)) {
            CACHE.get(classLoader).add(clazz);
        } else {
            List<Class<?>> classes = new ArrayList<>();
            CACHE.put(classLoader, classes);
            classes.add(clazz);
        }
    }

    private static void clear() {
        CACHE.clear();
    }
}

使用VM参数-XX:+PrintGC -XX:+PrintGCDetails执行上面的方法,JDK11默认使用G1收集器,由于Z收集器还在实验阶段,不是很建议使用,执行main方法后输出:

[11.374s][info   ][gc,task       ] GC(17) Using 8 workers of 8 for full compaction
[11.374s][info   ][gc,start      ] GC(17) Pause Full (System.gc())
[11.374s][info   ][gc,phases,start] GC(17) Phase 1: Mark live objects
[11.429s][info   ][gc,stringtable ] GC(17) Cleaned string and symbol table, strings: 5637 processed, 0 removed, symbols: 135915 processed, 0 removed
[11.429s][info   ][gc,phases      ] GC(17) Phase 1: Mark live objects 54.378ms
[11.429s][info   ][gc,phases,start] GC(17) Phase 2: Prepare for compaction
[11.429s][info   ][gc,phases      ] GC(17) Phase 2: Prepare for compaction 0.422ms
[11.429s][info   ][gc,phases,start] GC(17) Phase 3: Adjust pointers
[11.430s][info   ][gc,phases      ] GC(17) Phase 3: Adjust pointers 0.598ms
[11.430s][info   ][gc,phases,start] GC(17) Phase 4: Compact heap
[11.430s][info   ][gc,phases      ] GC(17) Phase 4: Compact heap 0.362ms
[11.648s][info   ][gc,heap        ] GC(17) Eden regions: 44->0(9)
[11.648s][info   ][gc,heap        ] GC(17) Survivor regions: 12->0(12)
[11.648s][info   ][gc,heap        ] GC(17) Old regions: 146->7
[11.648s][info   ][gc,heap        ] GC(17) Humongous regions: 3->2
[11.648s][info   ][gc,metaspace   ] GC(17) Metaspace: 141897K->9084K(1062912K)
[11.648s][info   ][gc             ] GC(17) Pause Full (System.gc()) 205M->3M(30M) 273.440ms
[11.648s][info   ][gc,cpu         ] GC(17) User=0.31s Sys=0.08s Real=0.27s

可见FullGC之后,元空间(Metaspace)回收了(141897-9084)KB,一共回收了202M的内存空间,初步可以认为元空间的内存被回收了,接下来注释掉main方法中调用的clear()方法,再调用一次main方法:

下来注释掉main方法中调用的clear()方法,再调用一次main方法:

....
[4.083s][info   ][gc,heap        ] GC(17) Humongous regions: 3->2
[4.083s][info   ][gc,metaspace   ] GC(17) Metaspace: 141884K->141884K(1458176K)
[4.083s][info   ][gc             ] GC(17) Pause Full (System.gc()) 201M->166M(564M) 115.504ms
[4.083s][info   ][gc,cpu         ] GC(17) User=0.84s Sys=0.00s Real=0.12s

可见元空间在FullGC执行没有进行回收,而堆内存的回收率也比较低,由此可以得出一个经验性的结论:只需要通过ClassLoader对象做映射关系保存使用它加载出来的新的类,只需要确保这些类没有没强引用、类实例都已经销毁,那么只需要移除ClassLoader对象的引用,那么在JVM进行GC的时候会把ClassLoader对象以及使用它加载的类回收,这样做就可以避免元空间的内存泄漏。

相关文章

网友评论

      本文标题:类加载

      本文链接:https://www.haomeiwen.com/subject/esrfiktx.html