类加载器与双亲委派
上一章讲了一下类的生命周期与加载过程,下面接着学习理解类的加载器与经常听到的双亲委派机制是个什么。
类加载器
jvm的类加载器,是通过类的全限定名(绝对路径)来查找需要被加载的类。
java中判断两个类是否“相等”,前提条件是这个两个类必须由同一个类加载器加载。如果两个类分别由不同的类加载加载,即时两个类源自同一个class文件,也一定不是“相等”的。
启动类加载器(Bootstrap Class Loader)
- 这个类是由C++实现,是虚拟机的一部分。主要是将<JAVA_HOME>/lib目录里的、或者-Xbootclasspath参数所指定路径中的,能够被java虚拟机识别的类库加载到虚拟机中。比如rt.jar等核心类库。
- 这里无论是<JAVA_HOME>/lib目录里的还是-Xbootclasspath参数所指定的,如果类库的名称不能够被java虚拟机识别,是无法被加载到虚拟机中的。
- Bootstrap加载了Extension加载器和Application加载器。
System.out.println(java.lang.Object.class.getClassLoader());
上面代理打印java.lang.Object是由哪个类加载器加载的。打印的结果是null,说明Object类是由Bootstrap加载器加载的。
扩展类加载器(Extension Class Loader)
- 加载扩展的jar包,是一种java系统类库的扩展机制,扩展java SE的功能。
- 主要是将<JAVA_HOME>/lib/ext/*.jar目录里的、或者-Djava.ext.dir系统变量所指定路径中的所有类库加载到虚拟机中。
System.out.println(DNSNameService.class.getClassLoader());
System.out.println("ExtClassLoader的类加载器: " + DNSNameService.class.getClassLoader().getClass().getClassLoader());
上面两行代码:
-
第一个打印
DNSNameService
的类加载器,打印结果:sun.misc.Launcher$ExtClassLoader@677327b6
,所以DNSNameService
的类加载器是Extension加载器。 -
第二个打印
DNSNameService
的类加载器的加载器是什么,打印结果:ExtClassLoader的类加载器: null
,可以得到,Extension类加载器是由Bootstrap类加载器加载的。
应用程序类加载器(Application Class Loader)
加载classpath指定路径的类库,如果应用程序中没有实现自定义类加载器(Custom Class Loader),那么一般情况下引用程序默认使用的就是Application类加载器。
然后代码:
public class TClassLoadDemo {
public static void main(String[] args) {
System.out.println(TClassLoadDemo.class.getClassLoader());
System.out.println("AppClassLoader的类加载器: " + TClassLoadDemo.class.getClassLoader().getClass().getClassLoader());
}
}
-
第一个打印
TClassLoadDemo
的类加载器,打印结果:sun.misc.Launcher$AppClassLoader@18b4aac2
,由Application加载器加载到jvm内存的。 -
第二个打印
TClassLoadDemo
的类加载器是由谁加载的,结果:AppClassLoader的类加载器: null
,所以,Application加载器是由Bootstrap类加载器加载的。
自定义类加载器(Custom Class Loader)
就是自己实现一个类加载器,加载自己指定路径下的class文件。
双亲委派机制
先看一个伪代码:
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
// 首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载时
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
-
一个类加载器收到去加载一个类的申请时候,首先判断自己是否已经加载过这个类,如果已加载过,直接返回,如果未被加载则调用父类的加载器。每一个层次的类加载器都是如此。
-
父类加载器反馈自己无法加载这个类的时候,子加载器才会尝试自己去加载这个类。每一个层次的类加载器都是如此。
这里我将自定义加载器也画进去了,流程跟结果是一样的,如果应用程序没有实现了自定义类加载器,那么Applicatoion类加载器就是最底层的那一个加载器
上图:
(1) 左边由下而上的箭头,表示子类收到一个加载类的请求,并去询问父类是否加载该类。
-
如果实现了Custom类加载器收到加载某类,判断自己是否已经加载了该类,如果加载了则返回该类,如果未加载则委托Application类加载器去加载。
-
Applicatoion类加载器收到了加载类的委托,先判断自己是否已经加载了该类,如果加载了则返回该类给Custom类加载器,如果没有加载则委托Extension类加载器去加载。
-
Extension类加载器收到了加载类的委托,先判断自己是否已经加载了该类,如果加载了则返回该类给Applicatoion类加载器,如果没有加载则委托Bootstrap类加载器去加载。
-
Bootstrap类加载器收到了加载该类的委托,先判断自己是否已经加载了该类,如果加载了则返回该类给Extension类加载器。如果没有加载,就是右边自上而下的箭头流程了。
(2) 右边自上而下的箭头,表示父类查找并判断该类是否由自己加载,如果自己可以加载则加载成功后返给子类,如果不由自己加载,则返回null表示自己无法加载该类,让子类自己加载。
-
Bootstrap类加载器首先判断该类是否在<JAVA_HOME>/lib目录里,或者-Xbootclasspath参数指定的路径之下,如果在,则自己加载该类。如果不在,则表示这个类不在Bootstrap加载器的加载范围内,则向子类Extension加载器返回null表示自己无法加载,让Extension自己加载。
-
Extension加载器收到Bootstrap返回null之后,自己尝试加载该类。首先判断是否在自己的加载范围之类(是否在<JAVA_HOME>/lib/ext/*.jar目录里、或者-Djava.ext.dir系统变量所指定路径中),如果是,则加载并返回给子类,如果不是在自己的加载范围内,则向Application加载器返回null表示自己无能为力,让Application自己加载。
-
Applicatoion类加载器也是同样的如此,先判断自己是否能加载,如果不能,返回null;如果能,返回加载信息。
-
Custom类加载器同样如此,只是它是返给调用方,无论是异常还是正常加载信息。
双亲委派的作用
-
避免类的重复加载。当父类加载器加载了该类时,子类加载器就不会再次加载。
-
防止java核心jar被随意替换。
这里重要的是安全问题,也就是防止核心jar被随意替换。比如,自己实现一个java.lang.String类,然后让JVM加载,类加载器逐级往上查找判断是否被加载,而在启动类加载器(Bootstrap)的时候,发现该java.lang.String已经被加载过了,不需要加载,直接返回,所以自己实现的java.lang.String就没被加载到。
当然,我们在实现代码的时候,是不能命名一个包路径为java.lang的,因为java.lang目录里面存放的都是java的核心jar包,是不允许应用程序自定义java.lang这个包名的,强行定义,在加载时会抛:java.lang.SecurityException: Prohibited package name: java.lang
实现一个自定义类加载器
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
上面这个的类加载器的源码,不能看出loadClass
这个方法实现的是双亲委派机制,并不是真正的进行类加载操作,真正的类加载操作是在c = findClass(name);
中的findClass
方法。
然后再看看findClass的源码,只有一句throw new ClassNotFoundException(name);
,需要自己实现。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
所以自定义类加载器只需要重写findClass方法即可。
- 需要加载的class文件未在classpath指定路径之下:
public class MyClassLoader extends ClassLoader{
private final String classPath = "C:\\Users\\Administrator\\Desktop\\delete\\learn\\";
private final String fileSuffix = ".class";
public static void main(String[] args) throws Exception {
Object instance = new MyClassLoader().loadClass("com.yl.test.class_load.ClassLoadDemo").newInstance();
System.out.println(instance.getClass());
System.out.println(instance.getClass().getClassLoader());
}
public static void classLoad() throws ClassNotFoundException {
Class<?> aClass = new MyClassLoader().getClass().getClassLoader().loadClass("com.yl.test.class_load.TClassLoadDemo");
System.out.println(aClass.getClassLoader());
System.out.println(aClass.getName());
}
/**
* 重写的findClass
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] arr = loadClassAsByteArr(name);
return super.defineClass(name, arr, 0, arr.length);
}
/**
* 读取指定目录下的class文件,作为二进制字节流
* @param name
* @return 二进制字节流
* @throws ClassNotFoundException
*/
private byte[] loadClassAsByteArr(String name) throws ClassNotFoundException {
if (null == classPath || null == name){
throw new ClassNotFoundException("类路径不能为空!");
}
String path = classPath + name.replaceAll("[.]", Matcher.quoteReplacement(File.separator)) + fileSuffix;
FileInputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(path);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int size = 0;
while ((size = in.read(buffer)) != -1) {
out.write(buffer, 0, size);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
}
我就直接上代码了,注释都有。
- 如果需要加载的类就在classpath指定路径下面。那么还可以简单些:
Class<?> aClass = new MyClassLoader().getClass().getClassLoader().loadClass("com.yl.test.class_load.TClassLoadDemo");
首先获取MyClassLoader
的类加载器:MyClassLoader
是由application类加载加载的,那么直接调用application类加载的loadClass
方法即可。