# 类的生命周期
加载
、 验证
、 准备
和 初始化
这四个阶段发生的顺序是确定的,而 解析
阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定 (也成为动态绑定或晚期绑定)。
这些阶段知识按顺序开始,并不是按顺序结束,这些阶段往往互相交叉混合进行,再一个阶段执行过程中调用或激活另一个阶段。
# 类的加载
首先介绍几个概念:
- 方法区:JVM 实例内部,类型信息被存储在方法区的内存逻辑区中。类型信息是由类加载时从类文件中提取出来的。方法区又叫做静态区,被所有线程共享,方法区包含所有
class
和static
。静态常量存放在方法区的常量区中,这之后会讲解。
加载阶段虚拟机需要完成:
- 通过类的全限定名(从方法区)来获取其定义的二进制字节流。
- 字节流代表的静态存储结构转化为方法区的运行时数据结构。
- 在 Java 堆中生成代表这个类的
java.lang.Class
对象,作为对方法区中这些数据的访问入口。
类加载器不需要等待某个类首次主动使用时在加载,JVM 允许预料某个类需要使用时主动加载。在预加载中遇到.class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才会报错误 --LinkageError。
# 连接
# 验证
确保被加载的类的正确性,也就是 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
如果使用纯 Java 代码编写的类,编译出来的 class 文件是相对安全的。但是.class 文件并不只是由 Java 源码编译而来,哪怕用键盘输入 01 并将文件后缀改为.class 都可以,所以验证非常有必要。
验证阶段的检验动作:
- 文件格式验证:验证字节流是否符合 Class 文件格式的规范。(魔数,主次版本,常量的 tag 标志等)
- 元数据验证:对字节码描述的信息进行语义分析。(该类是否继承不可继承的 final 类,是否覆盖父类的 final 字段等)
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用
-Xverifynone
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
# 准备
正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
- 内存分配的仅包括静态变量,不包括实例变量。实例变量是在对象实例化时随着对象一块分配在 Java 堆中。
- 初始值:
0,0L,null,false
等。
比如某个类由以下语句:
public static int value = 3; |
在准备阶段只会赋值 0,将 value
赋值为 3 的 put static
指令存放于 <clinit()>
方法中。所以初始化阶段才会赋值为 3。
<clinit>
方法是类加载的初始化过程中,编译器按语句在源文件中出现的顺序,依次自动收集类中的静态变量的赋值动作和静态代码块中的语句合并产生 <clinit>
方法。并且 <clinit>()
不需要显式调用父类(接口除外,接口不需要调用父接口的初始化方法,只有使用到父接口中的静态变量时才需要调用)的初始化方法 <clinit>()
,虚拟机会保证在子类的 <clinit>()
方法执行之前,父类的 <clinit>()
方法已经执行完毕。
随便写个类:
public class Test { | |
public static int i = 10; | |
private int a ; | |
} |
编译后通过 jclasslib
插件查看:
当 int 取值 -128~127 时,JVM 采用
bipush
指令将常量压入栈中。
还需要注意几点:
- 对于同时被
static
和final
修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;只被 final 修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值。
public class Test { | |
final int tmp; | |
} |
此时编译会报错:java: 变量 tmp 未在默认构造器中初始化。
必须在构造器中将 final
变量初始化:
public Test(int k) { | |
this.k = k; | |
} |
- 同时被
static
和final
修饰的常量,类会为它生成ConstantValue
属性,在准备阶段 JVM 就会根据ConstantValue
的设置将变量赋值。
# 解析
JVM 将常量池内的符号引用替换为直接引用的过程,解析动作主要针对 类
或 接口
、 字段
、 类方法
、 接口方法
、 方法类型
、 方法句柄
和 调用点
限定符 7 类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用
就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
# 初始化
初始化,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。
也就是
<clinit>
方法。
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
创建类的实例,也就是 new 的方式
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法(也经常和第二点合并在一起)
反射 (如 Class.forName ("com.pdai.jvm.Test"))
初始化某个类的子类,则其父类也会被初始化
Java 虚拟机启动时被标明为启动类的类 (Java Test),直接使用 java.exe 命令来运行某个主类
只有这六种是主动使用,其他的都是被动使用,被动使用不会初始化。被动引用的例子可以看这篇博客。
# 注意
关于访问类的静态变量,一定要注意这里写的是变量,变量,变量(重要的是强调三遍)。 static final int a = 10;
是常量,常量,常量。访问 a
不会让类被加载。
public class Main { | |
public static void main(String[] args) { | |
System.out.println(Test.str); | |
} | |
} |
写一下 Test
类:
public class Test{ | |
static { | |
System.out.println("我被初始化了!"); | |
} | |
public final static String str = "都看到这里了,不给个三连+关注吗?"; | |
} |
先编译一下,看一下 Main.class
的内容:
public class Main { | |
public Main() { | |
} | |
public static void main(String[] args) { | |
System.out.println("都看到这里了,不给个三连+关注吗?"); | |
} | |
} |
字节码文件中,将 Test.str
直接替换成了字符串,那么执行时和 Test
也就没有任何关系了,自然也不会实例化 Test
。
当 int 取值 -2147483648~2147483647 时,JVM 采用
ldc
指令将常量压入栈中。
# 卸载
JVM 将结束生命周期的几种情况:
- 执行了 System.exit () 方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致 Java 虚拟机进程终止
# 参考
https://blog.csdn.net/zhangliangzi/article/details/51319033
https://pdai.tech/md/java/jvm/java-jvm-classload.html
https://www.yuque.com/qingkongxiaguang/javase/keopmg#ddafc876
https://www.cnblogs.com/wangguoning/p/6109377.html
《深入理解 Java 虚拟机》第三版
https://www.cnblogs.com/czwbig/p/11155555.html