类的生命周期
一、类的加载与初始化
类的加载过程主要包含 加载、链接、初始化 三个步骤
1、加载(Load)
1)加载的流程
加载:读取字节码文件的二进制数据,并将其转换为JVM内部的数据结构(如
Class
对象)的过程。
- 通常是通过一个类的全限定名,获取定义此类的二进制字节流。
- 将 二进制字节流 代表的静态存储结构,转化为方法区的运行时数据结构。
- 在堆内存生成一个代表这个类的
java.lang.Class
对象,指向方法区这个类的元数据。java.lang.Class
由 Java类库 提供,任何类型都有一个对应的Class对象,用于存储类型的信息。
2)加载的方式
-
本地文件系统
从本地文件系统中读取类文件进行加载,这是最常见的加载方式。
-
网络下载
从远程服务器下载类文件进行加载,典型场景是Web Applet。
-
压缩包
从 JAR(Java Archive)或 WAR(Web Archive)文件中读取类文件进行加载。
-
运行时计算生成
使用最多的是动态代理技术,通过在运行时生成代理类来动态地加载类。
-
其他文件生成
典型场景是JSP(Java Server Pages)应用,JSP文件会被转换为对应的Java类文件然后加载执行。
-
专有数据库
某些情况下可能会将类文件存储在数据库中,然后从数据库中提取加载。较为少见
-
加密文件
一种保护措施,通过将类文件加密存储,在运行时再解密加载。
3)不同类型的加载
在 Java 中,数据类型分为 基本数据类型 和 引用数据类型。
- 基本数据类型:由虚拟机预先定义。
- 引用数据类型:需要进行类的加载。
数组类的加载
数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要直接创建的,但数组的元素类型仍然是依靠类加载器创建。
- 如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组 A 的元素类型;
- JVM 使用指定的 元素类型 和 数组维度 来创建 新的数组类。
如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为 public。
2、类的链接(Link)
1)验证(Verify)
验证:确保加载的字节码符合当前虚拟机要求,保证被加载类的正确性与安全性。
- 主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
- 字节码的格式是否正确、语义是否符合语法规定、字节码是否可以被JVM安全执行等
2)准备(Prepare)
准备:为类的静态变量
分配内存
并设置默认初始值
的过程。
各类型变量的默认初始值:
数据类型 | 默认初始值 |
---|---|
基本数据类型 - 整数类型 | 0 |
基本数据类型 - 小数类型 | 0.0 |
基本数据类型 - 字符类型 | \u0000(空字符) |
基本数据类型 - 布尔类型 | false |
引用数据类型 | null |
举例说明:
class A {
// 准备阶段:为静态变量a分配4个字节的空间,并设置初始值为0(到初始化阶段才会赋值为10)
static int a = 10;
}
注意:以下情况会在
链接-准备阶段
进行显示初始化(直接赋值,而不是设置默认初始值)
- final修饰的基本数据类型:不涉及 方法调用 与 构造器调用 且 赋的值是编译器可以确定的常量。
- final修饰的String类型:字面量赋值。
在编译时就已经确定的值,会被直接写入到常量池中,此时会在
链接-准备阶段
直接赋予常量池中的值,而不是默认初始值。
下面举例说明:
public static final int INT_CONSTANT1 = 10; // 在链接-准备阶段赋值
public static final int INT_CONSTANT2 = new Random().nextInt(10); // 在初始化阶段<clinit>()中赋值
public static int INT_CONSTANT3 = 1; // 在初始化阶段<clinit>()中赋值
public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100); // 在链接-准备阶段赋值
public static final Integer INTEGER_CONSTANT2 = Integer.valueOf(500); // 在初始化阶段<clinit>()中赋值
public static Integer INTEGER_CONSTANT3 = Integer.valueOf(100); // 在初始化阶段<clinit>()中概值
public static final String s0 = "helloworld0"; // 在链接-准备阶段赋值
public static final String s1 = new String("helloworld1"); // 在初始化阶段<clinit>()中赋值
public static String s2 = "hellowrold2"; // 在初始化阶段<clinit>()中赋值
上述例子中可能比较难理解的是 INTEGER_CONSTANT1
和 INTEGER_CONSTANT2
。
原因是:[-128, 127]
之间256个整数所有的包装对象提前创建好了,放在了整数常量池中。
3)解析(Resolve)
解析:将类中的
符号引用
转换为直接引用
的过程
# 类中的符号引用
就是字节码中的原始信息,比如类名,方法名,变量名等
主要针对 类或接口、字段、类方法、接口方法、方法类型、方法句柄 和 调用限定符 7类符号引用进行。
# 直接引用
运行时内存中相关的地址引用
# 说明
调用一个方法时,在java语言层面就是一个方法名这个符号,
但是在底层应该要根据这个符号找到内存中的直接地址来调用。
3、类的初始化(Initial)
初始化:静态变量的初始化(赋值操作)、静态代码块中语句的执行
- 如果初始化一个类的时候,其父类尚未初始化,则优先初始化父类。
- 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
class A {
// 初始化阶段:才会将10赋值给a(在此之前都是默认值)
static int a = 10;
}
这个阶段其实是 <clinit>
方法执行过程的体现。这个方法中的逻辑,就是当前类中 静态变量 和 静态代码块 初始化逻辑的整合。
1)<clinit>方法
一个类编译之后,字节码文件中会产生一个类构造器方法<cinit>()
,类初始化阶段底层就由该方法完成。
<cinit>()
方法中的逻辑,就是当前类中 静态变量
和 静态代码块
初始化逻辑的整合。
2)初始化顺序
如果有多个静态代码块和静态变量,初始化顺序又会是怎样的呢?
结论:
-
如果
静态变量
只有定义,没有做赋值初始化(如上述案例中的静态变量height
)那么只会设置默认值(
链接-准备
阶段),在<clinit>
方法中不会看到初始化的指令。 -
静态代码块和静态变量直接赋值(如上述案例中的静态变量
age
和name
),按照 编码顺序 从上到下 进行初始化,静态变量的值以最后一次赋值为准。
-
当一个类存在父类时,一定是先对父类进行初始化,然后再初始化子类,因为子类是依赖父类而存在的。
-
如果既没有静态变量的直接赋值,也没有静态代码块,就不会产生
<clinit>
方法
二、类初始化的时机
在Java中,类的使用方式可以分为主动使用
和被动使用
两种情况。
Java 虚拟机规定,一个类或接口在初次使用(指的是主动使用)之前,必须要进行初始化。
1、类的主动使用(加载和初始化)
主动使用
是指:在程序运行过程中,显式地对类进行操作或引用,导致类的加载和初始化。包括以下几种情况:
-
创建类的实例(如:
new
、反射
、clone
、反序列化
) -
使用反射方式对类进行操作(如:
Class.forName("ClassName")
) -
使用 类 的
静态变量(non-final)
或静态方法
。 -
初始化子类时,如果其父类还没有初始化,需要先初始化其父类。
-
启动类。当执行 Java 应用程序的
main方法
时,JVM 首先加载的就是 main方法所在的类。 -
MethodHandle
是 JDK 7 引入的一种轻量级的方法引用机制,可以动态地执行方法。如果
MethodHandle
的REF_getStatic
、REF_putStatic
、REF_invokeStatic
句柄对应的类没有初始化,则初始化。
2、接口的加载
在 Java 中,接口是不会像类一样被初始化的。接口中不允许有静态代码块,因此在加载接口的时候不会执行任何初始化操作。
在 Java 虚拟机中,接口是被动加载的,只有在需要使用接口时才会加载它们,而不是像类那样在引用时加载。
- 当一个类被加载,而该类通过继承或实现关系引用了一个接口时,该接口会被加载。
- 当使用
Class.forName()
方法动态加载类时,如果这个类实现了某个接口,那么该接口也会被加载。 - 当接口的静态字段(即
static final
字段)被使用时,会触发接口的加载。 - 当调用接口中的静态方法时,会触发接口的加载。
总的来说,接口在被其实现类或者其它方式直接引用时会被加载,这是因为Java虚拟机需要检查接口的方法签名以及静态字段的定义。
3、类的被动使用(不初始化)
被动使用
是指:在程序运行过程中,类的加载和初始化是由其它类的主动使用所引起的,而不是因为对类自身的直接引用导致的。
- 除了主动使用,其他的情况均属于被动使用。
- 被动使用不会引起类的初始化,但可能会进行类的加载。
1)定义数组引用
定义数组引用不会触发类的初始化。例如:
public class MyClass {
static {
System.out.println("MyClass 初始化");
}
}
// 在另一个类中定义 MyClass 的数组引用,不会引起类的初始化
public class AnotherClass {
public static void main(String[] args) {
MyClass[] array = new MyClass[5];
}
}
以下情况才会引起类的初始化
array[0] = new MyClass();
2)访问类的静态常量
访问类的静态常量(static final
)不会导致类的初始化,因为常量在链接阶段就已经被显式赋值了。
public class MyClass {
public static final int CONSTANT = 10;
}
// 在另一个类中访问 MyClass 的静态常量,不会引起类的初始化
public class AnotherClass {
public static void main(String[] args) {
System.out.println(MyClass.CONSTANT);
}
}
3)子类引用父类的静态变量
通过子类引用父类的静态变量,不会导致子类初始化,只有真正声明这个字段的类才会被初始化。
class Parent {
static {
System.out.println("Parent类初始化");
}
public static int parentStaticVar = 100;
}
class Child extends Parent {
static {
System.out.println("Child类初始化");
}
}
public class Test {
public static void main(String[] args) {
System.out.println(Child.parentStaticVar); // 只会初始化父类Parent,不会初始化子类Child
}
}
4)ClassLoader的loadClass()
调用 ClassLoader
类的 loadClass()
方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.test.java.Person");
三、对象的创建与初始化
1、对象的创建方式
new
(单例模式、工厂模式、建造者模式等都是其变形)反射
(Class对象 和 Constructor对象的 newInstance方法)clone
(实现 Cloneable 接口,重写 clone 方法)反序列化
:获取一个二进制流,读取文件中序列化的对象数据,还原成内存中的对象。第三方库
:利用一些字节码技术生成对象。例如 Objenesis
2、对象的内存分配
首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果是引用变量,仅分配引用变量空间即可(4个字节)
根据内存是否规整,分为以下两种分配方式:
- 内存规整:JVM 采用
指针碰撞法(Bump The Point)
来为对象分配内存。- 所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器。
- 分配内存就是 把指针移动一段与对象大小相等的距离。
- 内存不规整:JVM 维护一个
空闲列表(Free List)
来为对象分配内存。- 空闲列表记录了哪些内存空间是可用的。
- 分配内存就是 从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。
内存是否规整取决于 JVM 采用的垃圾收集器是否带有压缩整理功能决定。
- 一般使用 基于
复制算法
和标记-整理算法
的垃圾收集器时,会采用指针碰撞法
来分配。 - 一般使用 基于
标记-清除算法
的垃圾收集器时,会采用空闲列表
来分配。
3、内存分配的并发安全
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。
解决这个问题有两种方案:
-
同步处理:
对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性)
-
使用 TLAB(Thread Local Allocation Buffer) 本地线程分配缓冲区
TLAB 是 JVM 在 堆空间的 Eden区 为 每个线程 分配的一块 线程私有的区域,每个线程在各自的 TLAB 上进行内存的分配,避免并发问题的情况下还能够提升内存分配的吞吐量。
只有当 TLAB 空间用完 或 TLAB分配失败 时,才需要进行同步处理。
可以通过 -XX:+/-UserTLAB
参数来设定虚拟机是否使用TLAB(默认是开启的)
4、对象的准备(默认值)
为对象的所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用。
各类型变量的默认初始值:
数据类型 | 默认初始值 |
---|---|
整数类型 | 0 |
小数类型 | 0.0 |
字符类型 | \u0000(空字符) |
布尔类型 | false |
引用数据类型 | null |
5、对象的对象头
将对象的所属类(即类的元数据信息)、对象的 HashCode 和 对象的 GC 信息、锁信息等数据存储在对象的对象头中。
这个过程的具体设置方式取决于 JVM 实现。
6、对象的初始化
执行 <init>
方法(实例变量的初始化、实例代码块中初始化、构造器中初始化)并把堆内对象的首地址赋值给引用变量。
1)<init>方法
我们通过下面这个例子分析一下<init>方法:
从反编译后的结果可以推导出以下结论:
-
每个构造方法都会对应一个
<init>
方法。实例变量
初始化、实例代码块
初始化、构造函数
初始化的逻辑,都会统一的放到<init>
方法完成。 -
每个
<init>
方法内部都会先执行父类的<init>
方法。(默认继承Object,先执行Object.<init>
) -
执行完父类的
<init>
方法后,按编码顺序从上到下再执行 实例变量直接赋值 或 实例代码块。(没有则跳过) -
最后执行构造方法内部的赋值语句。
2)初始化顺序
<init>
方法中,优先执行父类的<init>
方法- 然后按编码顺序从上到下执行 实例变量直接赋值、实例代码块、构造方法内部的赋值语句。
3)类 与 对象 的初始化 混合
一般情况:类初始化 优先于 对象初始化。
特殊情况:类初始化阶段,涉及到本类的对象实例化 —> 先进行对象实例化(混合进行)
package test;
public class A {
// 静态变量
static int a = getA();
// 类初始化阶段,涉及到本类的对象实例化
static A obj = new A();
// 静态方法
private static int getA() {
System.out.print(1);
return 10;
}
// 静态代码块
static {
System.out.print(2);
a = 20;
}
// 实例变量
int b = getB();
// 实例方法
private int getB() {
System.out.print(3);
return 20;
}
// 实例代码块
{
System.out.print(4);
b = 40;
}
// 构造方法
public A() {
System.out.print(5);
}
// main方法
public static void main(String[] args) {
System.out.print(6);
new A();
}
}
【过程分析】
// main方法执行前:类初始化(静态变量)
1
// 类初始化阶段,涉及到本类的对象实例化 -> 先进行对象实例化
3 4 5
// 对象实例化结束 -> 继续进行类初始化(静态代码块)
2
// main方法执行:控制台打印
6
// 创建对象:对象实例化(实例变量 -> 实例代码块 -> 构造方法)
3 4 5
四、对象的内存布局
1、对象头(Header)
对象头包含的内容:
运行时元数据(Mark Word)
- 哈希值(HashCode)
- GC 分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程 ID
- 偏向时间戳
类型指针(Klass Word)
- 指向类元数据 InstanceKlass,确定该对象所属的类型。
- 如果是数组,还需要记录
数组的长度(array length)
。
1)32位虚拟机
普通对象
|------------------------------------------------------------|
| Object Header (64 bits) |
|-----------------------------|------------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|-----------------------------|------------------------------|
数组对象
|-------------------------------------------------------------------------------------------|
| Object Header (96 bits) |
|-----------------------------|------------------------------|------------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) | Array Length (32 bits) |
|-----------------------------|------------------------------|------------------------------|
Mark Word 结构
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
2)64位虚拟机
普通对象
|------------------------------------------------------------|
| Object Header (128 bits) |
|-----------------------------|------------------------------|
| Mark Word (64 bits) | Klass Word (64 bits) |
|-----------------------------|------------------------------|
数组对象
|-------------------------------------------------------------------------------------------|
| Object Header (160 bits) |
|-----------------------------|------------------------------|------------------------------|
| Mark Word (64 bits) | Klass Word (64 bits) | Array Length (32 bits) |
|-----------------------------|------------------------------|------------------------------|
Mark Word 结构
|-------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|-------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|-------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|-------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------------------|--------------------|
2、实例数据(Instance Data)
它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
- 相同宽度的字段总是被分配在一起
- 父类中定义的变量会出现在子类之前
- 子类的窄变量(占用较少字节的变量)可能插入到父类变量的空隙
3、对齐填充(Padding)
64位的操作系统,一次性读取 64bit(8 byte)整数倍的数据。所以 HotSpot 为了高效读取对象,就做了"对齐":
- 如果一个对象实际占用的内存大小不是 8 byte 的整数倍,就"补位"到 8 byte 的整数倍。
所以对齐填充区域的大小不是固定的。
4、对象的内存大小分析
我们以一个 Integer 对象 在 64位虚拟机 中占用的内存大小为例:
- 对象头:Mark Word(8个字节) + Klass Word(8个字节) = 16 字节
- 实例数据:Integer 对象实际存储的数据是一个 int 类型的值(4个字节)
- 对其填充:这种情况下,是4个字节
综上所述,一个 Integer 对象占用的总字节数大致为 24 个字节。
需要注意的是,这只是一个估计值,具体的占用字节数可能会因 JVM 实现、平台和对象内存对齐等因素而有所不同。
5、图解对象的内存布局
public class Customer {
int id = 1001;
String name;
Account acct;
{
name = "匿名客户";
}
public Customer() {
acct = new Account();
}
}
public class Account {}
public class CustomerTest {
public static void main(string[] args){
Customer cust = new Customer();
}
}
五、对象的访问定位
JVM 是如何通过栈帧中的对象引用访问到其内部的对象实例呢?
1、句柄访问
对象引用 指向堆中的句柄,通过句柄来分别访问 对象实例数据(堆)
和 对象类型数据(方法区)
。
- 优点:
- 对象被移动时(GC时很普遍)只要改变句柄中实例数据指针即可,reference 本身不需要修改。
- 缺点:
- 在堆空间中开辟了一块空间作为句柄池,句柄池本身也会占用空间。
- 通过两次指针访问才能访问到堆中的对象,效率低。
2、直接指针(HotSpot 采用)
对象引用 直接指向 对象实例数据(堆)
,对象实例中有类型指针
,指向对象类型数据(方法区)
。
- 优点:不需要开辟一块空间给句柄池;访问效率更高。
- 缺点:对象被移动时(GC时很普遍)需要修改 reference 的值
六、类的卸载
1、类加载器、类、类实例
- 在类加载器的内部实现中,用一个 Java 集合来存放所加载类的引用。
- 一个 Class 对象总是会引用它的类加载器,调用 Class 对象的 getClassLoader()方法,就能获得它的类加载器。
- 一个类的实例 总是引用 代表这个类的Class对象,通过 Object类 中定义的 getClass() 方法,就能获得它的Class对象。
2、类卸载的条件
通过以上的关系分析,可以分析出类的卸载需要同时满足 3 个要求:
- 该类所有的实例都已经被回收(堆中不存在该类及其任何派生子类的实例)
- 加载该类的类加载器已经被回收(除非是自定义类加载器,JDK自带的类加载器一般是不会被回收的)
- 该类对应的 Class对象 没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
由此可见,只有我们自定义的类加载器加载的类是可能被卸载的。
七、类的生命周期(小结)
Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的 class 文件加载到内存生成 class 对象。
按照 Java 虚拟机规范,从 class 文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下 7 个阶段:
类的生命周期小结:
-
加载(Loading) —> 这个过程由「类加载器」完成
根据类全名,获取类或接口的
二进制字节流
,转化为方法区
的数据结构
,并在堆创建Class对象,指向方法区的类的元数据 -
链接(Linking)
-
验证(Verify)
确保加载的字节码符合当前虚拟机要求,保证被加载类的正确性与安全性(验证 文件格式、元数据、字节码、符号引用)
-
准备(Prepare)
为
静态变量(non-final static)
分配内存
并设置默认初始值
-
解析(Resolve)
将
符号引用(变量名,方法名,类名)
转换为直接引用(地址指针)
的过程。
-
-
初始化(Initial)
进行
静态变量
的初始化(赋值)、执行静态代码块
中的语句。(<clinit>
方法) -
使用(Use)
- 创建实例对象
- 为
实例对象
在堆空间分配内存
并设置默认初始值
(对象空间的开辟是由new
指令完成的) - 进行
实例变量
的初始化(赋值)、执行实例代码块
和构造方法
中的语句。(<init>
方法)
-
卸载(Unload)
- 该类所有的实例都已经被回收(堆中不存在该类及其任何派生子类的实例)
- 加载该类的类加载器已经被回收(除非是自定义类加载器,JDK自带的类加载器一般是不会被回收的)
- 该类对应的 Class对象 没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
当以上条件都满足时,Class 对象才会结束生命周期,类在方法区内的数据才会被卸载。
八、经典笔试案例
阅读代码,分析出打印结果:
public class Test1 {
public static int k = 0;
public static Test1 t1 = new Test1("t1");
public static Test1 t2 = new Test1("t2");
public static int i = print("i");//3
public static int n = 99;
// 实例变量
public int j = print("j");
// 静态代码块
static {
print("静态块");
}
// 构造方法
public Test1(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++i;
++n;
}
// 实例代码块
{
print("构造块");
}
// 静态方法
public static int print(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
// main方法
public static void main(String[] args) {
Test1 t = new Test1("init");
}
}
【分析】
- main方法之前,进行类初始化
- 类初始化中间有两次对象创建,要进行对象初始化
- 对象初始化结束,继续类初始化
- 类初始化结束,执行main方法
- main方法中有一次对象创建,要进行对象初始化
类初始化:从上至下加载 静态变量/静态代码块
对象初始化:从上至下加载 实例变量/实例代码块
【参考答案】
1:j i=0 n=0
2:构造块 i=1 n=1
3:t1 i=2 n=2
4:j i=3 n=3
5:构造块 i=4 n=4
6:t2 i=5 n=5
7:i i=6 n=6
8:静态块 i=7 n=99
9:j i=8 n=100
10:构造块 i=9 n=101
11:init i=10 n=102