三种类加载器
启动类加载器(Bootstrap ClassLoader)
负责加载<JAVA_HOME>\lib
目录,或者被-Xbootclasspath
参数所指定的路径存放的,能够被虚拟机所识别的类库加载到虚拟机的内存中,这个类加载器的底层是由C++实现的,是虚拟机当中的一部分,其它类加载器都是由Java实现的,独立于虚拟机以外,全部继承自java.lang.ClassLoader
抽象类。
在启动类加载器执行时,会加载一个很重要的类:sun.misc.Launcher
,这个类里面含有两个静态内部类:
ExtClassLoader
扩展类加载器AppClassLoader
应用程序加载器
在Launcher类加载完成以后会对该类进行初始化,在初始化过程中会创建两个类加载器的实例,源码如下所示:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
}
扩展类加载器(Extension ClassLoader)
负责加载<JAVA_HOME>\lib\ext
目录下,或者被java.ext.dirs
系统变量所指定的路径中所有的类库。
应用程序类加载器(Application ClassLoader)
也被称为“系统类加载器”,负责加载用户类路径(java -classpath
)上的所有类库。如果没有自定义类加载器,那么这个加载器就是默认的类加载器。可以通过ClassLoader
的getSystemClassLoader()
获取该类加载器的实例。
注意:每种类加载器加载的类信息都会存放在方法区的不同区域上,所以不同的类加载器如果加载相同的一个类字节码文件,在虚拟机看来生成的两个类对象是不相同的!如果有两个相同的类User
,通过Application ClassLoader
加载和Extension ClassLoader
加载出来的两个User
类对象,是不相同的。
双亲委派模型
双亲委派的工作过程
一个类加载器收到类加载的请求时,它不会马上加载该类,而是把这个请求委托给父加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求都必须先通过启动类加载器尝试加载,只有当父加载器无法加载这个类时,才会把加载请求传递给它的子加载器去尝试加载,流程如下:
双亲委派模型的作用
使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object
存放在rt.jar
当中,无论哪一层的类加载器需要加载Object
类,最终都是委派到启动类加载器进行加载,所以可以保证Object
类在程序的各种类加载器环境中是同一个类。
不同的类加载器加载同一个类会导致出现相同的字节码文件产生不同的Class实例信息,即每个加载器加载同一个字节码文件会保存在方法区的不同位置,所以如果不用双亲委派模型,如果用户自己写了一个java.lang.Object
类,并放在程序的classpath
当中,那么系统中会出现多个版本的Object
类,那么程序就会一片混乱,使用Object
类时不知道该哪个加载器加载的Class实例。
如下图,在方法区中每个加载器都有自己的一个区域存储自己加载的类类型信息,所以双亲委派模型的重要性体现在此。
双亲委派模型的源码实现
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先检查类是否已经被加载过了
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异常,说明父加载器无法加载这个类
}
if (c == null) {
//如果没有加载这个类,按顺序调用findClass方法去尝试加载这个类
long t1 = System.nanoTime();
c = findClass(name);
//记录这个类的装入信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派模型的特点
- 父加载器的方法区内存储的类信息对子加载器是可见的,因为子加载器收到类加载请求后会先委派给父加载器,如果父加载器中已经加载了这个类,会直接返回这个类的信息给子加载器,就不用继续加载了。
- 双亲委派模型下每个类都是唯一的,只会由一个类加载器加载。
违背双亲委派的自定义类加载器
我们可以继承ClassLoader
类,覆盖loadClass()
并且在代码中不委派给父加载器,并且覆盖findClass()
方法定义自己的类加载规则。
我们定义与系统类库相同的一个类sun.applet.Main
,并用自定义的类加载器进行类加载,将其与JVM默认加载的sun.applet.Main
作类型判断,判断两个类是否相同。
测试Demo的结构图如下,按照结构图创建项目,把代码复制进去就一定没有错,曾经作者在找类路径这一块卡了一个晚上······
第一步,自定义sun.applet.Main
类
package sun.applet;
/**
* @author Zeng
* @date 2020/4/10 8:22
*/
public class Main {
static {
System.out.println("customized sun.applet.Main constructed");
}
public static void main(String[] args) {
System.out.println("recognized as sun.applet.Main in jdk," +
" and there isn't any main method");
}
}
第二步,自定义类加载器UnDelegationClassLoader
package sun.applet;
import java.io.*;
/**
* @author Zeng
* @date 2020/4/10 8:01
*/
public class UnDelegationClassLoader extends ClassLoader {
private String classpath;
public UnDelegationClassLoader(String classpath) {
super(null);
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
InputStream is = null;
try {
String classFilePath = this.classpath + name.replace(".", "/") + ".class";
is = new FileInputStream(classFilePath);
byte[] buf = new byte[is.available()];
is.read(buf);
return defineClass(name, buf, 0, buf.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
throw new IOError(e);
}
}
}
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clz = findLoadedClass(name);
if (clz != null) {
return clz;
}
// jdk 目前对"java."开头的包增加了权限保护,这些包我们仍然交给 jdk 加载
if (name.startsWith("java.")) {
return ClassLoader.getSystemClassLoader().loadClass(name);
}
return findClass(name);
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, FileNotFoundException {
sun.applet.Main obj1 = new sun.applet.Main();
UnDelegationClassLoader unDelegationClassLoader = new UnDelegationClassLoader("./out/production/classloading/");
System.out.println(unDelegationClassLoader.findClassPath());
String name = "sun.applet.Main";
Class<?> clz = unDelegationClassLoader.loadClass(name);
Object obj2 = clz.newInstance();
System.out.println("obj1 class: "+obj1.getClass());
System.out.println("obj2 class: "+obj2.getClass());
System.out.println("obj1 classloader: "+obj1.getClass().getClassLoader());
System.out.println("obj2 classloader: "+obj2.getClass().getClassLoader());
System.out.println("obj1 == obj2" + obj1 == obj2);
}
}
输出结果如下
可以看到Java类库中Main
的实例obj1
和自定义的obj2
实例的类对象都是sun.applet.Main
,但是由于类加载器不同,所以obj2
与Java类库中的Main
对象是不相同的。所以如果不进行双亲委派,如果第三方依赖需要调用sun.applet.Main
,就会变得糊涂,不知道该调用哪一个Main
。
自定义类加载器的正确姿势
自定义类加载器DelegationClassLoader
,并委托给父加载器AppClassLoader
package sun.applet;
import java.io.FileInputStream;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
/**
* @author Zeng
* @date 2020/4/10 7:23
*/
public class DelgationClassLoader extends ClassLoader {
private String classpath;
public DelgationClassLoader(String classpath, ClassLoader parent) {
super(parent);
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
InputStream is = null;
try {
String fileClasspath = this.classpath + name.replace(".", "\\") + ".class";
is = new FileInputStream(fileClasspath);
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
}catch (Exception e){
throw new ClassNotFoundException(name);
}finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
throw new IOError(e);
}
}
}
}
}
第二步,测试加载重名类sun.applet.Main
,由于篇幅问题,只贴出核心方法
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, FileNotFoundException {
sun.applet.Main obj1 = new sun.applet.Main();
DelgationClassLoader unDelegationClassLoader = new DelgationClassLoader("./out/production/classloading/", ClassLoader.getSystemClassLoader());
String name = "sun.applet.Main";
Class<?> clz = unDelegationClassLoader.loadClass(name);
Object obj2 = clz.newInstance();
System.out.println("obj1 class: "+obj1.getClass());
System.out.println("obj2 class: "+obj2.getClass());
System.out.println("obj1 classloader: "+obj1.getClass().getClassLoader());
System.out.println("obj2 classloader: "+obj2.getClass().getClassLoader());
System.out.println("obj1 instanceof sun.applet.Main: " + (obj1 instanceof sun.applet.Main));
System.out.println("obj2 instanceof sun.applet.Main: " + (obj2 instanceof sun.applet.Main));
}
输出结果如下图所示:
可以看到无论是Java类库中的sun.applet.Main
还是自定义的sun.applet.Main
,JVM都交给了启动类加载器去加载它们,所以在第三方依赖希望调用sun.applet.Main
时,它会非常清楚,只能调用这一个Class
对象。所以这也解释了为什么双亲委派模型可以确定系统中只有一个唯一的类。
总结:
这篇文章主要讲解了三种系统默认拥有的类加载器、什么是双亲委派模型、双亲委派模型的工作流程、源码分析以及它的特点,使用实际案例演示如果违背双亲委派模型会带来什么后果,反映出双亲委派模型的好处。当然,双亲委派模型不是一种强制约束,它只是虚拟机的其中一种实现,如Tomcat、JDBC等技术就破坏了双亲委派模型,有兴趣的读者可以去深入了解,作者由于能力有限、时间有限,无法讲解更多更详细深入的知识点。如果这篇文章对你有小小的帮助,你的点赞是对我最大的鼓励和支持!感谢你的阅读!
巨人的肩膀:
《深入理解Java虚拟机》第三版