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:\F\work\JDK\JDK1.8\jre\lib\resources.jar;D:\F\work\JDK\JDK1.8\jre\lib\rt.jar;D:\F\work\JDK\JDK1.8\jre\lib\sunrsasign.jar;D:\F\work\JDK\JDK1.8\jre\lib\jsse.jar;D:\F\work\JDK\JDK1.8\jre\lib\jce.jar;D:\F\work\JDK\JDK1.8\jre\lib\charsets.jar;D:\F\work\JDK\JDK1.8\jre\lib\jfr.jar;D:\F\work\JDK\JDK1.8\jre\classes D:\F\work\JDK\JDK1.8\jre\lib\ext;C:\Windows\Sun\Java\lib\ext D:\F\work\JDK\JDK1.8\jre\lib\charsets.jar;D:\F\work\JDK\JDK1.8\jre\lib\deploy.jar;D:\F\work\JDK\JDK1.8\jre\lib\ext\access-bridge-64.jar;D:\F\work\JDK\JDK1.8\jre\lib\ext\cldrdata.jar;D:\F\work\JDK\JDK1.8\jre\lib\ext\dnsns.jar;D:\F\work\JDK\JDK1.8\jre\lib\ext\jaccess.jar;D:\F\work\JDK\JDK1.8\jre\lib\ext\jfxrt.jar;D:\F\work\JDK\JDK1.8\jre\lib\ext\localedata.jar;D:\F\work\JDK\JDK1.8\jre\lib\ext\nashorn.jar;D:\F\work\JDK\JDK1.8\jre\lib\ext\sunec.jar;D:\F\work\JDK\JDK1.8\jre\lib\ext\sunjce_provider.jar;D:\F\work\JDK\JDK1.8\jre\lib\ext\sunmscapi.jar;D:\F\work\JDK\JDK1.8\jre\lib\ext\sunpkcs11.jar;D:\F\work\JDK\JDK1.8\jre\lib\ext\zipfs.jar;D:\F\work\JDK\JDK1.8\jre\lib\javaws.jar;D:\F\work\JDK\JDK1.8\jre\lib\jce.jar;D:\F\work\JDK\JDK1.8\jre\lib\jfr.jar;D:\F\work\JDK\JDK1.8\jre\lib\jfxswt.jar;D:\F\work\JDK\JDK1.8\jre\lib\jsse.jar;D:\F\work\JDK\JDK1.8\jre\lib\management-agent.jar;D:\F\work\JDK\JDK1.8\jre\lib\plugin.jar;D:\F\work\JDK\JDK1.8\jre\lib\resources.jar;D:\F\work\JDK\JDK1.8\jre\lib\rt.jar;D:\F\work\java_space\jvm-lecture\target\classes;D:\F\work\java\maven_repository\mysql\mysql-connector-java\8.0.20\mysql-connector-java-8.0.20.jar;D:\F\work\java\maven_repository\com\google\protobuf\protobuf-java\3.6.1\protobuf-java-3.6.1.jar;D:\D\Program Files\IntelliJ IDEA 2020.1\lib\idea_rt.jar
上面的程序我们可以看到,根加载器加载类的路径其中有一个是:D:\F\work\JDK\JDK1.8\jre\classes,默认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:\F\java_space\jvm-lecture\target\classes>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:\F\work\java_space\jvm-lecture\target\classes>jar cvf test.jar com/leolin/jvm/MyTest1.class 已添加清单 正在添加: com/leolin/jvm/MyTest1.class(输入 = 605) (输出 = 359)(压缩了 40%) D:\F\work\java_space\jvm-lecture\target\classes>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:\F\java_space\jvm-lecture\target\classes>java com.leolin.jvm.MyTest23 D:\F\JDK\JRE1.8\lib\resources.jar;D:\F\JDK\JRE1.8\lib\rt.jar;D:\F\JDK\JRE1.8\lib\sunrsasign.jar;D:\F\JDK\JRE1.8\lib\jsse.jar;D:\F\JDK\JRE1.8\lib\jce.jar;D:\F\JDK\JRE1.8\lib\charset s.jar;D:\F\JDK\JRE1.8\lib\jfr.jar;D:\F\JDK\JRE1.8\classes D:\F\JDK\JRE1.8\lib\ext;C:\WINDOWS\Sun\Java\lib\ext .;D:\F\JDK\JDK1.8\lib;D:\F\JDK\JDK1.8\lib\tools.jar
可以看到应用类加载器有打印出一个.,而且应用类加载器所加载的路径和之前直接在idea里运行有很大的变化,这是因为由idea执行,idea会为我们的应用类加载器路径添加一些额外的路径,而我们在命令行执行的话,只打印系统所设定的变量。
如果我们修改了根类加载器所加载的路径,可以看到,程序启动时会报找不到Object类的错误。
D:\F\java_space\jvm-lecture\target\classes>java -Dsun.boot.class.path=./ com.leolin.jvm.MyTest23 Error occurred during initialization of VM java/lang/NoClassDefFoundError: java/lang/Object