类加载的过程
通过Java命令执行代码的大体流程:
其中loadClass的过程如下:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
- 加载:在硬盘上查找并通过IO读入字节码文件,使用类时才会记载,调用类的main()方法、new类的对象等等。在加载阶段会在内存生成一个代表这个类的java.lang.Class对象,作为方法区这个类各种数据的访问入口。
- 验证:校验字节码文件的正确性
- 准备:给类的静态变量初始化内存并复制默认值
- 解析:将符号引用替换为直接引用,将静态方法(符号引用比如main()方法)替换为指向数据所存内存的指针或者句柄等(直接引用),这种叫静态链接过程(类加载期间完成)。动态链接是在程序运行期间完成的将符号引用替换为直接引用。
- 初始化:对类的静态变量初始化为指定的值,且加载static代码块。
类被加载到方法区之后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。其中Class对象是开发人员访问方法区中类定义的入口和切入点。
类的初始化
对于类的初始化只有在主动引用的场景下才会进行初始化操作。即类加载了(取决jvm的实现)但类不一定去初始化。主动引用有五种场景:
- new实例、读取设置类的static字段、调用类的静态方法时
- 对类进行反射调用时
- 初始化一个类时发现父类没有初始化,触发父类的初始化
- main方法调用时要对类进行初始化
- jdk1.7之后对动态语言支持,MethodHandler实例解析结果句柄对应的类没有初始化时要初始化。
这里注意:
对应static final修饰的常量被调用不会触发主动引用。但是运行时常量是会触发类初始化的,比如下面这样的运行时常量:
1
2// 运行时常量是特殊的 不会加入到对应类的常量池中 会主动调用常量所在的类 导致类初始化 对应的助记符也是getstatic助记符
static final String rom = UUID.randomUUID().toString();引用类型数组new出来不会造成引用类的初始化,这里比较特殊,会JVM自己生成一个数组对象,不算主动引用。
类初始化换个角度来说就是对类所有变量赋值和执行static代码块的过程,即执行类构造器(不是构造函数)
类加载器和双亲委派机制。
jdk自带的类加载器:
- 引导类加载器:负责加载支撑JVM运行的位于jre的lib目录下的核心类库,比如rt.jar、charsets.jar等。
- 扩展类加载器:负责加载支撑JVM允许的位于jre的lib目录下的ext扩展目录中的jar包类
- 应用程序类加载器:负责加载ClassPath路径下的类包,主要是开发人员写的类。
关于类加载器的一个demo:1
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
28
29
30
31
32
33
34
35public class TestClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader()); // 输出null,因为引导类加载器是由C++实现的 不是直接在Launcher中的内部类
System.out.println(DESKeyFactory.class.getClassLoader()); // ext扩展类加载器
System.out.println(TestClassLoader.class.getClassLoader()); // app类加载器 加载开发者写在classpath的class
// 看下维护的parent父加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); // systemClassLoader默认是app类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
ClassLoader bootstarpClassLoader = extClassLoader.getParent();// 在逻辑上是引导类加载器 但是引导类加载器是由c++实现的。 这里ext的parent是null
System.out.println("systemClassLoader:" + systemClassLoader);
System.out.println("extClassLoader:" + extClassLoader);
System.out.println("bootstarpClassLoader:" + bootstarpClassLoader);
// 线程上下文保存的类加载器 有些SPI场景需要通过这个来打破双亲委派原则
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); //线程上下文类加载器默认是appClassLoader
System.out.println(contextClassLoader);
// 加载路径 APP类加载器对应 classpath的路径 这里注意也会有rt.jar和ext的包路径 但是因为双亲委派会优先给引导类加载器去加载核心jar包 自己写的类在target下最终会在appClassLoader中加载
System.out.println(System.getProperty("java.class.path"));
}
}
// 输出
null
sun.misc.Launcher$ExtClassLoader@2c7b84de
sun.misc.Launcher$AppClassLoader@18b4aac2
systemClassLoader:sun.misc.Launcher$AppClassLoader@18b4aac2
extClassLoader:sun.misc.Launcher$ExtClassLoader@2c7b84de
bootstarpClassLoader:null
sun.misc.Launcher$AppClassLoader@18b4aac2
省略appClassLoader路径的输出
类加载器初始化过程
AppClassLoader和ExtClassLoader是JVM启动类sun.misc.Launcher的内部类,其都继承了URLClassLoader。在JVM启动时,虚拟机会创建一个Launcher类的实例,且保证全局单例,而在Launcher的构造函数中初始化了ExtClassLoader和AppClassLoader:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 内部DCL单例创建ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 内部DCL创建AppClassLoader var1(extClassLoader)作为参数传入赋值给parent变量
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 线程上下文加载器
Thread.currentThread().setContextClassLoader(this.loader);
// 省略代码
}
可以看到先初始化了一个extClassLoader,然后将ext作为参数传入了AppClassLoader的构造过程中,其中APP类加载器中的parent父类加载器会赋值为ext类加载器,方便后续的双亲委派。
JVM默认设置线程上下文的类加载器为AppClassLoader。ClassLoader.getSystemClassLoader()返回的也是APP类加载器。
双亲委派机制
由上面的代码可以看到jdk自带的类加载器在逻辑上是有继承父子关系的,而类加载器的加载是存在双亲委派的机制。
双亲委派机制:加载类时会先委托给父类加载器去加载,找不到再委托给上层父类加载器去加载,如果所有父类加载器在自己的加载路径下找不到目标类,则此时才在子类加载器加载目标类。
来看下代码是怎么实现双亲委派机制的:1
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70// AppClassLoader.loadClass方法
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
// 直接委托给super.loadClass方法
return super.loadClass(var1, var2);
}
}
// ClassLoader中的loadClass方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 检查当前类加载器是否已经加载了该类 如果存在直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果此类加载器还有parent类加载器 则委托给父类loadClass
c = parent.loadClass(name, false);
} else {
// parent为null 让引导类加载器去加载 这里相当于extClassLoader的parent是引导类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
// 返回类
return c;
}
}
从代码里可以看到,双亲委派就是委托给parent父类加载器变量来load的,如果parent为null则委托给引导类加载器。如果父类加载器加载失败,会回到子类加载器在findClass方法中根据name加载对应的类。如果自定义类加载器想符合双亲委派机制,可以直接重写findClass这个方法,且默认自定义类加载器的父加载器是AppClassLoader,这样自定义类加载器会遵循双亲委派机制来加载类。
双亲委派的优点
- 沙箱安全机制:自己写的java.lang.String类不会被加载,因为优先委托给引导类加载器来加载,只会加载rt.jar中的核心api,这样可以防止核心API被篡改。
- 避免类的重复加载:父类加载器已经加载过的类,因为双亲委派不会被加载第二次。保证加载类的唯一性。
双亲委派机制的打破
双亲委派机制本身也存在限制和缺点。双亲委派使得类有了层级划分(跟随类加载器),越基础的类越上层的类加载器去加载,但是如果基础的类要调用用户写的类时(不在加载基础类加载器包路径下),这个模型就不灵活。或者比如在Tomcat这种容器中,想实现不同war包之间的隔离和部分共享,双亲委派机制也不能满足。所以需要打破双亲委派机制。
打破方式:
- 利用线程上下文类加载器来加载对应类。比如SPI的应用,JNDI或者JDBC。对应JNDI来说,基础的资源查找和管理都在rt.jar中由启动类加载器加载,而其需要加载的spi文件有的在classpath里,这时就依赖线程上下文类加载器来加载,打破了双亲委派机制。
- 自己实现自定义的类加载器(默认的parent为应用程序类加载器),重写loadClass方法,不委托对应的parent去加载。
Tomcat打破双亲委派的机制
Tomcat作为web容器要解决的问题:
- web容器可能要部署不同的war包,要依赖三方包不同的版本,不能要求同一个类只加载了一次,要实现war包之间的隔离。
- 可以共享的类在不同的war包也不应该加载10次,有web程序可共享的类。
- server自己和web程序依赖的类应该是隔离的,为了安全。
- web容器要支持jsp的修改,支持jsp的热更新。
显然,默认的双亲委派机制是支持不聊tomcat这个需求的。第一点和第三点其实都是要多个相同全限定类名的类加载多次实现隔离,这显然不能去双亲委派。而jsp的要求热更新,jsp对应的class文件在加载之后变更,可以卸载jsp的类加载器,再重新去生成一个类加载器去加载对应的jsp文件实现热更新。
在tomcat中有三组目录(/common/、/server/、/shared/*),分别对应所有共享、server容器自己所用、web程序共享,当然每个web程序都有自己的/WEB-INF/目录来存放自己的web应用程序的资源。tomact的类加载器依赖了这些目录的功能,如下: