zoukankan      html  css  js  c++  java
  • 【Java虚拟机8】自定义类加载器、类加载器命名空间、类的卸载

    前言

    学习类加载器就一定要自己实现一个类加载器,今天就从一个简单的自定义类加载器说起。

    自定义类加载器

    例1

    一个简单的类加载器,从一个给定的二进制名字读取一个字节码文件的内容,然后生成对应的class对象。

    package com.jamie.jvmstudy;
    
    import java.io.*;
    
    public class CustomizedClassLoader extends ClassLoader {
    
        private String classLoaderName;
    
        private String fileExtension = ".class";
    
        public CustomizedClassLoader(String classLoaderName) {
            super(); //如果调用默认构造器,代表默认的父类加载器是系统类加载器SystemClassLoader
            this.classLoaderName = classLoaderName;
        }
    
        public CustomizedClassLoader(ClassLoader parent, String classLoaderName) {
            super(parent); //如果指定父类加载器,那么该构造器执行完之后,这个类加载器就有指定的parent了。(默认是系统类加载器)
            this.classLoaderName = classLoaderName;
        }
    
        @Override
        public Class<?> findClass(String className) throws ClassNotFoundException {
            byte[] data = this.loadClassData(className);
    
            return this.defineClass(className, data, 0, data.length);
        }
    
        private byte[] loadClassData(String className) {
            byte[] data = null;
            try(InputStream is = new FileInputStream(new File(className + this.fileExtension));
                ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                int ch;
                while(-1 != (ch = is.read())) {
                    baos.write(ch);
                }
                data = baos.toByteArray();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return data;
        }
    
        public static void main(String[] args) throws Exception {
            CustomizedClassLoader customizedClassLoader = new CustomizedClassLoader("jamie loader");
            test(customizedClassLoader);
        }
    
        private static void test(ClassLoader classLoader) throws Exception {
            Class<?> myClass = classLoader.loadClass("com.jamie.jvmstudy.TestClassLoader");
            Object o = myClass.newInstance();
            System.out.println(String.format("classLoader in this method is [%s]", classLoader));
            System.out.println(String.format("object [%s] has been created by [%s].", o, myClass.getClassLoader()));
            System.out.println(String.format("class loader of CustomizedClassLoader is [%s]", classLoader.getClass().getClassLoader()));
        }
    }
    

    运行结果如下:

    classLoader in this method is [com.jamie.jvmstudy.CustomizedClassLoader@4b67cf4d]
    object [com.jamie.jvmstudy.TestClassLoader@7ea987ac] has been created by [sun.misc.Launcher$AppClassLoader@14dad5dc].
    class loader of CustomizedClassLoader is [sun.misc.Launcher$AppClassLoader@14dad5dc]
    

    这里重点说明一下:示例中的"com.jamie.jvmstudy.TestClassLoader"Class是被系统类加载器加载的,而不是我们自定义的加载器。
    原因:

    1. 自定义的加载器CustomizedClassLoader的父类构造器是系统类加载器。因为我们加载类调用的方法是:classLoader.loadClass("com.jamie.jvmstudy.TestClassLoader")
    2. 默认的loadClass()方法实现就是双亲委派的源码实现,因为系统类加载器会在当前的classpath(类路径)下查找是否存在匹配的"binary name",如果存在,则系统类加载器加载成功。
    3. 所以只要类路径下存在匹配的二进制名字的字节码,就会被系统类加载器成功加载。

    例2

    基于例1的基础上,自定义加载器获取字节码文件的内容改为从一个指定的路径中读取。并且传入一个非classpath的路径,去加载某个字节码。
    因为自定义的类加载器默认的父类加载器是系统类加载器,运行下例的时候,需要在编译之后把类路径下的com.jamie.jvmstudy.TestClassLoader字节码文件删除(防止[双亲委托机制]使[系统类加载器AppClassLoader]把指定类加载进虚拟机),然后运行本例。

    package com.jamie.jvmstudy;
    
    import java.io.*;
    
    public class CustomizedClassLoader extends ClassLoader {
    
        private String classLoaderName;
    
        private String path;
    
        private String fileExtension = ".class";
    
        public CustomizedClassLoader(String classLoaderName) {
            super(); //如果调用默认构造器,代表默认的父类加载器是系统类加载器SystemClassLoader
            this.classLoaderName = classLoaderName;
        }
    
        public CustomizedClassLoader(ClassLoader parent, String classLoaderName) {
            super(parent);
            this.classLoaderName = classLoaderName;
        }
    
        @Override
        public Class<?> findClass(String className) throws ClassNotFoundException {
            System.out.println("Self findClass() invoked");
            byte[] data = this.loadClassData(className);
            return this.defineClass(className, data, 0, data.length);
        }
    
        private byte[] loadClassData(String className) {
            byte[] data = null;
            className = className.replace(".", "/");
            try(InputStream is = new FileInputStream(new File(path + className + this.fileExtension));
                ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                int ch;
                while(-1 != (ch = is.read())) {
                    baos.write(ch);
                }
                data = baos.toByteArray();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return data;
        }
    
        public static void main(String[] args) throws Exception {
            CustomizedClassLoader loader1 = new CustomizedClassLoader("jamie loader1");
            loader1.setPath("D:/temp/");
            Class<?> myClass1 = loader1.loadClass("com.jamie.jvmstudy.TestClassLoader");
            System.out.println(String.format("Hashcode of myClass1 is [%s].", myClass1.hashCode()));
            System.out.println(String.format("myClass1 is [%s]", myClass1));
            System.out.println();
    
            CustomizedClassLoader loader2 = new CustomizedClassLoader("jamie loader2");
            loader2.setPath("D:/temp/");
            Class<?> myClass2 = loader2.loadClass("com.jamie.jvmstudy.TestClassLoader");
            System.out.println(String.format("Hashcode of myClass2 is [%s].", myClass2.hashCode()));
            System.out.println(String.format("myClass2 is [%s]", myClass2));
    
            System.out.println();
            System.out.println("myClass1 == myClass2 ? " + (myClass1 == myClass2));
        }
    
        public void setPath(String path) {
            this.path = path;
        }
    }
    

    运行结果如下:

    Self findClass() invoked
    Hashcode of myClass1 is [1956725890].
    myClass1 is [class com.jamie.jvmstudy.TestClassLoader]
    
    Self findClass() invoked
    Hashcode of myClass2 is [21685669].
    myClass2 is [class com.jamie.jvmstudy.TestClassLoader]
    
    myClass1 == myClass2 ? false
    

    结论:由运行结果可以看出,此时的虚拟机中出现了两个不一样的TestClassLoader.class对象。
    这就引入了类加载器的命名空间的问题
    我们之前理解的“Class对象只存在一份”是基于同一个类加载器的命名空间来说的。

    类加载器的命名空间

    • 每个类加载器都有自己的命名空间。命名空间由该加载器和所有父加载器所加载的类组成。
    • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
    • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

    后续写了一篇文章类加载器之命名空间详解,里面有对【命名空间】更加详细的分析与示例。感兴趣请跳转。

    类的卸载

    • 当一个类被加载、连接和初始化之后,它的生命周期就开始了。当代表该类的Class对象不再被引用,既不可达时,Class对象就会结束生命周期,该类在方法区内的数据也会被卸载,从而结束该类的生命周期。
    • 一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
    • 被Java虚拟机自带的ClassLoader加载的类,是不会被卸载的。因为JVM本身会始终引用这些ClassLoader,而这些ClassLoader始终会引用它加载的所有类的Class对象。所以它们永远可达。
    • 只有自定义的类加载器加载的类,才有可能被卸载。

    类卸载的证明:

    基于上述例2,改造一下main方法,并在执行时,添加JVM运行参数:-XX:+TraceClassUnloading

        public static void main(String[] args) throws Exception {
            CustomizedClassLoader loader1 = new CustomizedClassLoader("jamie loader1");
            loader1.setPath("D:/temp/");
            Class<?> myClass1 = loader1.loadClass("com.jamie.jvmstudy.TestClassLoader");
            System.out.println(String.format("Hashcode of myClass1 is [%s].", myClass1.hashCode()));
            System.out.println(String.format("myClass1 is [%s]", myClass1));
            System.out.println();
    
            loader1 = null;
            myClass1 = null;
            System.gc();
    
            CustomizedClassLoader loader2 = new CustomizedClassLoader("jamie loader2");
            loader2.setPath("D:/temp/");
            Class<?> myClass2 = loader2.loadClass("com.jamie.jvmstudy.TestClassLoader");
            System.out.println(String.format("Hashcode of myClass2 is [%s].", myClass2.hashCode()));
            System.out.println(String.format("myClass2 is [%s]", myClass2));
    
            System.out.println();
            System.out.println("myClass1 == myClass2 ? " + (myClass1 == myClass2));
            Thread.sleep(50000); //为了查看类的卸载情况增加延时
        }
    

    运行结果:可以看到TestClassLoader类被卸载一次。

    =====Self findClass() invoked=====
    Hashcode of myClass1 is [1956725890].
    myClass1 is [class com.jamie.jvmstudy.TestClassLoader]
    
    [Unloading class com.jamie.jvmstudy.TestClassLoader 0x00000007c0061028]
    =====Self findClass() invoked=====
    Hashcode of myClass2 is [21685669].
    myClass2 is [class com.jamie.jvmstudy.TestClassLoader]
    
    myClass1 == myClass2 ? false
    

    增加main方法睡眠时间,使用Java VisualVM可以看到下图:

  • 相关阅读:
    转几篇关于反射的文章
    几篇并发的文章
    线程池的使用(转)
    深入理解java不可变对象(转)
    收集的书
    BeanPostProcessor(转)
    JDK的动态代理深入解析(Proxy,InvocationHandler)(转)
    Java中InputStream和String之间的转换方法
    linux的一些常用命令
    Linux下查看文件内容的命令
  • 原文地址:https://www.cnblogs.com/1626ace/p/13436766.html
Copyright © 2011-2022 走看看