放置兽人军团
64.77MB · 2025-09-29
平时小伙伴们在做业务开发的时候可能比较少去接触类加载器,但是如果你想要深入学习Tomcat这种开源项目的话,熟悉类加载的原理就是必须的了。
类加载器,顾名思义,就是一个可以将java字节码加载为java.lang.Class实例的工具。
类加载器有以下两个特点:
1.动态加载:不需要在程序一开始运行的时候就去加载,而是在程序运行的过程中,动态地按需加载,.class字节码可以来源于各个地方,比如jar包、war包、网络中等等。
2.全盘负责:当一个类加载器去加载一个类时,这个类所依赖的、引用的其它所有的类都将由这个类加载器去加载,除非在程序中显示地指定另外一个类加载器加载。
一个类的唯一性是由加载它的类加载器和这个类的本身决定(类的全限定名+类加载器的实力ID作为唯一标识,也就是PackageName+ClassName+ClassLoader Id),因此在一个运行程序中有可能存在两个包名和类名完全一致的类,但是如果这两个类不是由同一个ClassLoader加载的,就会被视为两个不同的类。比较两个类是否相等(包括Class对象的equals()、isInstance()以及instanceof关键字等),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不相等。
从实现方式上,类加载器可以分为两种:一种是启动类加载器,是由C++语言实现的,是虚拟机自身的一部分;另一种是继承于java.lang.ClassLoader的类加载器,包括扩展类加载器、应用程序类加载器以及自定义类加载器。
启动类加载器(Bootstrap ClassLoader):负责加载<JAVA_HOME>lib目录中的jar包的,或者被-Xbootclasspath参数所指定的路径,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果想设置Bootstrap ClassLoader为其parent,可直接设置null。
我们写个代码来测试一下启动类加载器的打印:
为什么打印的不是“Bootstrap ClassLoader”而是 null 呢? 这是因为启动类加载器(Bootstrap ClassLoader)是由 C++ 实现的,而这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。
扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>libext目录中的jar包的,或者被java.ext.dirs系统变量所指定路径中的所有类库。该类加载器由sun.misc.Launcher$ExtClassLoader实现。扩展类加载器由启动类加载器加载,其父类加载器为启动类加载器,即parent=null。
我们写个代码来测试一下扩展类加载器的打印:
应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)上所指定的类库,由sun.misc.Launcher$App-ClassLoader实现。开发者可直接通过java.lang.ClassLoader中的getSystemClassLoader()方法获取应用程序类加载器,所以也可称它为系统类加载器。应用程序类加载器也是启动类加载器加载的,但是它的父类加载器是扩展类加载器。在一个应用程序中,系统类加载器一般是默认类加载器。
应用程序类加载器是用来加载 classpath 也就是用户写的所有类的,接下来我们写代码测试一下应用程序类加载器的打印,代码如下:
双亲委派机制,可以用一句话来说表达:任何一个类加载器在接到一个类的加载请求时,都会先让其父类进行加载,只有父类无法加载(或者没有父类)的情况下,才尝试自己加载。
双亲委派机制是java类加载器的一种工作模式,通过这种工作模式,java虚拟机将类文件加载到内存中,这样就保证了java程序能够正常的运行起来。JVM 并不是在启动时就把所有的.class文件都加载进来,而是程序在运行过程中用到了这个类才去加载。除了启动类加载器外,其他所有类加载器都需要继承抽象类ClassLoader,这个抽象类中定义了三个关键方法,理解清楚它们的作用和关系非常重要。
public abstract class ClassLoader {
//每个类加载器都有个父加载器
private final ClassLoader parent;
public Class<?> loadClass(String name) {
//查找一下这个类是不是已经加载过了
Class<?> c = findLoadedClass(name);
//如果没有加载过
if( c == null ){
//先委派给父加载器去加载,注意这是个递归调用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加载器为空,查找Bootstrap加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加载器没加载成功,调用自己的findClass去加载
if (c == null) {
c = findClass(name);
}
return c;
}
protected Class<?> findClass(String name){
//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
...
//2. 调用defineClass将字节数组转成Class对象
return defineClass(buf, off, len);
}
// 将字节码数组解析成一个Class对象,用native方法实现
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
}
从上面的代码可以得到几个关键信息:
双亲委派机制原理:
双亲委派机制保证类加载器自下而上的委派,又自上而下的去加载类,这样可以保证每一个类在各个类加载器中都是同一个类。
一个非常明显的目的就是保证java官方的类库<JAVA_HOME>lib和扩展类库<JAVA_HOME>libext的加载安全性,不会被我们开发者覆盖,加载过了,就不用再加载一遍。
例如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。
如果开发者自己开发开源框架,也可以自定义类加载器,利用双亲委派模型,保护自己框架需要加载的类不被应用程序覆盖。