一、双亲委派模型
我们知道类加载机制是将一个类从字节码文件转化为虚拟机可以直接使用类的过程,但是是谁来执行这个过程中的加载过程,它又是如何完成或者说保障了类加载的准确性和安全性呢?答案就是类加载器以及双亲委派机制。
双亲委派模型的工作机制是:当类加载器接收到类加载的请求时,它不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,只有当父加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去加载。
我们可以从JDK源码中将它的工作机制一窥究竟。
ClassLoader#loadClass(String,boolean)
这是在JDK1.8的java.lang.ClassLoader类中的源码,这个方法就是用于加载指定的类。
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) {
long t0 = System.nanoTime();
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);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
通过以上代码得出结论:
- 当类加载器接收到类加载的请求时,首先检查该类是否已经被当前类加载器加载;
- 若该类未被加载过,当前类加载器会将加载请求委托给父类加载器去完成;
- 若当前类加载器的父类加载器为null,会委托启动类加载器完成加载;
- 若父类加载器无法完成类的加载,当前类加载器才会去尝试加载该类。
类加载器分类
启动类加载器 Bootstrap ClassLoader
启动类加载器作为所有类加载器的鼻祖,是由C++实现的,不继承java.lang.ClassLoader类。它在虚拟机启动时会由虚拟机的一段C++代码进行加载,所以它没有父类加载器,在加载完成后,它会负责去加载扩展类加载器和应用类加载器。
启动类加载器用于加载java的核心类,位于JAVA_HOMElib中,或者被 -Xbootclasspath参数所指定的路径中,并且是虚拟机能够识别的类库。
扩展类加载器 Extension ClassLoader
拓展类加载器继承于java.lang.ClassLoader类,它的父类加载器是启动类加载器,而启动类加载器在java中的显示就是null。
引⾃ jdk1.8 ClassLoader#getParent() ⽅法的注释,这个⽅法是⽤于获取类加载器的⽗类加载器: Returns the parent class loader for delegation. Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class loader’s parent is the bootstrap class loader
扩展类加载器负责加载JAVA_HOMElibext目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
注意:扩展类加载器仅支持加载被打包为 .jar 格式的字节码文件。
应用类/系统类加载器 Application ClassLoader
JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader 实现自定义的类加载器。
自定义类加载器 Custom ClassLoader
自定义类加载器继承于 java.lang.ClassLoader 类,它的父类加载器是应用类加载器。
这是用户自定义的类加载器,可加载指定路径的字节码文件。
自定义类加载器需要继承 java.lang.ClassLoader 类并重写 findClass方法,用于实现自定义的加载类逻辑。
双亲委派模型的好处
基于双亲委派模型规定的这种带有优先级的层次性关系,虚拟机运行程序时就能够避免类的重复加载。
当父类类加载器已经加载过类时,如果再有该类的加载请求传递到子类加载器,子类加载器执行loadClass方法,然后委托给父类加载器尝试加载该类,但是父类加载器执行 Class<?> c = findLoadedClass(name);检查该类是否已经被加载过这一阶段就会检查到该类已经被加载过,直接返回该类,而不会再次加载此类。
双亲委派模型能够避免核心类篡改。一般我们描述的核心类是 rt.jar、tools.jar 这些由启动类加载器加载的类,这些类库在日常开发中被广泛运用,如果被篡改,后果将不堪设想。
双亲委派模型的不足
由于历史原因( ClassLoader 类在 JDK1.0 时就已经存在,⽽双亲委派模型是在 JDK1.2 之后才引⼊的),在未引⼊双亲委派模型时,⽤户⾃定义的类加载器需要继承 java.lang.ClassLoader 类并重写 loadClass() ⽅法,因为虚拟机在加载类时会调⽤ ClassLoader#loadClassInternal(String) ,⽽这个⽅法(源码如下)会调⽤⾃定义类加载重写的 loadClass() ⽅法。⽽在引⼊双亲委派模型后,ClassLoader#loadClass ⽅法实际就是双亲委派模型的实现,如果重写了此⽅法,相当于打破了双亲委派模型。为了让⽤户⾃定义的类加载器也遵从双亲委派模型, JDK新增了 findClass ⽅法,⽤于实现⾃定义的类加载逻辑。
private Class<?> loadClassInternal(String name) throws ClassNotFoundException{
// For backward compatibility, explicitly lock on 'this' when
// the current class loader is not parallel capable.
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}
由于双亲委派模型规定的层次性关系,导致⼦类类加载器加载的类能访问⽗类类加载器加载的类,⽽⽗类类加载器加载的类⽆法访问⼦类类加载器加载的类。为了让上层类加载器加载的类能够访问下层类加载器加载的类,或者说让⽗类类加载器委托⼦类类加载器完成加载请求,JDK 引⼊了线程上下⽂类加载器,藉由它来打破双亲委派模型的屏障。
当⽤户需要程序的动态性,⽐如代码热替换、模块热部署等时,双亲委派模型就不再适⽤,类加载器会发展为更为复杂的⽹状结构。
总结
最后
在文章的最后作者为大家整理了很多资料!包括java核心知识点+全套架构师学习资料和视频+一线大厂面试宝典+面试简历模板+阿里美团网易腾讯小米爱奇艺快手哔哩哔哩面试题+Spring源码合集+Java架构实战电子书等等!欢迎关注公众号:前程有光,领取!