类加载机制

一. 虚拟机类加载机制

1. 概述:

把描述类的数据从class文件加载到内存,对数据进行校验、转换和初始化,最终形成可被虚拟机使用的java类型。

2. 过程

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段),而解析阶段则不一定(它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。

验证、准备、解析称为连接Linking阶段。

加载 验证 准备 初始化 卸载 需要按顺序开始

3. 初始化

有且只有5种情况必须立刻初始化的:

  1. 使用new实例化对象,读取或设置一个类的静态字段,调用静态方法(不包括常量)
  2. 使用java.lang.reflect包对类进行反射调用,如果没有进行类初始化,则需要先触发其初始化
  3. 当初始化一个类的时候,如果发现其父类还没初始化,要先触发其父类的初始化
  4. 虚拟机启动时,需要指定一个要执行的主类
  5. java7动态语言支持,一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、 REF_putStatic、 REF_invokeStatic方法句柄,并且方法句柄对应的类没有进行过初始化,需要先触发其初始化。(没用过不是很理解)

以上五种引用类的方法称为主动引用,除此之外都是被动引用。
比如:

  1. 通过子类调用父类的静态字段。
  2. 通过数组定义来引用类

接口初始化

接口的加载过程与类的加载过程稍有不同。接口中不能使用static{}块。当一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有真正在使用到父接口时(例如引用接口中定义的常量)才会初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
class superClass{
static {
System.out.println("123");
}
}

public class Demo {

public static void main(String[] args) {
superClass[] a = new superClass[2];
}
}
///会触发一个superClass一维数组类的初始化
  1. 引用常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.fenixsoft.classloading;

/**
* 被动使用类字段演示三:
* 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
**/
public class ConstClass {

static {
System.out.println("ConstClass init!");
}

public static final String HELLOWORLD = "hello world";
}

/**
* 非主动使用类字段演示
**/
public class NotInitialization {

public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}

PS:接口的不同,在于第三种,不要求其父接口全部完成了初始化,只有在使用的时候才会初始化。

二. 类加载过程

1. 加载

  1. 通过一个全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成代表这个类的java.lang.Class对象

加载阶段即可以使用系统提供的类加载器在完成,也可以由用户自定义的类加载器来完成。加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

总结

就是加载类的二进制资源,转为相应的方法区数据结构生成Class对象

2. 验证(连接阶段)

确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机。

  1. 文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

  1. 元数据验证

对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。

  1. 字节码验证

主要工作是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为

  1. 符号引用验证

发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析阶段”中发生。
使用-Xverify:none关闭大部分反复使用的代码验证

3. 准备(连接阶段)

正式为类变量分配内存并设置类变量的初始值。仅仅包括类变量(static)不包括实例变量,
这里的初始值是0,而不是实际值。

1
public static int value = 123;

value=123在类构造器的方法中,在初始化阶段才会执行。
若在字段属性表中存在ConstantValue属性,那么在准备阶段就会初始化为所指定的值

1
public staic final int value = 123;

在准备阶段就会将value赋值为123

4. 解析(连接阶段)

将常量池符号引用替换为直接引用

  • 符号引用:内存布局无关,不一定已经加载到内存中
  • 直接引用:内存布局相关,如果有那么一定在内存中存在

符号引用:

  1. 类和接口的全限定名
  2. 字段的名称和描述符
  3. 方法的名称和描述符

5. 初始化

真正开始执行类中定义的java代码,初始化阶段执行类构造器
自动收集类变量的赋值和静态语句块,按照出现顺序决定,优先执行父类

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

static {
i = 20;//可以赋值
System.out.println(i);//不能引用
}
static int i = 10;

public static void main(String[] args) {
System.out.println(Demo.i);

}
}

加载顺序可以参考这个图

三. 类加载器(热部署)

只有同一个类加载器加载出来的类,才是真正意义上的相等。

  1. 启动类加载器bootstrap
    负责将存放在\lib目录或者-Xbootclasspath参数所指定的路径中的类库加载到虚拟机内存中。
  2. 扩展类加载器extension
    \lib\ext目录or java.ext.dirs系统变量指定路径的类库

  3. 应用程序类加载器application(系统类加载器)
    加载classpath指定类库

    1
    2
    3
    4
    5
    graph BT
    扩展类加载器-->启动类加载器
    应用程序类加载器-->扩展类加载器
    自定义类加载器-->应用程序类加载器
    自定义类加载器-->应用程序类加载器

双亲委派模型

除了启动类加载器外,所有的类加载器都有自己的父类加载器,而且是通过组合而不是继承的关系来实现。这样可以保证应用程序的稳定。比如所有的java.lang.object都是通过启动类加载器加载rt.jar中的object实现的

破坏双亲委派模型实现热部署

每一个程序模块都有一个自己的类加载器,当需要更换一个模块时,就把模块连同类加载器一起换掉以实现代码的热替换。