JVM学习笔记:类加载器
类加载器简介
类加载器是一个负责加载类的对象。ClassLoader
是一个抽象类。给定类的二进制名称,类加载器应该尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的"类文件"。
每个Java类都有一个引用指向加载它的ClassLoader
。不过,数组类不是通过ClassLoader
创建的,而是JVM在需要的时候自动创建的,数组类通过getClassLoader()
方法获取ClassLoader
的时候和该数组的元素类型的ClassLoader
是一致的。
由此可得:
- 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
- 每个Java类都有一个引用指向加载它的
ClassLoader
- 数组类不是通过
ClassLoader
创建的(数组类没有对应的二进制字节流),是由JVM直接生成的。
简单来说:类加载器的主要作用就是加载Java类的字节码(.class文件)到JVM中(在内存中生成一个代表该类的Class对象)。
类加载器加载规则
JVM启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
对于已经加载的类会被放在ClassLoader
中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。
类加载器的层次
注意:这里父类加载器并不是通过继承关系来实现,而是采用组合实现的。
类加载器可以大致划分为以下三类:
- 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或者被-Xbootclasspath参数指定的路径中,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类),开发者可以直接使用扩展类加载器。
- 扩展类加载器:Extension ClassLoader,该加载器由
sun.misc.Launcher$ExtClassLoader
实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库,开发者可以直接使用扩展类加载器 - 应用程序类加载器:Application ClassLoader,该类加载器由
sun.misc.Launcher$AppClassLoader
来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
自定义类加载器
通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader 类,我们只需要重写 findClass 方法即可。
类加载机制
- 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对饮的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
- 双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有类加载器请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
双亲委派机制过程
- 当AppClassLoader加载一个Class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父加载器ExtClassLoader去完成
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成
- 如果BootStrapClassLoader加载失败,会使用ExtClassLoader来尝试加载
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
双亲委派代码实现:
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
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 {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
双亲委派模型的好出
双亲委派模型保证了java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了Java的核心API不被篡改。
- 避免类的重复加载:
由于每个类加载器都会先尝试让其父类加载器加载类,这样可以确保每个类在JVM中只有一个唯一的版本,避免了同一个类被多次加载的问题。 - 保证Java核心库的安全:
核心库中的类由启动类加载器加载,这意味着无论应用代码如何尝试通过自定义类加载器加载替代的核心类(如java.lang.Object),都会首先由启动类加载器处理,从而防止替换这些核心类的任何尝试,确保了Java平台的安全性。 - 促进正版类库的稳定运作:
开发者可以自由扩展类加载器,但是由于双亲委派机制的存在,核心库和扩展库的类不会被应用的类所覆盖,从而保证了类库的稳定性和兼容性。
打破双亲委派模型的方法
自定义加载器的话,需要继承 ClassLoader
。如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。
为什么是重写 loadClass()
方法打破双亲委派模型呢?双亲委派模型的执行流程已经解释了:类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()
方法来加载类)。重写 loadClass()
方法之后,我们就可以改变传统双亲委派模型的执行流程。例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化。