一、Java 类加载过程
- 将多个 java文件,经过 编译打包 生成可运行的 jar包。
- 首先需要通过 类加载器 把 主类 加载到 JVM。
- 最终由 java命令,运行主类的 main()函数,启动程序。
- 主类在运行过程中,如果使用到其他类,会逐步加载这些类。
- 注意:jar 包里的类 不是一次性全部加载的,而是 使用到时才加载。
1. Java 类加载过程分七步
1加载 > 2验证 > 3准备 > 4解析 > 5初始化 > 6使用 > 7卸载
- 加载:
在硬盘上查找,并通过 IO 读入字节码文件。
使用到类时才会加载。
例如:调用类的 main()方法,new对象 等等。- 验证:
校验 字节码文件 的正确性。- 准备:
给类的 静态变量 分配内存,并赋予默认值。- 解析:
将 符号引用 替换为 直接引用。
该阶段会把一些 静态方法(比如:main()方法),替换为指向数据所在内存的 指针 或 句柄 等(直接引用)。
- 这是所谓的 静态链接 过程(类加载期间完成)。
- 动态链接 是在程序运行期间完成的,将 符号引用 替换为 直接引用。
- 初始化:
对类的 静态变量 初始化为指定的值,执行静态代码块。
二、Java 类加载器
- 上面 Java 类加载过程,主要是通过 Java 类加载器 来实现的。
1. Java 三个核心类加载器
- 启动类加载器:
负责加载支撑 JVM 运行的,位于 JRE 的 lib 目录下的 核心类库。
比如:rt.jar、charsets.jar 等。- 扩展类加载器:
负责加载支撑 JVM 运行的,位于 JRE 的 lib/ext 目录下的 扩展 jar 包。- 应用程序类加载器:
负责加载 ClassPath 路径下的类包,主要就是加载自己写的类。- 自定义加载器:
负责加载用户 自定义路径 下的类包。
2. 自定义类加载器
- 自定义类加载器:
- 继承
java.lang.ClassLoader
类。- 重写
loadClass(..)
方法,实现了 双亲委派机制。- 重写
findClass(.)
方法。
loadClass(..)
方法:实现了 双亲委派机制。
- 首先检查一下 指定类名 是否已经加载过。
如果加载过了就不需要再加载,直接返回。- 如果没有加载过,再判断一下是否有 父加载器。
如果有父加载器,则由父加载器加载(即:调用parent.loadClass(name, false);
)。
否则调用 BootstrapClassLoader 来加载。- 如果 父加载器 及 BootstrapClassLoader,都没有找到指定的类。
那么调用 当前类加载器 的findClass(.)
方法来完成类加载。
findClass(.)
方法:默认的实现是抛出 ClassNotFoundException 异常。
所以 自定义类加载器 必须重写这个方法。
/**
* @author wy
* describe 自定义类加载器
*/
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return super.loadClass(name, resolve);
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
return super.findClass(className);
}
}
3. 双亲委派机制
- 加载某个类时,会先委托 父加载器 寻找目标类,找不到再委托 上层父加载器 加载。
- 如果所有 父加载器 都找不到目标类,则在 自己的类加载路径 下查找并载入目标类。
- 加载 MyClass 类:
- 最先会找 应用程序类加载器 加载,应用程序类加载器 会委托 扩展类加载器 加载,扩展类加载器 再委托 启动类加载器 加载。
- 顶层 启动类加载器 在自己的类加载路径下没有找到 MyClass类,则向下退回加载 MyClass 类的请求。
- 扩展类加载器 收到回复就自己加载,在自己的类加载路径下也没找到 MyClass类,又向下退回 MyClass 类的加载请求给 应用程序类加载器。
- 应用程序类加载器 于是在自己的类加载路径里找 MyClass 类,结果找到了就自己加载了。
- 双亲委派机制 简单点解释:先找父亲加载,不行再由儿子自己加载。
3.1 示例——双亲委派机制
- 为什么设计双亲委派机制?
- 沙箱安全机制:
自定义java.lang.String
类不会被加载,这样便可以 防止 核心API库 被随意篡改。- 避免类的重复加载:
当父亲已经加载过该类时,子类加载器 就没有必要再加载一次,保证 被加载类 的唯一性。
package java.lang;
/**
* @author wy
* describe 双亲委派机制。
* 防止恶意代码替换`JDK`源码,为了保证安全。
* <p>
* AppClassLoader > ExtClassLoader > BootstrapClassLoader(最终执行)。
* 1、类加载器 收到类加载请求。
* 2、`AppClassLoader`将请求委托给父类加载器,一直向上委托,直到`BootstrapClassLoader`。
* 3、`BootstrapClassLoader`开始加载,可以加载到结束,加载不到则抛出异常,通知子类加载器。
* 4、`ExtClassLoader`开始加载,可以加载到结束,加载不到则抛出异常,通知子类加载器。
* 4、最后`AppClassLoader`开始加载,可以加载到结束,加载不到抛出异常(`ClassNotFoundException`)。
*/
public class String {
@Override
public String toString() {
return "String{}";
}
public static void main(String[] args) {
String string = new String();
string.toString();
/*
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
*/
}
}
3.2 示例——打破双亲委派机制,验证沙箱安全机制
- 自定义类加载器,加载自定义
java.lang.String
类。
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
// 类路径
MyClassLoader2 classLoader = new MyClassLoader2("E:\\java\\toobox\\learn\\java-learn\\java-jvm\\src\\main\\java");
// 包名
Class clazz = classLoader.loadClass("java.lang.String");
/*
java.lang.SecurityException: Prohibited package name: java.lang
`禁止的包名称:java.lang`
*/
}
4. 打破双亲委派机制
- Tomcat 为了打破双亲委派机制,需要解决的问题:
- 一个 Web容器 可能需要 部署两个应用程序,不同的应用程序 可能会依赖 相同类库 的 不同版本。
不能要求 相同类库 在服务器只有一份,因此 每个应用程序的类库 都是独立的,要 保证相互隔离。- 部署在同一个 Web容器 中的应用程序,相同类库 的 相同版本 可以共享。
否则,如果服务器有 10 个应用程序,那么要有 10 份 相同的类库 加载进虚拟机。- Web容器 也有 自己依赖的类库,不能与应用程序的类库混淆。
基于安全考虑,应该让 容器的类库 和 程序的类库 隔离开来。- Web容器 要支持 jsp 的修改,jsp文件 最终也是要编译成 class文件,才能在虚拟机中运行。
但程序运行后修改 jsp文件 已经是司空见惯的事情,Web容器 需要支持 jsp 修改后不用重启。
- 上面问题的解决办法:
- 问题一,如果使用 默认的类加载机制(双亲委派),那么是 无法加载进 相同类库 的 不同版本的。
默认的类加器 是不管你是什么版本的,只在乎你的 全限定类名 只有一份。- 问题二,默认的类加载器 是能够实现的,因为他的职责就是 保证唯一性。
- 问题三,和 问题一 一样。
- 问题四,需要实现 jsp文件 的热加载,jsp文件 其实也就是 class文件。
- 如果修改了 jsp文件,但类名还是一样,类加载器 会直接取方法区中已经存在的。
这样修改后的 jsp文件 是不会重新加载的。- 解决办法可以 卸载掉这个 jsp文件 的类加载器。
所以每个 jsp文件 对应一个 唯一的类加载器。- 当一个 jsp文件 修改了,就直接卸载这个 jsp类加载器。
重新创建 类加载器,重新加载 jsp文件。
三、Tomcat 类加载器
1. Tomcat 四个核心类加载器
- CommonClassLoader 最基本的类加载器:
加载路径中的 class文件,可以被 Tomcat容器本身 以及 各个Webapp 访问。- CatalinaClassLoader 容器私有的类加载器:
加载路径中的 class文件,对于 Webapp 不可见。- SharedClassLoader 各个Webapp 共享的类加载器:
加载路径中的 class文件,对于 所有Webapp 可见,但是对于 Tomcat容器 不可见。- WebappClassLoader 各个Webapp 私有的类加载器:
加载路径中的 class文件,只对 当前Webapp 可见。
2. Tomcat 委派机制
- CommonClassLoader 加载的类,实现了公有类库的共用。
可以被 CatalinaClassLoader 和 SharedClassLoader 使用。- CatalinaClassLoader 和 SharedClassLoader 自己加载的类,则与对方相互隔离。
- WebAppClassLoader 可以使用 SharedClassLoader 加载到的类。
但 各个WebAppClassLoader 实例之间 相互隔离。- JasperLoader 的加载范围,仅仅是这个 jsp文件 所编译出来的那一个 class文件。
- JasperLoader 出现的目的就是为了被丢弃。
- 当 Web容器 检测到 jsp文件 被修改时,会替换掉目前的 JasperLoader 的实例。
- 并通过再建立一个新的 jsp类加载器,来实现 jsp文件 的热加载功能。
3. Tomcat 打破双亲委派
- Tomcat类加载机制 违背了 Java 推荐的 双亲委派模型。
- 双亲委派机制,要求除了 顶层的启动类加载器 之外,其余的类加载器 都应当由自己的 父类加载器 先加载。
- 很显然,Tomcat 不是这样实现的,Tomcat 为了实现 隔离性,没有遵守这个约定。
- 每个 WebappClassLoader 加载自己的目录下的 class文件,不会传递给 父类加载器,打破了双亲委派机制。