深入理解JVM-类加载器深入解析(3)

深入理解JVM-类加载器深入解析(3)

获得ClassLoader的途径

获得当前类的ClassLoader
clazz.getClassLoader()
获得当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
获得系统的ClassLoader
ClassLoader.getSystemClassLoader()
获得调用者的ClassLoader
DriverManager.getCallerClassLoader()

例子:

public class MyTest13 {

    public static void main(String[] args) {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();

        System.out.println(classLoader);

        while (null != classLoader) {
            classLoader = classLoader.getParent();
            System.out.println(classLoader);
        }
    }
}

public class MyTest14 {

    public static void main(String[] args) throws IOException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        String resourceName = "jvm/classloader/MyTest13.class";

        Enumeration<URL> resources = classLoader.getResources(resourceName);
        while (resources.hasMoreElements()) {
            URL url = resources.nextElement();
            System.out.println(url);
        }
    }
}

数组的类加载器是在运行期间jvm为其创建的,如果该数组是一个引用类型的数组,那么这个数组的类加载器就是加载该引用类型的类的类加载器,如果是原生类型的数组,那么这个数组是没有类加载器

public class MyTest15 {

    /**
     * String的类加载器是根类加载器,打印出来为null
     * null
     * --------
     * sun.misc.Launcher$AppClassLoader@18b4aac2
     * ---------
     * 原始类型的数据没有类加载器
     * null
     * @param args
     */
    public static void main(String[] args) {
        String[] strings = new String[2];
        System.out.println(strings.getClass().getClassLoader());

        System.out.println("--------");

        MyTest15[] myTest15s = new MyTest15[2];
        System.out.println(myTest15s.getClass().getClassLoader());

        System.out.println("---------");

        int[] ints = new int[2];
        System.out.println(ints.getClass().getClassLoader());
        
    }
}

命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所以附加在其所加载的类组成.
  • 在同一个命名空间中,不会出来类的完整名字(包括类的包名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
  • 同一个命名空间内的类是相互可见的
  • 子加载器的命名矿建包含所有父加载器的命名空间.因此自加载器加载的类能看见附加在其加载的类.例如系统类加载器加载的类能看见根类加载器加载的类
  • 由父加载器加载的类不能看见子加载器加载的类
  • 如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见

类的卸载

当MySample类被加载,连接和初始化后,它的生命周期就开始了.当代表MySample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,MySample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期.
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期
由java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载.
由用户自定义的类加载器所加载的类是可以被卸载的

public class MyTest16 extends ClassLoader{

    private final String fileExtension = ".class";

    private String classLoadName;

    private String path;

    public MyTest16(String classLoadName) {
        super();
        this.classLoadName = classLoadName;
    }

    public MyTest16(ClassLoader parent,String classLoadName) {
        super(parent);
        this.classLoadName = classLoadName;
    }

    public void setPath(String path) {
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String className) {
        System.out.println("findClass invoked: " + className);
        System.out.println("class loader name: " + this.classLoadName);
        byte[] data = this.loadClassData(className);
        return this.defineClass(className, data, 0, data.length);
    }

    private byte[] loadClassData(String name) {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;

        name = name.replace(".", "/");
        try {
            is = new FileInputStream(new File(this.path+name + this.fileExtension));
            baos = new ByteArrayOutputStream();

            int ch = 0;
            while (-1 != (ch = is.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                is.close();
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }


    public static void main(String[] args) throws IllegalAccessException, InstantiationException,
            ClassNotFoundException, InterruptedException {

        /**
         * findClass invoked: jvm.classloader.Mytest1
         * class loader name: loader1
         * jvm.classloader.Mytest1@60e53b93
         *
         * [Unloading class jvm.classloader.Mytest1 0x00000007c0061028]
         * findClass invoked: jvm.classloader.Mytest1
         * class loader name: loader1
         * class: 644117698
         * jvm.classloader.Mytest1@6f94fa3e
         *
         * 我们在运行的时候加上-XX:+TraceClassUnloading 这个参数
         * 我们在gc之后就会打印出class卸载的信息
         */
        MyTest16 loader1 = new MyTest16("loader1");
        loader1.setPath("/Users/luozhiyun/Downloads/test/");
        Class<?> clazz = loader1.loadClass("jvm.classloader.Mytest1");
        Object object = clazz.newInstance();
        System.out.println(object);
        System.out.println();

        loader1 = null;
        clazz = null;
        object = null;

        System.gc();

        Thread.sleep(10000);

        loader1 = new MyTest16("loader1");
        loader1.setPath("/Users/luozhiyun/Downloads/test/");
        clazz = loader1.loadClass("jvm.classloader.Mytest1");
        System.out.println("class: " + clazz.hashCode());
        object = clazz.newInstance();
        System.out.println(object);
        System.out.println();

        /**
         * findClass invoked: jvm.classloader.Mytest1
         * class loader name: loader1
         * jvm.classloader.Mytest1@60e53b93
         * findClass invoked: jvm.classloader.Mytest1
         * class loader name: loader1
         * jvm.classloader.Mytest1@266474c2
         * 打印出来的两个加载出来的类的实例都不一样
         */
        //MyTest16 myTest16 = new MyTest16("loader1");
        //
        //myTest16.setPath("/Users/luozhiyun/Downloads/test/");
        //
        //Class<?> clazz = myTest16.loadClass("jvm.classloader.Mytest1");
        //
        //Object object = clazz.newInstance();
        //
        //System.out.println(object);
        //
        //
        //MyTest16 loader = new MyTest16("loader1");
        //
        //loader.setPath("/Users/luozhiyun/Downloads/test/");
        //
        //Class<?> clazz2 = loader.loadClass("jvm.classloader.Mytest1");
        //
        //Object object2 = clazz2.newInstance();
        //
        //System.out.println(object2); 
    }
}
public class MySample {

    public MySample() {
        System.out.println("MySample: " + MyCat.class.getClassLoader());

        new MyCat();
    }
}

public class MyCat {

    public MyCat() {
        System.out.println("mycat: " + MyCat.class.getClassLoader());
    }
}


public class MyTest17 {

    public static void main(String[] args) throws Exception {
        MyTest16 loader1 = new MyTest16("loader1");

        Class<?> clazz = loader1.loadClass("jvm.classloader.MySample");
        System.out.println("class: " + clazz.hashCode());
        //如果注释掉该行,那么并不会实例化MySample对象,即MySample构造方法不会被调用
        // 因此不会实例化MyCat对象,即没有对MyCat进行主动使用,这里就不会加载MyCat cLass
        //Object o = clazz.newInstance();

    }
}

public class MyTest18 {

    public static void main(String[] args) {

        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(System.getProperty("java.class.path"));
    }
}

public class MyTest18_1 {

    public static void main(String[] args) throws Exception {
        //Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/jre/classes
        //如果我们把自己的class放置到上面的根类加载器加载class的目录下的话, 那么会打印出class loader为null
        //也就是说我们的class直接由根类加载器进行加载,不会再由系统加载器加载
        //由这个例子我们就可以看出,当前类加载器会先委托父类加载器去加载,
        // 如果父类加载器已经加载了,那么当前类加载器是不会再去加载的
        MyTest16 loader1 = new MyTest16("loader1");
        loader1.setPath("/Users/luozhiyun/Downloads/test/");
        Class<?> clazz = loader1.loadClass("jvm.classloader.Mytest1");

        System.out.println("class:" + clazz.hashCode());
        System.out.println("class loader:" + clazz.getClassLoader());
    }
}

public class MyTest19 {
    public static void main(String[] args) {
        /**
         * 使用java -Djava.ext.dirs=./ jvm.classloader.MyTest19
         * 会打印出找不到AESKeyGenerator这个类,因为这个类不在当前目录下
         */
        AESKeyGenerator aesKeyGenerator = new AESKeyGenerator();
        System.out.println(aesKeyGenerator.getClass().getClassLoader());
        System.out.println(MyTest19.class.getClassLoader());

    }
}

public class MyTest20 {

    public static void main(String[] args) throws Exception {
        MyTest16 loader1 = new MyTest16("loader1");
        MyTest16 loader2 = new MyTest16("loader2");

        //loader1.setPath("/Users/luozhiyun/Downloads/test/");
        Class<?> clazz1 = loader1.loadClass("jvm.classloader.MyPerson");
        Class<?> clazz2 = loader2.loadClass("jvm.classloader.MyPerson");
        //true
        System.out.println(clazz1 == clazz2);

        Object o1 = clazz1.newInstance();
        Object o2 = clazz2.newInstance();

        Method method = clazz1.getMethod("setMyPerson", Object.class);
        method.invoke(o1, o2);
    }
}

/**
 * 类加载器的双亲委托模型的好处:
 * 1. 可以确保java核心库的类型安全:
 * 所有的java应用至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类
 * 会被加载到java虚拟机中;如果这个加载过程是由java应用自己的类加载器所完成的,那么很可能就会在
 * jvm中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容,相互不可见的(正是命名空间在发挥着作用).
 * 借助于双亲委托机制,java核心类库中的类的加载工作都是由启动类加载器来统一完成加载工作,从而确保了java应用所使用
 * 的都是同一版本的java核心类库,他们之间是相互兼容的.
 *
 * 2.可以确保java核心类库所提供的类不会被自定义的类所替代
 * 3.不同的类加载器可以被相同名称(binary name)的类创建额外的命名空间,相同名称的类可以并存在java虚拟机中,只需要用
 * 不同的加载器来加载他们即可,不同类加载器所加载的类之间是不兼容的,这相当于在java虚拟机内部创建了一个又一个相互隔离
 * 的java类空间,这类技术在很多框架中都得到了实际应用
 */

public class MyTest21 {

    public static void main(String[] args) throws Exception {
        MyTest16 loader1 = new MyTest16("loader1");
        MyTest16 loader2 = new MyTest16("loader2");

        loader1.setPath("/Users/luozhiyun/Downloads/test/");
        loader2.setPath("/Users/luozhiyun/Downloads/test/");
        Class<?> clazz1 = loader1.loadClass("jvm.classloader.MyPerson");
        Class<?> clazz2 = loader2.loadClass("jvm.classloader.MyPerson");
        //false
        //因为loader1和loader2两者是没有任何关系的,并且他们都是各自都加载了一次MyPerson
        //所以两个类加载器加载的类其实是两个class
        //如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见
        System.out.println(clazz1 == clazz2);

        Object o1 = clazz1.newInstance();
        Object o2 = clazz2.newInstance();

        /**
         * Exception in thread "main" java.lang.reflect.InvocationTargetException
         * 	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
         * 	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
         * 	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
         * 	at java.lang.reflect.Method.invoke(Method.java:498)
         * 	at jvm.classloader.MyTest21.main(MyTest21.java:25)
         * Caused by: java.lang.ClassCastException: jvm.classloader.MyPerson cannot be cast to jvm.classloader.MyPerson
         * 	at jvm.classloader.MyPerson.setMyPerson(MyPerson.java:11)
         * 	... 5 more
         * 	这里会抛出一个这样的异常,因为两个类实际上已经不是同一个类了,所以不能转换
         */
        Method method = clazz1.getMethod("setMyPerson", Object.class);
        method.invoke(o1, o2);
    }
}

类加载器的双亲委托模型的好处:

  1. 可以确保java核心库的类型安全: 所有的java应用至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到java虚拟机中;如果这个加载过程是由java应用自己的类加载器所完成的,那么很可能就会在
    Jvm中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容,相互不可见的(正是命名空间在发挥着作用).
    借助于双亲委托机制,java核心类库中的类的加载工作都是由启动类加载器来统一完成加载工作,从而确保了java应用所使用的都是同一版本的java核心类库,他们之间是相互兼容的.
  2. 可以确保java核心类库所提供的类不会被自定义的类所替代
  3. 不同的类加载器可以被相同名称(binary name)的类创建额外的命名空间,相同名称的类可以并存在java虚拟机中,只需要用不同的加载器来加载他们即可,不同类加载器所加载的类之间是不兼容的,这相当于在java虚拟机内部创建了一个又一个相互隔离的java类空间,这类技术在很多框架中都得到了实际应用

在运行期,一个java类是由该类的完全限定名(binary name,二进制名)和用于加载该类的定义类加载器(defining loader)所共同决定的.如果同样名字(即相同的完全限定名)的类是由两个不同的类加载器所加载,那么这些类就是不同的,即便.class文件的字节码完全一样,并且从相同的位置加载亦如此.

在Oracle的Hotspot实现中,系统属性sun.boot.class.path如果修改错了,则运行会出错,提示如下错误信息:
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object

/*
在运行期,一个java类是由该类的完全限定名(binary name,二进制名)和用于加载该类的定义类加载器(defining loader)所共同决定的.
如果同样名字(即相同的完全限定名)的类是由两个不同的类加载器所加载,那么这些类就是不同的,
即便.class文件的字节码完全一样,并且从相同的位置加载亦如此.
 */

/*
java -Dsun.boot.class.path=./ jvm.classloader.MyTest23

在Oracle的Hotspot实现中,系统属性sun.boot.class.path如果修改错了,则运行会出错,提示如下错误信息:
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object

 */
public class MyTest23 {
    public static void main(String[] args) {

        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(System.getProperty("java.class.path"));

        //null 也就是说由启动类加载器负责加载
        System.out.println(ClassLoader.class.getClassLoader());
        //扩展类加载器与系统类加载器也是由启动类加载器所加载的
        System.out.println(Launcher.class.getClassLoader());

        System.out.println("----");

        //java -Djava.system.class.loader=jvm.classloader.MyTest16  jvm.classloader.MyTest23
        //jvm.classloader.MyTest16
        //sun.misc.Launcher$AppClassLoader@18b4aac2
        //sun.misc.Launcher$AppClassLoader@18b4aac2
        //jvm.classloader.MyTest16@4e25154f
        //如果加了上面的那个属性就会这样打印,因为我们把默认的加载器改成了自定义的加载器
        System.out.println(System.getProperty("java.system.class.loader"));
        System.out.println(MyTest23.class.getClassLoader());
        System.out.println(MyTest16.class.getClassLoader());
        System.out.println(ClassLoader.getSystemClassLoader());

    }
}

内建于jvm中的启动类加载器会加载java.lang.ClassLoader以及其他的java平台类.
当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器(Bootstrap).
启动类加载器并不是java类,而其他的加载器则都是java类
启动类加载器是特定于平台的机器指令,它负责开启整个加载过程,所有类加载器(除了启动类加载器)都被实现为java类.不过,总归要有一个组件来加载第一个java类加载器,从而让整个加载过程能够顺利进行下去,加载第一个纯java类加载器就是启动类加载器的指责.
启动类加载器还会负责加载供jre正常运行所需要的基本组件,这包括java.util与java.lang包中的类等等.

当前类加载器(Current ClassLoader)

每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指的是所依赖的类)
如果ClassX引用了ClassY,那么ClasX的类加载器就回去加载ClasY(前提是ClassY尚未被加载)

线程上下文类加载器(Context ClassLoader)

线程上下文类加载器是从JDK1.2开始引入的,类Thread中的与getConTgextClassLoader()与setConTextClassLoader(ClassLoader)
分别用来获取和设置上下文加载器.
如果没有通过setConTextClassLoader进行设值的话,线程将继承其父线程的上下文类加载器.
Java应用运行时的初始线程的上下文类加载器是系统类加载器.在线程中运行的代码可以通过该类加载器来加载类与资源

线程上下文类加载器的重要性:

父classLoader可以使用当前线程Thread.currentThread().getContextLoader()所指定的classLoader加载的类这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader加载类的情况,即改变了双亲委托模型.

线程上下文类加载器就是当前线程的Current ClassLoader.
在双亲委托模型下,类加载时由上至下的,即下层的类加载器会委托上层进行加载.但是对SPI来说,有些接口是java核心库提供的.而java核心库是由启动类加载器来加载的,而这些接口的实现却来自不同的jar包(厂商提供),java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求.而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对接口实现类的加载

线程上下文类加载器的一般使用模式(获取 - 使用 - 还原)
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
将想要使用的类加载器设置进去
Thread.currentThread().setContextClassLoader(targetTccl);
然后在自己写的方法使用类加载器
myMethod();
}finally {
最后把当前线程的类加载器还原
Thread.currentThread().setContextClassLoader(classLoader);
}

如果一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载的(如果该依赖类之前没有被加载过的话)
ContextClassLoader的作用就输出为了破坏java的类加载器委托机制.

当高层提供了同一的借口让底层去实现,同时又要在高层加载(或实例化)底层的类时,就必须要通过线程上下文类加载器来帮助高层的classLoader找到并加载该类

java学习笔记/jvm#

posted @ 2019-03-10 18:04  luozhiyun  阅读(407)  评论(0编辑  收藏  举报