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

  

 

posted @ 2020-05-06 08:51  北洛  阅读(188)  评论(0编辑  收藏  举报