类加载机制详解

类是在 ** 运行期间第一次使用 ** 时动态加载的,而不是编译时期一次性加载。因为如果在编译时期一次性加载,那么会占用很多的内存

1. 类的生命周期

image.png
包括以下 7 个阶段:

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

2. 类加载过程

包含了加载、验证、准备、解析和初始化这 5 个阶段

2.1. 加载

加载过程完成以下三件事:

  1. 获取二进制字节流: 通过一个类的全限定名来获取定义此类的 二进制字节流
    1. 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础
    2. 由其他文件生成,例如由 JSP 文件生成对应的 Class 类
    3. 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流
    4. 从网络中获取,最典型的应用是 Applet
  2. 存储结构转化: 将这个字节流所代表的 ** 静态存储结构 ** 转化为方法区的 运行时存储结构
  3. 生成 Class 对象: 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口

2.2. 验证

概述: 确保 Class 文件的字节流中包含的信息 符合当前虚拟机的要求(如 cafe babe),并且不会危害虚拟机自身的安全

2.3. 准备

概述: 准备阶段为 ** 静态变量 ** 分配内存并 设置初始值,使用的是 ** 方法区 ** 的内存

  • 实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在堆中

  • 初始值一般为 0 值,例如下面的静态变量 value 被初始化为 0 而不是 123

    public static int value = 123;

  • 如果静态变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0

    public static final int value = 123;

2.4. 解析

概述:
将常量池的符号引用替换为 直接引用 的过程
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定
符号引用:
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中
直接引用: 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在

2.5. 初始化

概述:
初始化阶段即虚拟机执行 类构造器 () 方法的过程
初始化阶段才真正开始执行类中定义的 Java 程序代码
特点:

  • 静态初始化: 编译器自动收集类中所有 静态变量的赋值动作和静态语句块中的语句,由 class 类构造器对 ** 静态变量 静态代码块 ** 进行 ** 初始化 **。编译器收集的顺序由语句在源文件中出现的顺序决定。

    • 如果一个类中不包含静态语句块,也没有对静态变量有赋值操作,编译器可以不为该类生成 () 方法。
    • 静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
      1
      2
      3
      4
      5
      6
      7
      public class Test {
      static {
      i = 0; // 给变量赋值可以正常编译通过
      System.out.print(i); // 这句编译器会提示“非法向前引用”
      }
      static int i = 1;
      }
  • 先父后子: 与类的构造函数(或者说实例构造器 ())不同,() 不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 () 方法运行之前,父类的 () 方法已经执行结束。因此虚拟机中第一个执行 () 方法的类肯定为 java.lang.Object。这也意味着父类中定义的 静态语句块的执行要优先于子类

  • 接口: 执行接口的 () 方法不需要先执行父接口的 () 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 () 方法

  • 加锁等待: 如果多个线程同时初始化一个类,只会有 ** 一个线程执行 ** 这个类的 () 方法,其它线程都会阻塞等待,直到活动线程执行 () 方法完毕。如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽

3. 类初始化时机

3.1. 主动引用

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):

  • 当虚拟机启动时,用户需要指定一个要执行的 入口类(包含 main () 方法的那个类),虚拟机会先初始化这个主类
  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:
    • 使用 new 关键字实例化对象的时候
    • 读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候
    • 以及调用一个类的静态方法的时候
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要 ** 先触发其父类 ** 的初始化
  • 使用 java.lang.reflect 包的方法对类进行 ** 反射调用 ** 的时候,如果类没有进行初始化,则需要先触发其初始化
  • 当使用 JDK 1.7 的 ** 动态语言 ** 支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

3.2. 被动引用

以上 5 种场景中的行为称为对一个类进行主动引用
除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:

  • 通过子类 引用父类的静态字段,不会导致子类初始化

    1
    2
    // value 字段在 SuperClass 中定义
    System.out.println(SubClass.value);
  • 通过 数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法

    1
    SuperClass[] sca = new SuperClass[10];
  • ** 常量 ** 在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

    1
    System.out.println(ConstClass.HELLOWORLD);

4. 类与类加载器

两个类相等需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间
这里的相等,包括类的 Class 对象的 equals () 方法、isAssignableFrom () 方法、isInstance () 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true

4.1. 类加载器分类

从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:

  • ** 启动类加载器(Bootstrap ClassLoader): ** 这个类加载器用 C++ 实现,是虚拟机自身的一部分
  • 所有其他类的加载器: 这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader

从 Java 开发人员的角度看,类加载器可以划分得更细致一些:

  • 启动类加载器(Bootstrap ClassLoader):
    • 此类加载器负责将存放在 \lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中
    • 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可
  • 扩展类加载器(Extension ClassLoader):
    • 这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 /lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器
  • 应用程序类加载器(Application ClassLoader):
    • 这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader () 方法的返回值,因此一般称为系统类加载器
    • 它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

4.2. 双亲委派模型

应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器
下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)
该模型要求除了顶层的启动类加载器外,其余的类加载器都应 有自己的父类加载器 。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现
image.png
工作过程:
一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载
** 优点:** 使得 Java 类随着它的类加载器一起具有一种带有 优先级的层次关系,从而使得基础类得到 统一

  • 共享功能: 可以避免重复加载,当父亲已经加载了该类的时候,子类不需要再次加载,一些 Framework 层级的类一旦被顶层的 ClassLoader 加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。
  • 隔离功能: 主要是为了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String ,同时也避免了重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就是不同的两个类,如果相互转型的话会抛 java.lang.ClassCaseException 。

例: java.lang.Object 存放在 rt.jar(使用启动类加载器)中,如果编写另外一个 java.lang.Object 的类(应用程序类加载器)并放到 ClassPath 中。
由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
实现:
以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass () 方法运行过程如下:
先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。

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
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

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) {
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.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}

4.3. 自定义类加载器实现

FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。
它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass () 方法来把这些字节代码转换成 java.lang.Class 类的实例。

java.lang.ClassLoader 的 loadClass () 实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,但是需要重写 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
38
39
40
public class FileSystemClassLoader extends ClassLoader {

private String rootDir;

public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}

new 关键字创建对象的过程:
image.png