JVM类加载机制——类的生命周期

什么是类加载机制?

虚拟机把描述类的数据文件(字节码)加载到内存,并对数据进行验证、准备、解析以及类初始化,最终形成可以被虚拟机直接使用的java类型(java.lang.Class对象),这就是java虚拟机的类加载机制。——《 深入理解java虚拟机》

类的生命周期

从类被加载进内存开始直到卸载出内存为止,类的生命周期包括装载、验证、准备、解析、初始化、使用和卸载 7个过程,其中验证、准备、解析三个过程统称为链接。

这7个过程会按顺序开始,但无需等到上一个过程结束下一个过程才能开始运行,实际上他们通常都是相互交叉混合着运行。

在Java中,类的加载和链接过程都是在程序运行期间完成的。另外,Java可以动态扩展的语言特性就是依赖运行期间动态加载、动态链接这个特点实现的。

接下来我们详细了解下类的整个生命周期

一、装载(加载)

在加载阶段,虚拟机完成3件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。

    二进制字节流除了从Class文件中获取,还可以从哪些地方获取?

    从ZIP或jar包中读取
    从网络中获取
    运行时计算生成(Java动态代理技术)
    由其他文件生成(由JSP文件生成对应的Class类)
    从数据库中读取
    复制代码
  2. 加二进制字节流存储在方法区中(按照虚拟机所需的格式存储)。

  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。Class对象比较特殊,存放在方法区中(HotSpot虚拟机)

二、验证

验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
一般包括两个方面:

  1. 格式语义校验:
    例如:
    1.是否以0xCAFEBASE开头
    2.主、次版本号是否在当前虚拟机处理范围内
    复制代码
  2. 代码逻辑校验

三、准备

准备阶段正式为静态变量分配内存并设置初始值,这些静态变量在方法区中分配内存。

注意:

  1. 准备阶段,JVM只会为静态变量(static修饰)分配内存,不包括实例变量,实例变量将会在对象实例化时随对象一起分配在Java堆中。

    准备阶段,只会为value分配内存,不会为name分配内存
    public static int value = 123;
    private String name = "Tom";
    复制代码
  2. 设置初始值:

  • static修饰的变量(无final):零值或null

准备阶段,未执行任何Java方法,而value赋值为123指令是程序编译后,存放于类构造器方法中,在初始化阶段才会执行,因此准备阶段,会设置零值。

准备阶段,会设置零值
public static int value = 123;
复制代码
  • static final修饰的常量:实际值
常量,准备阶段会设置实际值123
public static final int value = 123;
复制代码

注意:static final修饰的基本数据类型或者String会在javac编译时生成ConstantValue属性,在类加载的准备阶段直接把ConstantValue的值赋给该字段。可以理解为在编译期即把结果放入了常量池中。所以当A类调用B类的static final字段(基本数据类型或者String),不会触发B类的加载。

四、解析

解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程
(在某些情况下可以在初始化阶段之后开始)
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四种常量类型。

1、类或接口的解析 :判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。

2、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系递归搜索其父类,直至查找结束.

3、类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。

4、接口方法解析:与类方法解析步骤类似,只是接口不会有父类,因此,只递归向上搜索父接口就行了。

看如下例子:

class Super{
public static int m = 11;
static{
System.out.println("执行了super类静态语句块");
}
}
 
class Father extends Super{
public static int m = 33;
static{
System.out.println("执行了父类静态语句块");
}
}
 
class Child extends Father{
static{
System.out.println("执行了子类静态语句块");
}
}
 
public class StaticTest{
public static void main(String[] args){
System.out.println(Child.m);
}
}
复制代码

输出结果:

执行了super类静态语句块
执行了父类静态语句块
33
复制代码

为什子类的static块不会执行?

static块是在初始化阶段执行的,而static变量发生在静态解析阶段,也即是初始化之前,此时已经将字段的符号引用转化为了内存引用,也便将它与对应的类关联在了一起,由于在子类中没有查找到与m相匹配的字段,那么m便不会与子类关联在一起,因此并不会触发子类的初始化。
复制代码

同理,如果注释掉Father类中对m定义的那一行,则输出结果如下:

执行了super类静态语句块
11
复制代码

五、初始化

初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器

GankRobot转载声明

原文出处:掘金Androidd

原文作者:酸辣汤

原文地址:https://juejin.im/post/5d634864e51d4561ee1bdf8c