1. 类加载过程
1.1. 加载
- 通过类名获取类的二进制字节流。可以从从本地文件、网络、数据库、自定义等方式获取。
- 在内存的方法区中生成一个代表这个类的java.lang.Class对象。
1.2. 链接
1.2.1. 验证
- 确保Class文件字节流符合JVM的要求。
- 主要包括对文件格式、元数据、字节码、符号引用等进行验证。
1.2.2. 准备
初始化static变量并设置初始值。初始值是数据类型的零值,例如int是0、boolean是false。
1.2.3. 解析
把编译时的符号引用(类似名称)转换为运行时的直接引用(具体地址),让程序能准确找到各种类、方法和字段。如果找不到对应目标,程序会出问题。
1.3. 初始化
- 执行类构造器<clinit>(),注意,不是构造函数(构造函数是“对象”初始化的时候用的)。
- 为static变量赋值,也就是程序员在代码中定义的初始值。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
类比记忆:
用组装一辆自行车的过程来类比 Java 类加载的各个阶段:
一、加载(买回来)
买一堆组装自行车的零件回来。就像类加载器获取类的二进制字节流,我们先要去寻找车架、车轮、车把等各种零件。
二、链接(掏出来)
- 验证(看买的是不是合适):检查零件是否符合要求。就像类加载中验证Class文件的字节流是否符合JVM要求。
- 准备(掏出来放到地上):将零件从塑料袋里拿出来,摆好。就像为类变量分配内存并设置初始值。
- 解析(看图纸):确定各个零件的安装位置和安装方式。比如知道车把要安装在哪个部位、链条如何连接。就像类加载中,将符号引用转换为直接引用,明确各种资源的具体使用方式。
三、初始化(组装)
正式组装自行车。按步骤将各个零件组装在一起,使自行车能够正常使用。就像类加载中,执行类构造器方法,为类变量赋予指定的初始值。
只有当有人准备骑这辆车(主动使用类),才会触发整个组装过程。如果只是看看自行车的图片(非主动使用),则不会触发组装过程。
2. 类加载器
类加载器是用于加载字节码到JVM中,代表某个类的Class对象。
- 数组和基本类型没有类加载器,由JVM直接创建
- 其他类型的对象,都有类加载器
3. 三种类加载器
3.1. 启动类加载器(Bootstrap ClassLoader)
- JVM内置的,C/C++实现,加载JVM自身运行所需的核心库,如Java安装目录下的jre/lib目录中的文件。
- java.lang.Object、java.lang.String等核心类都是由启动类加载器加载的。
- Java代码中不能获得启动类加载器的实例,它对开发人员是不可见的,代码中获取classLoader的时候如果获取到null,就说明是启动类加载器加载的。
3.2. 扩展类加载器(Extension ClassLoader)
- 加载Java的扩展库,如Java安装目录下jre/lib/ext或者系统属性java.ext.dirs指定的目录。
- 扩展类加载器是Java实现的,它的父加载器是启动类加载器。
3.3. 应用程序类加载器(Application ClassLoader)
- 也称为系统类加载器,加载应用程序类路径(classpath)下的类。通常是开发人员编写的Java代码所对应的类。
- 应用程序类加载器的父加载器是扩展类加载器。
3.4. 自定义类加载器
- 开发人员可编写自己的类加载器,以实现灵活的类加载策略,如加解密字节码、热部署(运行时替换类)、远程加载(加载网络上的类)、隔离类(避免冲突和安全)。
- 若未指定,自定义类加载器的父加载器默认是应用程序类加载器。
- 可以指定自定义类加载器是扩展、应用程序、其他自定义类加载器作为父加载器。
- 不能指定启动类加载器作为父加载器。
启动核心无法动,扩展加载扩展类。应用加载开发类,自定灵活随便搞。
以大型超市为记忆宫殿:
启动:超市仓库,存放核心货物,对应加载核心类库,神秘不可见。
扩展:进口商品区,带来特色商品,如加载扩展类库增添功能。
应用程序:日常用品区,顾客常光顾,如同加载日常开发的类。
自定义:特色定制区,根据特殊需求定制商品,对应灵活创新的自定义加载。
4. 双亲委派模式
当类加载器收到加载请求时,先将请求委派给父类加载器,父类加载器无法加载时自己才尝试加载。
这样确保了核心类库的安全稳定,避免类重复加载,实现类的隔离。
5. 打破双亲委派
打破双亲委派模型应该谨慎使用,因为这可能会导致一些潜在的问题,如类的重复加载、类的不一致性等。在大多数情况下,双亲委派模型是一种安全和可靠的类加载机制,应该尽量遵循。
方法1. 自定义类加载器
在自定义类加载器中,可以重写 loadClass方法来实现自己的类加载逻辑,从而打破双亲委派模型
public class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
// 先尝试自己去加载类
return findClass(name);
} catch (ClassNotFoundException e) {
// 如果自己加载失败,再调用父类的 loadClass 方法
return super.loadClass(name);
}
}
}
方法2. 用线程上下文类加载器
线程上下文类加载器可以在运行时动态地设置为特定的类加载器,从而使得在特定的代码区域中使用这个类加载器来加载类,而不是遵循双亲委派模型。
public class ClassLoaderExample {
public static void main(String[] args) throws Exception {
// 获取当前线程的上下文类加载器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
// 设置上下文类加载器为自定义类加载器
Thread.currentThread().setContextClassLoader(new CustomClassLoader());
// 使用上下文类加载器加载类
Class<?> clazz = contextClassLoader.loadClass("SomeClass");
}
}
6. tomcat为啥要打破双亲委派?
tomcat上可以部署多个应用,如果Tomcat不打破双亲委派,则所有应用的类都被同一个加载器管着,会有两个问题:
- 类冲突:比如两个应用都要用了同一个jar但版本不同,就会冲突;
- 做不到热部署:应用改了代码后不能快速重新加载新的类,这样开发和部署起来就不方便。