1. JVM三种类加载器路径获取
可以通过System.getProperty() 方法获取类加载器加载类的路径:
System.getProperty("sun.boot.class.path") //表示根类加载器加载路径
System.getProperty("java.ext.dirs") //表示扩展类加载器加载路径
System.getProperty("java.class.path") //表示应用类加载器加载路径
一般地,我们编写自定义的类,都是通过应用类加载器加载的。这是由类加载器的双亲委托机制决定的。
如果让根类加载器或扩展类加载器我们自定义的类,该如何实现呢? 看第二小节
2. 改变类的加载器
先来个小例子说明问题:
public class MyTest19 {
public static void main(String[] args) throws ClassNotFoundException {
//根类加载器加载类的路径
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"));
MyClassLoader classLoader = new MyClassLoader("D:\\study\\study_code\\jvm\\dcj\\out\\production\\classes");
Class<?> clazz = classLoader.loadClass("classloader3.MySample");
System.out.println("clazz classLoader is :" + clazz.getClassLoader());
}
}
运行结果:
D:\study\study_software\java\jdk\jre\lib\resources.jar;D:\study\study_software\java\jdk\jre\lib\rt.jar;D:\study\study_software\java\jdk\jre\lib\sunrsasign.jar;D:\study\study_software\java\jdk\jre\lib\jsse.jar;D:\study\study_software\java\jdk\jre\lib\jce.jar;D:\study\study_software\java\jdk\jre\lib\charsets.jar;D:\study\study_software\java\jdk\jre\lib\jfr.jar;D:\study\study_software\java\jdk\jre\classes
D:\study\study_software\java\jdk\jre\lib\ext;C:\windows\Sun\Java\lib\ext
D:\study\study_software\java\jdk\jre\lib\charsets.jar;D:\study\study_software\java\jdk\jre\lib\deploy.jar;D:\study\study_software\java\jdk\jre\lib\ext\access-bridge-64.jar;D:\study\study_software\java\jdk\jre\lib\ext\cldrdata.jar;D:\study\study_software\java\jdk\jre\lib\ext\dnsns.jar;D:\study\study_software\java\jdk\jre\lib\ext\jaccess.jar;D:\study\study_software\java\jdk\jre\lib\ext\jfxrt.jar;D:\study\study_software\java\jdk\jre\lib\ext\localedata.jar;D:\study\study_software\java\jdk\jre\lib\ext\nashorn.jar;D:\study\study_software\java\jdk\jre\lib\ext\sunec.jar;D:\study\study_software\java\jdk\jre\lib\ext\sunjce_provider.jar;D:\study\study_software\java\jdk\jre\lib\ext\sunmscapi.jar;D:\study\study_software\java\jdk\jre\lib\ext\sunpkcs11.jar;D:\study\study_software\java\jdk\jre\lib\ext\zipfs.jar;D:\study\study_software\java\jdk\jre\lib\javaws.jar;D:\study\study_software\java\jdk\jre\lib\jce.jar;D:\study\study_software\java\jdk\jre\lib\jfr.jar;D:\study\study_software\java\jdk\jre\lib\jfxswt.jar;D:\study\study_software\java\jdk\jre\lib\jsse.jar;D:\study\study_software\java\jdk\jre\lib\management-agent.jar;D:\study\study_software\java\jdk\jre\lib\plugin.jar;D:\study\study_software\java\jdk\jre\lib\resources.jar;D:\study\study_software\java\jdk\jre\lib\rt.jar;D:\study\study_code\jvm\dcj\out\production\classes;D:\Program Files\JetBrains\IntelliJ IDEA 2018.1.3\lib\idea_rt.jar
clazz classLoader is :sun.misc.Launcher$AppClassLoader@18b4aac2
前三行打印了三种不同的类加载器加载类的路径。第四行表明MySample类是由应用类加载器加载的。
观察第一行输出的最后,D:\study\study_software\java\jdk\jre\classes
, 表明根类加载器会去该路径加载类,该路径其实就是$JAVA_HOME/jre/classes, 把 MyTest19 和 MySample 类 拷贝至 该目录下, 再次运行程序, 输出结果如下:
clazz classLoader is :null
表明MySample类由根类加载器加载了。
注意: 扩展类加载器不能直接去加载class文件,需要打成一个jar包,操作命令如下:
#将class文件打成一个jar包,跟tar命令非常类似
jar cvf test.jar classloader3/MyTest20.class
#动态指定java.ext.dirs路径
java -Djava.ext.dirs=./ classloader3.MyTest21
这样,在运行MyTest21类的时候,扩展类加载器会去指定的路径寻找, MyTest20类就会被sun.misc.Launcher$ExtClassLoader类加载器所加载。
3. 类的命名空间
- 每个类加载器都有自己的命名空间,命名空间是由
该类加载器及其所有的父类加载器所加载的类组成
。 - 在同一个命名空间里,不会出现类的完整名字相同的两个类(class对象)。
- 在不同的命令空间里,有可能出现类的完整名字相同的两个类(class对象)。
同一个命名空间的类是相互可见的。子加载器的命名空间包含所有父加载器的命名空间。系统类加载器加载的类可以看见根类加载器加载的类。
public class MyTest20 {
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("C:\\Users\\Administrator\\Desktop\\classes\\");
MyClassLoader loader2 = new MyClassLoader("C:\\Users\\Administrator\\Desktop\\classes\\");
Class<?> clazz1 = loader1.loadClass("classloader3.MyCat");
Class<?> class2 = loader2.loadClass("classloader3.MyCat");
System.out.println(clazz1 == class2);
Object myCat = clazz1.newInstance();
Object myCat2 = class2.newInstance();
//这行可能抛异常
Method method = clazz1.getMethod("setCat", Object.class);
method.invoke(myCat, myCat2);
}
}
当把 MyCat类class 文件放入对应位置, 并且将IDEA 工程下的 MyCat类class 文件删除后,打印结果为false。 因为 MyCat类由两个不同的类加载器加载了两次,他们位于两个不同的命名空间, 相互不可见。最后反射调用时,会出现一个异常:
Caused by: java.lang.ClassCastException: classloader3.MyCat cannot be cast to classloader3.MyCat
看着有点奇怪吧,本质上就是因为两个类在不同的命名空间,相互不可见。
总结:在运行期,一个java类是由该类的完全限定名(binary name)和 用于加载该类的定义类加载器(defining loader)共同决定的。如果同样名字的类由两个不同的加载器加载,那么这些类就是不同的, 尽管.class文件的字节码完全相同。
4 . 类加载器双亲委托模型的的好处
- 确保java核心类的类型安全:所有的java应用至少都会引用java.lang.Object类,在运行期,java.lang.Object类会被加载到JVM中,如果这个加载过程由自定义的类加载器完成,可能存在多个版本的Object类,这些类是不兼容的,相互不可见(由命名空间导致)。借助双亲委托机制,java核心类库的类加载工作由启动类加载器完成,他们之间是相互兼容的。
- 可以确保java核心类库所提供的类不会被自定义的类所替代。
- 不同的类加载器可以为相同名称(binaryname)的类创建不同的命名空间。相同名称的类可以并存在java虚拟机中,只需要不同的java类加载器来加载他们即可。不同的类加载器所加载的类是不兼容的。
5 . 总结
- 技术上,我们可以让根类加载器或者扩展类加载器加载我们自定义的类,需要将class文件放入对应的加载路径下(扩展类加载器需要加载jar包)。因此,main方法所在的类我们也可以做到让启动类加载器去加载。
- 同一个命名空间的类是相互可见的。子加载器的命名空间包含所有父加载器的命名空间。系统类加载器加载的类可以看见根类加载器加载的类。