JVM系列-类的加载

类加载

从class文件到内存中的类,按先后顺序需要经过加载、链接、初始化三个步骤,内存中的类没有经过初始化是不能使用的。

加载过程

加载的过程就是查找字节流的过程,对于数组类来说没有对应的字节流,是由Java虚拟机直接生成的,对于其他类来说则需要借助类加载器完成查找字节流的过程。

加载器和双亲委派模型

所有的类都有个顶级类加载器BootstrapClassLoader,启动类加载器由 C++ 实现,没有对应的 Java 对象所以在 Java中只能用null来指代。

除了启动类加载器,其他类加载器都是 java.lang.ClassLoader的子类,因此有对应的 Java 对象,这些类加载器需要由另一个加载器比如启动类加载器加载到 Java 虚拟机中,才能执行类加载

类加载的过程中,先检查是否已经加载过,若没有加载则调用父类加载器的loadClass()方法,每一层的类加载器都是如此,所有的请求都会传送到顶层的类加载器中,父加载器反馈自己无法完成这个加载请求后,子加载器才会尝试自己去加载。若父类加载器为空则默认使用启动类加载器作为父加载器。若父加载失败,抛出ClassNotFoundException异常后,调用自己的findClass()方法进行加载。

双亲委派模型作用:

比如现在黑客自己定义了一个java.lang.String类,该String类和系统的String类有一样的功能,可能只是该其中的一些东西,加入一些病毒代码,没有双亲委派模型,JVM 可能就会认为黑客自己定义的java.lang.String类才是系统的String类,但是在双亲委派模型下,最顶端的类加载器加载系统的java.lang.String,自定义的类加载器无法加载java.lang.String类。

加载器

Java9之前,启动类加载器负载加载最基础、最重要的类,比如存放在JRElib目录下的jar包中的类(还有虚拟机参数-Xbootclasspath指定的类),除了启动类加载器之外,另外两个重要的类加载器是扩展类加载器(extension class loader)和应用类加载器(application class loader),均由Java核心类库提供。

链接

链接指的是将创建成的类合并到Java虚拟机中,让他能够执行的过程,可分为验证、准备以及解析三个阶段。

验证

验证阶段的目的,在于确保被加载的类能够满足Java虚拟机的约束条件,就好比做一件事可能需要交给上级审核之后才能才能继续下面的工作,通常来说Java编译器生成的类文件必然满足于Java虚拟机的约束条件。

准备

准备阶段的目的则是为加载的静态字段分配内存,Java代码对具体静态字段的初始化会放到初始化阶段,除了分配内存以外,部分Java虚拟机还会在此阶段构造其他类层次相关的数据结构,比如实现虚方法的动态绑定的方法表。

比如

1
private static int value=123;

那变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。

解析

在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。

举例来说,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。

解析阶段的目的,正是将这些符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。

初始化

在 Java 代码中,如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中对其赋值。

如果直接赋值的静态字段被final修饰,并且它的类型是基本类型或字符串时,那么这个字段会被Java编译器标记为常量值,初始化直接由Java虚拟机完成,除此之外的直接赋值操作,和静态代码块中的代码,都会被Java编译器放在<clinit>()方法中。

初始化就是为标记为常量值的字段赋值,和执行<clinit>()方法的过程,Java虚拟机会通过加锁来确保<clinit>()方法执行一次。

初始化完成之后,类才正式成为可执行的状态。JVM 中类的初始化何时会被触发呢?

  • 虚拟机启动时,初始化用户指定的主类
  • 用到new xxx类的时候,初始化new指令的目标类
  • 遇到调用静态方法的指令,初始化静态方法所在的类
  • 遇到访问静态指令的时候,初始化静态指令所在的类
  • 子类的初始化会触发父类的初始化
  • 如果一个接口定义了default方法,直接实现或间接实现这个接口的类的初始化,也会触发这个接口的初始化
  • 使用反射API对某个类进行反射调用的时候,初始化这个类
  • 初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类

双亲委派模型详解

我们先写出如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DemoApplication {

public static void main(String[] args) {
//当前类的加载器
ClassLoader cur = DemoApplication.class.getClassLoader();
System.out.println(cur);
//cur的父加载器
ClassLoader cur1 = cur.getParent();
System.out.println(cur1);
//cur1的父加载器
ClassLoader cur2 = cur1.getParent();
System.out.println(cur2);
}

}

输出结果如下:

1
2
3
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@73c6c3b2
null

类加载器这个体系结构中,最高的一层也就是Bootstrap,在JVM启动时创建,通常由操作系统相关的本地代码实现。是最根基的类加载器,它负责将 <JAVA_HOME>/lib 路径下的核心类库或 -Xbootclasspath 参数指定的路径下的jar包加载到内存中

第二层是Extension ClassLoader(JDK9以后是Platform ClassLoader),它负责加载 <JAVA_HOME>/lib/ext 目录下或者由系统变量 -Djava.ext.dir 指定位路径中的类库

第三层是Application ClassLoader的应用类加载器,主要是加载用户定义的CLASSPATH路径下的类。

只有Bootstrap不是由Java语言实现的

classloader

双亲委派机制的具体实现

低层次的类加载器不能覆盖高层次类加载器已经加载的类,如果低层次类加载器想加载一个还没被加载过的类,需要逐级向上询问这个类是否已经被加载,被问到的类加载器会看看自己是否已经加载过这个类,如果没有他要问自己的父加载器自己能不能加载这个类,父加载器也会看自己有没有加载过这个类,并且问父加载器的父加载器自己能不能加载这个类,直到问到Bootstrap ClassLoader,然后再逐级乡下尝试能不能加载这个类,直到问到发起类加载请求的类加载器。

源代码如下:

在java.lang.ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

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
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) {
c = parent.loadClass(name, false);
} else {
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();
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;
}
}

双亲委派机制的优势

系统的安全性:比如String类,存放在rt.jar中,无论哪一个类加载器要去加载这个类,一定最后会让Bootstrap进行加载,防止有心人进行恶意替换。

自定义类加载器

继承 ClassLoader,重写 findClass() 方法,调用 defineClass() 类。

代码如下,我在 /home/gzr/ 目录下放了个 Hello.classHello.java 代码如下。使用 javac 命令编译了一下。

1
2
3
4
5
public class Hello {
public static void sayHello(){
System.out.println("Hello,I am ....Iron Man?");
}
}

自定义类加载器代码如下:

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
package com.gzr.classloader;

import java.io.*;

public class CustomClassLoader extends ClassLoader{

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);

if(result == null) {
throw new FileNotFoundException();
} else {
return defineClass(name, result, 0, result.length);
}
} catch (Exception e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}

private byte[] getClassFromCustomPath(String name) {
//假定class的位置
String path = "/home/gzr/" + name + ".class";

byte[] data=null;
try (InputStream in = new FileInputStream(new File(path));
ByteArrayOutputStream out=new ByteArrayOutputStream()){

int len=0;
while(-1!=(len=in.read())){
out.write(len);
}
data=out.toByteArray();

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return data;
}

public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();

try {
Class<?> clazz = Class.forName("Hello", true, customClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}

}
}

执行结果如下:

1
com.gzr.classloader.CustomClassLoader@27c170f0

为什么要自定义类加载器?

隔离加载类

在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境,比如阿里的有些容器框架就是通过自定义类加载器来确保应用中以来的jar包不会影响到中间件运行的jar包。

一般中间件都会有自己的jar包,同一个工程引用多个框架的时候,导致某些类存在包路径、类名相同的情况,引起类的冲突,导致应用程序异常。主流的容器类框架都会自定义类加载器,实现不同中间件之间的类隔离,有效避免类冲突。

修改类加载方式

类的加载模型并非强制,除Boostrap外,其他的加载并非一定要引入,可以根据实际情况在某个时间点按需动态加载。

扩展加载源

可以从数据库,网络等地方进行加载类

防止源码泄露

Java代码可能会被篡改,可以进行编译加密,那么类加载也要自定义才能进行还原加密的字节码了。