• JVM类加载器(三)


    日常开发中,我们会在类中的方法引用其他的类,如果A类的方法引用了B类,那么加载器在加载A类时,所引用的类或者对象是怎么加载呢?

    这里我们预先生成两个类MyCat和MySample:

    package com.leolin.jvm;
    
    public class MyCat {
        public MyCat() {
            System.out.println("MyCat is loaded by:" + this.getClass().getClassLoader());
        }
    }
    

      

    package com.leolin.jvm;
    
    public class MySample {
        public MySample() {
            System.out.println("MySample is loaded by:" + this.getClass().getClassLoader());
            new MyCat();
        }
    }
    

      

    编译上面的两个Java文件,在classpath下生成class文件后,我们编写MyTest17,加载MySample:

    package com.leolin.jvm;
    
    public class MyTest17 {
        public static void main(String[] args) throws Exception {
            MyTest16 loader1 = new MyTest16("loader1");
            Class<?> clazz = loader1.loadClass("com.leolin.jvm.MySample");
            System.out.println("class:" + clazz.hashCode());
        }
    }
    

      

    配置-XX:+TraceClassLoading后运行程序:

    ……
    [Loaded com.leolin.jvm.MyTest17 from file:/D:/F/work/java_space/jvm-lecture/target/classes/]
    ……
    [Loaded com.leolin.jvm.MyTest16 from file:/D:/F/work/java_space/jvm-lecture/target/classes/]
    [Loaded com.leolin.jvm.MySample from file:/D:/F/work/java_space/jvm-lecture/target/classes/]
    class:734659227
    ……
    

      

    可以看到,程序在加载MySample后,并没有加载MyCat。

    现在,将classpath先复制到桌面,并将classpath下的MyCat和MySample的class文件删除。然后我们执行下面的程序:

    package com.leolin.jvm;
    
    public class MyTest17_1 {
        public static void main(String[] args) throws Exception {
            MyTest16 loader1 = new MyTest16("loader1");
            loader1.setPath("C:\Users\admin\Desktop\");
            Class<?> clazz = loader1.loadClass("com.leolin.jvm.MySample");
            System.out.println("class:" + clazz.hashCode());
            Object object = clazz.newInstance();
        }
    }
    

      

    执行结果:

    findClass invoked:com.leolin.jvm.MySample
    class loader name:loader1
    class:504590507
    MySample is loaded by:com.leolin.jvm.MyTest16@120f0be
    findClass invoked:com.leolin.jvm.MyCat
    class loader name:loader1
    MyCat is loaded by:com.leolin.jvm.MyTest16@120f0be
    from MySample:class com.leolin.jvm.MyCat
    

      

    这个结果很好理解,我们的classpath下没有MyCat和MySample,所以loader1就去桌面上加载。但是,现在我们重新编译我们的项目,在classpath下生成MyCat和MySample之后,我们再删除MyCat,然后运行程序,得到如下结果:

    class:1534141586
    MySample is loaded by:sun.misc.Launcher$AppClassLoader@7b7035c6
    Exception in thread "main" java.lang.NoClassDefFoundError: com/leolin/jvm/MyCat
    ……
    

      

    应用类加载器加载MySample后,创建一个实例,创建实例时需要创建MyCat的实例,要先去加载MyCat类,因为MySample 的类加载器是应用加载器,所以在构造方法中要创建MyCat的实例,也会由应用类加载器去加载,因为MyCat的class不在classpath下,所以报错。

    重新编译,删除MySample保留MyCat,运行MyTest17_1:

    findClass invoked:com.leolin.jvm.MySample
    class loader name:loader1
    class:2023612323
    MySample is loaded by:com.leolin.jvm.MyTest16@52a7b7ff
    MyCat is loaded by:sun.misc.Launcher$AppClassLoader@7b7035c6
    from MySample:class com.leolin.jvm.MyCat
    

      

    这次程序并没有报错,之所以没报错,是因为MySample虽然是由MyTest16加载,但加载MyCat 的时候,MyTest16的父加载器可以加载到MyCat。

    MyCat的构造方法如下:

    public MyCat() {
    	System.out.println("MyCat is loaded by:" + this.getClass().getClassLoader());
    	System.out.println("from MyCat:" + MySample.class);
    }
    

      

    重新编译后删除MySample,然后运行程序,有如下结果:

    findClass invoked:com.leolin.jvm.MySample
    class loader name:loader1
    class:1286066966
    MySample is loaded by:com.leolin.jvm.MyTest16@52a7b7ff
    MyCat is loaded by:sun.misc.Launcher$AppClassLoader@7b7035c6
    Exception in thread "main" java.lang.NoClassDefFoundError: com/leolin/jvm/MySample
    ……
    

      

    从输出上来看,应该是在MyCat构造方法中,第二行打印报错。因为MySample是自定义的加载器加载的,MyCat是应用加载器加载的,这两个类分别由不同的加载器加载,而加载MyCat的加载器,无法访问到它的子类所加载的MySample,所以报错,这里就涉及到之前所说的命名空间。

    我们分别将MySample和MyCat的构造方法改成如下:

    public MySample() {
    	System.out.println("MySample is loaded by:" + this.getClass().getClassLoader());
    	new MyCat();
    	System.out.println("from MySample:" + MyCat.class);
    }
    
    public MyCat() {
    	System.out.println("MyCat is loaded by:" + this.getClass().getClassLoader());
    }
    

      

    重新编译后删除MySample,执行程序得到如下结果:

    class:1534141586
    MySample is loaded by:sun.misc.Launcher$AppClassLoader@3da997a
    MyCat is loaded by:sun.misc.Launcher$AppClassLoader@3da997a
    from MySample:class com.leolin.jvm.MyCat
    

      

    MySample访问MyCat不报错,是因为子加载器访问父加载器加载的类。

    在Java中,根加载器、扩展类加载器和应用类加载器所加载的路径都是由Java的系统属性所设定的,分别是:

    • sun.boot.class.path
    • java.ext.dirs
    • java.class.path

    下面我们用Java来打印一下根加载器、扩展类加载器和应用类加载器所加载的路径:

    package com.leolin.jvm;
    
    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"));
            //应用类加载器路径,idea会自动将工程下面的类路径加载到应用类所要加载的路径下
            System.out.println(System.getProperty("java.class.path"));
        }
    }
    

      

    运行结果:

    D:FworkJDKJDK1.8jrelib
    esources.jar;D:FworkJDKJDK1.8jrelib
    t.jar;D:FworkJDKJDK1.8jrelibsunrsasign.jar;D:FworkJDKJDK1.8jrelibjsse.jar;D:FworkJDKJDK1.8jrelibjce.jar;D:FworkJDKJDK1.8jrelibcharsets.jar;D:FworkJDKJDK1.8jrelibjfr.jar;D:FworkJDKJDK1.8jreclasses
    D:FworkJDKJDK1.8jrelibext;C:WindowsSunJavalibext
    D:FworkJDKJDK1.8jrelibcharsets.jar;D:FworkJDKJDK1.8jrelibdeploy.jar;D:FworkJDKJDK1.8jrelibextaccess-bridge-64.jar;D:FworkJDKJDK1.8jrelibextcldrdata.jar;D:FworkJDKJDK1.8jrelibextdnsns.jar;D:FworkJDKJDK1.8jrelibextjaccess.jar;D:FworkJDKJDK1.8jrelibextjfxrt.jar;D:FworkJDKJDK1.8jrelibextlocaledata.jar;D:FworkJDKJDK1.8jrelibext
    ashorn.jar;D:FworkJDKJDK1.8jrelibextsunec.jar;D:FworkJDKJDK1.8jrelibextsunjce_provider.jar;D:FworkJDKJDK1.8jrelibextsunmscapi.jar;D:FworkJDKJDK1.8jrelibextsunpkcs11.jar;D:FworkJDKJDK1.8jrelibextzipfs.jar;D:FworkJDKJDK1.8jrelibjavaws.jar;D:FworkJDKJDK1.8jrelibjce.jar;D:FworkJDKJDK1.8jrelibjfr.jar;D:FworkJDKJDK1.8jrelibjfxswt.jar;D:FworkJDKJDK1.8jrelibjsse.jar;D:FworkJDKJDK1.8jrelibmanagement-agent.jar;D:FworkJDKJDK1.8jrelibplugin.jar;D:FworkJDKJDK1.8jrelib
    esources.jar;D:FworkJDKJDK1.8jrelib
    t.jar;D:Fworkjava_spacejvm-lecture	argetclasses;D:Fworkjavamaven_repositorymysqlmysql-connector-java8.0.20mysql-connector-java-8.0.20.jar;D:Fworkjavamaven_repositorycomgoogleprotobufprotobuf-java3.6.1protobuf-java-3.6.1.jar;D:DProgram FilesIntelliJ IDEA 2020.1libidea_rt.jar
    

    上面的程序我们可以看到,根加载器加载类的路径其中有一个是:D:FworkJDKJDK1.8jreclasses,默认jre下是没有classes这个目录的,不过我们可以新建这个目录,并把我们工程下的classpath拷贝到classes目录下,然后我们尝试加载MyTest1,看看MyTest1的加载器还会不会是应用类加载器。

    package com.leolin.jvm;
    
    public class MyTest18_1 {
        public static void main(String[] args) throws Exception {
            MyTest16 loader1 = new MyTest16("loader1");
            loader1.setPath("C:\Users\admin\Desktop\");
            Class<?> clazz = loader1.loadClass("com.leolin.jvm.MyTest1");
            System.out.println("class:" + clazz.hashCode());
            System.out.println("class loader:" + clazz.getClassLoader());
        }
    }
    

      

    运行代码,得到如下结果:

    class:21685669
    class loader:null
    

      

    这里,我们成功用根加载器加载了我们所编写的应用类MyTest1。测试成功后,我们就可以删除classes目录,回到原样。

    接下来,我们是否可以尝试用扩展类加载器来加载我们所编写的应用类呢?我们编写如下代码:

    package com.leolin.jvm;
    
    import com.sun.crypto.provider.AESKeyGenerator;
    
    public class MyTest19 {
        public static void main(String[] args) {
            AESKeyGenerator aesKeyGenerator = new AESKeyGenerator();
            System.out.println(aesKeyGenerator.getClass().getClassLoader());
            System.out.println(MyTest19.class.getClassLoader());
        }
    }
    

      

    运行上面的程序,得到如下输出:

    sun.misc.Launcher$ExtClassLoader@6e0be858
    sun.misc.Launcher$AppClassLoader@18b4aac2
    

      

    可以看到AESKeyGenerator这个类时由扩展类加载器去加载的,而MyTest19依旧是由应用类加载器加载,这里没什么好分析的。但现在我们能否尝试着将

    编译上面的代码,我们到工程的类路径下修改扩展类路径java.ext.dirs,将其路径改为当前路径./,并运行MyTest19:

    java -Djava.ext.dirs=./ com.leolin.jvm.MyTest19
    Exception in thread "main" java.lang.NoClassDefFoundError: com/sun/crypto/provider/AESKeyGenerator
            at com.leolin.jvm.MyTest19.main(MyTest19.java:7)
    Caused by: java.lang.ClassNotFoundException: com.sun.crypto.provider.AESKeyGenerator
    ……

      

    可以看到,我们的程序出错了,因为我们修改扩展类加载器加载的路径后,程序找不到AESKeyGenerator这个类,所以报NoClassDefFoundError的错误。

    我们再来看另一个例子,我们先编写一个MyPerson类:

    package com.leolin.jvm;
    
    public class MyPerson {
        private MyPerson myPerson;
    
        public void setMyPerson(Object object) {
            this.myPerson = (MyPerson) object;
        }
    }
    

      

    然后,我们声明两个类加载器loader1和loader2,这两个加载器分别加载MyPerson。显然,根据我们之前的知识,这两个类加载器会委托它们的父加载器应用类加载器去加载MyPerson,所以clazz1等于clazz2,因为都是引用同一个对象。之后,我们利用Java的反射,调用object1的setMyPerson方法,将object2传入。

    package com.leolin.jvm;
    
    import java.lang.reflect.Method;
    
    public class MyTest20 {
        public static void main(String[] args) throws Exception {
            MyTest16 loader1 = new MyTest16("loader1");
            MyTest16 loader2 = new MyTest16("loader2");
            Class<?> clazz1 = loader1.loadClass("com.leolin.jvm.MyPerson");
            Class<?> clazz2 = loader2.loadClass("com.leolin.jvm.MyPerson");
            System.out.println(clazz1 == clazz2);
            Object object1 = clazz1.newInstance();
            Object object2 = clazz2.newInstance();
            Method method = clazz1.getMethod("setMyPerson", Object.class);
            method.invoke(object1, object2);
        }
    }
    

      

    在理解了上面的代码之后,我们来看另外一个例子,我们将工程下的classpath拷贝到桌面,然后删除classpath下的MyPerson,转为让类加载器到桌面上加载MyPerson:

    package com.leolin.jvm;
    
    import java.lang.reflect.Method;
    
    public class MyTest21 {
        public static void main(String[] args) throws Exception {
            MyTest16 loader1 = new MyTest16("loader1");
            MyTest16 loader2 = new MyTest16("loader2");
            loader1.setPath("C:\Users\admin\Desktop\");
            loader2.setPath("C:\Users\admin\Desktop\");
            //loader1是clazz1的定义类加载器,也是初始类加载器,loader1和loader2是独立的两个命名空间,加载的类相互不可见
            Class<?> clazz1 = loader1.loadClass("com.leolin.jvm.MyPerson");
            Class<?> clazz2 = loader2.loadClass("com.leolin.jvm.MyPerson");
            System.out.println(clazz1 == clazz2);
            Object object1 = clazz1.newInstance();
            Object object2 = clazz2.newInstance();
            Method method = clazz1.getMethod("setMyPerson", Object.class);
            method.invoke(object1, object2);
        }
    }
    

      

    运行上面的代码,得到如下输出:

    findClass invoked:com.leolin.jvm.MyPerson
    class loader name:loader1
    findClass invoked:com.leolin.jvm.MyPerson
    class loader name:loader2
    false
    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 com.leolin.jvm.MyTest21.main(MyTest21.java:18)
    Caused by: java.lang.ClassCastException: com.leolin.jvm.MyPerson cannot be cast to com.leolin.jvm.MyPerson
    	at com.leolin.jvm.MyPerson.setMyPerson(MyPerson.java:7)
    	... 5 more
    

      

    可以看到,现在clazz1和clazz2不相等了,即便这两个类加载器加载的是同一个类。而且后面用反射调用object1的setMyPerson,将object2传入,也报错了,报错信息也非常有意思,类型转换错误:com.leolin.jvm.MyPerson无法被转换为com.leolin.jvm.MyPerson。

    之所以有这样的错误,是因为loader1和loader2在加载MyPerson的时候,不再委托给负加载器,而是通过自身来加载,使得同样一个类型,分别处于loader1和loader2两个独立的命名空间,使得这两个类互相不可见,而这两个类生成的对象,也互相不可见,在Java看来,就是两个类型不同的对象。

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

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

    我们来看下面这段代码,一般情况下,MyTest22和MyTest1都会由应用类加载器加载:

    package com.leolin.jvm;
    
    public class MyTest22 {
        static {
            System.out.println("MyTest22 init");
        }
    
        public static void main(String[] args) {
            System.out.println(MyTest22.class.getClassLoader());
            System.out.println(MyTest1.class.getClassLoader());
        }
    }
    

      

    先前的MyTest19中,因为有AESKeyGenerator这个类,所以我们修改扩展类加载器为当前工程目录下的classpath,导致AESKeyGenerator加载失败。现在,我们尝试将扩展类加载器的路径修改为当前工程下的classpath,然后执行MyTest22:

    D:Fjava_spacejvm-lecture	argetclasses>java -Djava.ext.dirs=./ com.leolin.jvm.MyTest22
    MyTest22 init
    sun.misc.Launcher$AppClassLoader@73d16e93
    sun.misc.Launcher$AppClassLoader@73d16e93
    

      

    可以看到,即便我们将扩展类加载器的路径指到当前classpath下,但MyTest1和MyTest22依旧由应用类加载器加载。因为扩展类加载器加载类的时候,并不直接加载class文件,而是加载jar包。因此,这里我们再尝试将MyTest1打成一个jar包。

    D:Fworkjava_spacejvm-lecture	argetclasses>jar cvf test.jar com/leolin/jvm/MyTest1.class
    已添加清单
    正在添加: com/leolin/jvm/MyTest1.class(输入 = 605) (输出 = 359)(压缩了 40%)
    D:Fworkjava_spacejvm-lecture	argetclasses>java -Djava.ext.dirs=./ com.leolin.jvm.MyTest22
    MyTest22 init
    sun.misc.Launcher$AppClassLoader@2a139a55
    sun.misc.Launcher$ExtClassLoader@3d4eac69
    

      

    可以看到,当我们把MyTest1打成jar包之后,便由扩展加载器来加载了。

    我们来看下面的MyTest23:

    package com.leolin.jvm;
    
    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"));
        }
    }
    

      

    这段程序,我们现在不直接用idea运行,而是在命令行里执行:

    D:Fjava_spacejvm-lecture	argetclasses>java com.leolin.jvm.MyTest23
    D:FJDKJRE1.8lib
    esources.jar;D:FJDKJRE1.8lib
    t.jar;D:FJDKJRE1.8libsunrsasign.jar;D:FJDKJRE1.8libjsse.jar;D:FJDKJRE1.8libjce.jar;D:FJDKJRE1.8libcharset
    s.jar;D:FJDKJRE1.8libjfr.jar;D:FJDKJRE1.8classes
    D:FJDKJRE1.8libext;C:WINDOWSSunJavalibext
    .;D:FJDKJDK1.8lib;D:FJDKJDK1.8lib	ools.jar
    

      

    可以看到应用类加载器有打印出一个.,而且应用类加载器所加载的路径和之前直接在idea里运行有很大的变化,这是因为由idea执行,idea会为我们的应用类加载器路径添加一些额外的路径,而我们在命令行执行的话,只打印系统所设定的变量。

    如果我们修改了根类加载器所加载的路径,可以看到,程序启动时会报找不到Object类的错误。

    D:Fjava_spacejvm-lecture	argetclasses>java -Dsun.boot.class.path=./ com.leolin.jvm.MyTest23
    Error occurred during initialization of VM
    java/lang/NoClassDefFoundError: java/lang/Object
    

      

  • 相关阅读:
    ftpserver / FTP model:PORT/PASV/EPRT/EPSV
    windows 环境下搭建electricSearch+kibana
    springBoot2.x整合 logback实现日志记录
    springBoot使用aop添加处理rest请求 打印请求时间 和请求参数
    springCloud 使用feign复制请求头调用其他服务 content-length不一致导致调用失败
    mysql查询重复用户最新的一条数据
    【开源】 开源社区
    【数据库】 SQL 使用注意点
    【数据库】 SQL 常用语句
    【数据结构】 List 简单实现
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/12823428.html
Copyright © 2020-2023  润新知