第十章 对象的实例化内存布局与访问定位
10.1 对象的实例化
面试题
美团:
对象在 JVM 中是怎么存储的?
对象头信息里面有哪些东西?
蚂蚁金服:
Java 对象头有什么?
对象的实例化

10.1.1 创建对象的方式
-
new:最常见的方式、Xxx 的静态方法,XxxBuilder/XxxFactory 的静态方法
-
Class 的
newInstance()方法:反射的方式,只能调用空参的构造器,权限必须是 public -
Constructor 的
newInstance(XXX):反射的方式,可以调用空参、带参的构造器,权限没有要求 -
使用
clone():不调用任何的构造器,要求当前的类需要实现 Cloneable 接口,实现clone() -
使用序列化:从文件中、从网络中获取一个对象的二进制流
-
第三方库 Objenesis
10.1.2 创建对象的步骤(字节码角度)
public class ObjectTest {
public static void main(String[] args) {
Object obj = new Object();
}
}
反编译ObjectTest类
找到对应的字节码文件,运行javap -v -p ObjectTest.class
...
{
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
8 1 1 obj Ljava/lang/Object;
}
SourceFile: "ObjectTest.java"
-
new #2:分配对象内存JVM 在堆中为
java.lang.Object类型分配一块未初始化的内存空间,并返回该内存的引用指针。此时对象的字段(如Object的hashCode、锁状态等)均为默认值(0 或 null),但构造函数尚未调用。详细过程如下:-
new指令根据#2常量池引用,定位到java.lang.Object的类信息 -
JVM 在方法区查找该类的结构:字段数量、方法表、常量池、访问标志等
-
堆中分配空间,新建对象
-
新建的对象,其对象头的 Klass Pointer(类指针)就被设置为指向方法区中
Object.class的元数据地址
-
-
dup:复制引用栈顶由于后续需要调用构造函数(
invokespecial),而构造函数需要一个对象引用作为this参数,因此必须将刚刚创建的引用复制一份到操作数栈顶。这样,一份用于构造函数调用,另一份保留用于后续赋值 -
invokespecial #1:调用构造函数通过类指针,确认
Object.<init>是否存在,是否可以访问,最后再调用。Object.<init>,这是 Java 中所有类的默认构造函数。它会:-
调用父类
Object的构造器(无显示父类时默认继承自Object) -
初始化对象的实例变量(此处无字段,故无实质操作)
-
设置对象头信息(如 Mark Word,包含锁状态、GC 分代年龄等)
此时对象才真正“活过来”,具备了语义上的完整性
-
-
astore_1:引用赋值给局部变量将操作数栈顶的对象引用存入局部变量表的第 1 个槽位(obj),完成变量绑定。此后,
obj就指向了堆中那个已经初始化的Object实例 -
return:方法结束main方法执行完毕,局部变量obj在方法栈帧销毁后失去作用域,但堆中的对象仍然存在,等待 GC 回收
方法区是对象创建的前提,堆是对象存在的载体,二者通过类指针形成闭环。
10.1.3 创建对象的步骤(执行过程角度)

1、判断对象对应的类是否加载、链接、初始化
虚拟机遇到一条 new 指令,首先去检查这个指令的参数能否在 Metaspace 的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化(判断类元信息是否存在)
如果没有,那么在双亲委派模式下,使用当前类加载器以 ClassLoader + 包名 + 类名为 key 进行查找对应的.class文件
-
如果没有找到文件,则抛出
ClassNotFoundException异常 -
如果找到,则进行类加载,并生成对应的 Class 对象
2、为对象分配内存
首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即 4 个字节大小
如果内存规整:虚拟机将采用的是指针碰撞法(Bump The Point)来为对象分配内存
- 意思是所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针指向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是 Serial,ParNew 这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带 Compact(整理)过程的收集器时,使用指针碰撞
如果内存不规整:虚拟机需要维护一个空闲列表(Free List)来为对象分配内存
- 已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容
选择哪种分配方式由 Java 堆是否规整所决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
3、处理并发问题
-
采用 CAS 失败重试、区域加锁保证更新的原子性
-
每个线程预先分配一块 TLAB:通过设置
-XX:+UseTLAB参数来设定
4、初始化分配到的内存
所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
5、设置对象的对象头
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的 GC 信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于 JVM 实现
6、执行init方法进行初始化
在 Java 程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量
因此一般来说(由字节码中跟随invokespecial指令所决定),new 指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完成创建出来
给对象属性赋值的操作
-
属性的默认初始化(对应上面 4)
-
显式初始化(对应上面 6)
-
代码块中初始化(对应上面 6)
-
构造器中初始化(对应上面 6)
代码演示
/**
* 测试对象实例化的过程
*
* 给对象赋值的操作
* 1.属性的默认初始化 2.显示初始化 3.代码块中初始化 4.构造器中初始化
*/
public class Customer {
int id = 1001;
String name;
Account acct;
{
name = "匿名用户";
}
public Customer() {
acct = new Account();
}
}
class Account {
}
用jclasslib插件进行查看

测试对象实例化的过程
-
加载类元信息
-
为对象分配内存
-
处理并发问题
-
属性的默认初始化(零值初始化)
-
设置对象头的信息
-
属性的显示初始化、代码块中初始化、构造器中初始化
10.2 对象内存布局

10.2.1 对象头(Header)
对象头包含了两部分,分别是运行时元数据(Mark Word)和类型指针。如果是数组,还需要记录数组的长度
运行时元数据
-
哈希值(HashCode)
-
GC 分代年龄
-
锁状态标志
-
线程持有的锁
-
偏向线程 ID
-
偏向时间戳
类型指针
指向类元数据 Instance Klass,确定该对象所属的类型
10.2.2 实例数据(Instance Data)
它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
-
相同宽度的字段总是被分配在一起
-
父类中定义的变量会出现在子类之前
-
如果
CompactFields参数为 true(默认为 true):子类的窄变量可能插入到父类变量的空隙
10.2.3 对齐填充(Padding)
不是必须的,也没有特别的含义,仅仅起到占位符的作用
10.2.4 小结

10.3 举例说明
Customer类
/**
* 测试对象实例化的过程
*
* 给对象赋值的操作
* 1.属性的默认初始化 2.显示初始化 3.代码块中初始化 4.构造器中初始化
*/
public class Customer {
int id = 1001;
String name;
Account acct;
{
name = "匿名用户";
}
public Customer() {
acct = new Account();
}
}
class Account {
}
CustomerTest类
public class CustomerTest {
public static void main(String[] args) {
Customer cust = new Customer();
}
}
图示

10.4 对象的访问定位

创建对象的目的是为了使用它
JVM 是如何通过栈帧中的对象引用访问到期内部的对象实例呢?
- 定位,通过栈上 reference 访问
对象访问方式主要有两种
-
句柄访问
-
直接指针(Hotspot 采用)
10.4.1 句柄访问

reference 中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference 本身不需要被修改
10.4.2 直接指针(Hotspot 采用)

直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据

浙公网安备 33010602011771号