类的生命周期
类生命周期
概述
类的生命周期可以大致分为加载
、连接
、初始化
、使用
、卸载
,其中连接阶段又可以细分为验证
、准备
、解析
这三个阶段。其中作为程序员,接触最多的是使用阶段,例如:通过反射或 new 的方式获得一个新对象。
加载阶段
- 类加载器会根据类的全限定名通过不同的渠道(本地文件、动态代理生成、网络传输)以二进制流的方式获取字节码信息。
- JVM 会在类加载器将类加载完之后,把字节码中的信息保存到方法区。
- JVM 会将字节码的信息保存到内存的方法区中,此时会生成一个 InstanceKlass 对象,保存类的所有信息,包含了实现特定功能的例如多态的信息。
- 同时,JVM 还会在堆中生成一份与方法区中数据类似的 java.lang.Class 对象,其作用是在 Java 代码中去获取类的信息以及存储静态字段的数据(在 JDK 8之后)。
对于程序开发人员,其只需要访问堆中的 Class 对象而不是访问方法区中的所有信息。JVM 可以很好的控制开发者的访问数据的范围。
查看内存中的对象
先新建一个程序并执行
import java.io.IOException;public class Hello {public static final int num = 0;public static void main(String[] args) throws InterruptedException, IOException {Hello hello = new Hello();hello.hello();// 防止程序执行后直接退出System.in.read();}public void hello() {System.out.println("Hello!");}
}
可以使用JDK自带的hsdb工具查看 Java 虚拟机内存的信息。工具位于 JDK 安装目录的 lib 文件中的 sa-jdi.jar 中。由于该jar内部有多个启动程序,所以需要指定其中的 HSDB 进行启动
java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
注意:上述启动方法仅限于 JDK 8及以下版本,在8之上的版本,在 lib 目录下是无法找到 sa-jdi.jar 这个文件的,这是因为jhsdb
工具在 JDK 17 中已经取代了以前的 hsdb
工具。
如果已经配置JAVA_HOME
环境变量,可以直接使用下面命令启动
jhsdb hsdb
若没有配置环境变量,则需要将jhsdb替换成jdk所在bin目录下的jhsdb.exe,用双引号包围起来,例如"C:\Program Files\Java\jdk-17\bin\jhsdb.exe" hsdb
启动成功之后会出现一个白色的窗口

此时点击 File ,再点击 Attach to HotSpot Process…连接某个指定的 Java进程。如果不知道运行的程序的进程号是多少,可以使用jps命令在cmd窗口进行查看
输入要查看的进程号,然后点击ok

然后点击 Tools ,再点击 Object Histogram

就会来到一个搜索页面,在输入框内输入类全限定名,按下 ENTER 键即可查询到运行的程序

双击即可进入对象内部,再点击 Insepct 进行查看

找到两处标红的地方,分别对应的就是上面描述的方法区对象与堆区对象。其中的num对应的是java中所定义的静态变量。

连接阶段
- 验证:验证内容是否满足“Java 虚拟机规范”,若不满足则不允执行,防止危害 JVM
- 准备:给程序中的静态变量赋初始值
- 解析:将常量池中的符号引用替换成指向内存的直接引用
验证
该阶段主要检测 Java 字节码文件是否遵守规范中的约束。该阶段一般不需要程序员进行参与。
验证主要会包含以下四个部分:
- 文件格式验证,比如文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本要求。
- 元信息验证,例如类必须有父类(super不能为空),即使程序员不主动继承父类,Java也会默认让所有类都统一继承 Object 类。
- 验证程序执行指令的语义,比如方法内的指令执行中跳转到不正确的位置。
- 符号引用验证,例如是否访问了其他类中的private方法等。
准备
该阶段会给静态变量分配相应的内存并设置默认的初始值。
数据类型 | (默认)初始值 |
---|---|
int | 0 |
long | 0L |
short | 0 |
char | ‘\u0000’ |
byte | 0 |
boolean | false |
double | 0.0 |
引用数据类型 | null |
凡是都有个但是,如果该静态变量是由final修饰的基本数据类型,其在准备阶段会将代码中的值进行赋值,并且要求被final修饰的静态变量必须赋予初始值,否则编译器就会报错。
解析
该阶段主要就是将常量池找中的符号引用替换为直接引用,即不再使用编号,而是直接使用内存中的地址进行访问具体的数据
初始化阶段
该阶段会执行静态代码块中的代码,并将为静态变量赋值,并执行字节码文件中的 clinit
部分的字节码指令。一个类一般只会加载和初始化一次。
下面的几种方式会触发类的初始化:
- 访问一个类的静态变量或静态方法,注意其变量若是被final修饰且等号右边是常量则不会被初始化。
- 调用
Class.forName(String className)
方法(注意,其重载方法Class<?> forName(String name, boolean initialize, ClassLoader loader)
,第二个参数可以传值控制类是否需要初始化)
- 通过new的方式去创建一个该类的对象时。
- 执行 Main 方法的当前类。
- 子类的初始化 clinit 调用之前,会优先调用父类的 clinit 初始化方法。
- final 修饰的变量赋值为非常量时,会执行 clinit 方法进行初始化
测试类是否被初始化,可以在运行代码的时候添加-XX:+TraceClassLoading
这行JVM参数将加载并初始化的类打印输出到控制台(-XX:+TraceClassUnloading
将卸载的类打印输出在控制台)。
下面几种情况不会触发类的初始化
- 无静态代码块且无静态变量赋值语句。
- 有静态变量的声明,但无赋值语句。
- 静态变量的定义使用 final 关键字,会导致这些变量在准备阶段便直接初始化。
- 直接访问父类的静态变量,不会触发子类的初始化。
- 数组创建不会导致数组中元素类进行初始化。