JVM 类加载器过程
# 类加载过程
Java 类加载过程大体上可以分为三个阶段:加载、链接、初始化。详细定义可以查看java虚拟机规范 (opens new window)。
其中链接(Linking)阶段又可以分为:验证、准备、解析
- 加载(Loading):从任意数据源(例如:文件、jar包、数据库,甚至是网络)中获得字节流并加载到 jvm 中,然后映射为一个 Class 对象
- 链接(Linking)
- 验证(Verification):验证字节码是否符合 jvm 规范,是 jvm 安全运行的报障
- 准备(Preparation):创建类(包括接口)中的静态变量,并为静态变量赋初始值,分配所需要的内存空间
- 解析(Resolution):将常量池中的符号引用替换为直接引用
- 初始化(Initialization):执行编译器自动生成的
<clinit>()
方法(如果没有静态变量或者静态代码块,则不会生成<clinit>()
方法),为静态变量赋值,执行静态代码块逻辑(static),父类初始化优先于子类
<clinit>()
方法是为静态变量赋值和执行静态代码块,在编译期间自动收集所有静态变量和所有静态代码块,合并成一个方法如果是
final
修饰的静态变量,在编译期就已经赋值,不会包含在<clinit>()
方法里
# 双亲委派
双亲委派机制是一种规范,在类加载过程中,优先使用父级已经加载的 Class(与父级类加载器是组合关系,不是继承),如果父类也没有定义对应的 Class,则继续向父级的父级查找,直到父级 Classloader 为 null(启动类加载器 - BootstrapClassLoader),委派模型的目的是为了避免重复加载 Class。
委派关系图如下
双亲委派的意义:
- 避免类被重复加载
- 保护核心类库被篡改
打破双亲委派的意义
- 资源隔离,如 tomcat 支持部署多个项目,并且互不干扰
- 解决 jar 包冲突,如老项目迭代,存在两个不同版本jar包,解决版本冲突
可以通过启动参数,指定对应类加载器加载的 jar \ Class 文件
启动类加载器 - BootstrapClassLoader
# 指定新的bootclasspath,替换java.*包的内部实现 java -Xbootclasspath:<your_boot_classpath> # a意味着append,将指定目录添加到bootclasspath后面 java -Xbootclasspath/a:<your_dir> # p意味着prepend,将指定目录添加到bootclasspath前面 java -Xbootclasspath/p:<your_dir>
1
2
3
4
5
6// 打印加载路径 URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs(); System.out.println("================启动类加载器================"); for (URL urL : urLs) { System.out.println(urL); }
1
2
3
4
5
6扩展类加载器 - ExtClassLoader
# 覆盖 jre/lib/ext/ 目录的 jar 包 java -Djava.ext.dirs=path_to_ext_dir
1
2System.out.println("================扩展类加载器================"); String extDirs = System.getProperty("java.ext.dirs"); // String[] dirs = extDirs.split(";"); // for windows String[] dirs = extDirs.split(":"); for (String dir : dirs) { System.out.println(dir); }
1
2
3
4
5
6
7应用程序类加载器 App(System)ClassLoader
# -classpath 或者 -cp,两者等价,多个文件或路径用`:`分隔(windows用`;`) java -cp path_to_jar:path_to_dir # 指定应用程序类加载器,设置这个参数后 App(System)ClassLoader 将成为自定义类加载器的父亲 java -Djava.system.class.loader=com.pkg.MyClassLoader
1
2
3
4
提示
实际上,并不是所有的类加载器都符合这个规范,例如 Tomcat 通过自定义的类加载器,实现了应用隔离和动态编译 jsp。
tomcat 类加载器架构
# 自定义 Classloader
java 支持用户实现自定义 Classloader
来实现不修改 jdk 代码的同时来扩展 java 功能,这一点在 c 和 c++ 就无法实现。
要自定义 ClassLoader
只需要继承 java.lang.ClassLoader
或其子类,通常我们可以重写findClass(String name)
或者loadClass(String name, boolean resolve)
方法实现自定义类加载逻辑。
通过查看loadClass(String name, boolean resolve)
方法源码可以知道,方法内部会首先判断parent
是否已经加载过对应类,如果没有加载才会执行自身的加载逻辑,即:保留了双亲委派模型。
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) {
}
if (c == null) {
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
由此得出:
- 保留双亲委派模型则重写
findClass
方法 - 不保留双亲委派模型则重写
loadClass
方法
# 应用
自定义类加载器的常见应用
- 实现模块化机制,可以隔离运行环境,解决jar包冲突等
- 实现代码热加载(如 tomcat 实现 jsp 热加载)
- 字节码加密,防止反编译
- 扩展加载源