HotSpot虚拟机对象在Java堆中分配、布局和访问的全过程。
总结:
1、对象的创建:
- 类加载检查(没加载则加载,获取对象所需内存)
- -》内存分配(指针碰撞、空闲列表,并发通过TLAB+CAS重试 解决)
- -》对象初始化(设置对象头信息+其他信息零值填充)
- -》调用构造器(dup指令复制对象指针+invokespecial指令调用对象构造方法)。
2、对象的内存分布:
- 对象头(mark word(哈希码+GC年龄+锁标志位)+
- 类元数据指针+数组长度)+实例数据(根据字段类型排序)+
- 对齐填充(保证是8字节的倍数)
3、对象的访问定位:
- 句柄访问:reference访问堆中句柄池中的对象句柄,然后在找到堆中对象
- 直接指针访问:reference直接存储对象地址。
详解:
如何进行对象的创建?
创建对象需要解决的问题?
- 1、先知道创建对象需要多少内存? -> 类加载检查 -> 获取类信息,如不存在就类加载后再获取类信息 -> 类信息就决定了对象所需要的内存大小
- 2、怎么给对象进行内存分配?-> 分配策略:指针碰撞(简单高效 0(1))、空闲列表(0(n))
- 3、分配时的并发问题怎么解决?-> TLAB(线程本地缓冲)+ CAS重试
- 4、对象如何初始化?-> 设置对象头信息(类元数据指针、GC年龄、哈希值、锁标志位)+ 零值填充其他信息
- 5、对象怎么提供给程序进行访问? -> dup(复制引用)-> invokespecial(通过dup复制的引用调用构造器方法)
对象创建流程?其中如何解决的上述问题?
- 1、先进行类加载检查
- 解析指令(获取类引用):遇到new指令时,虚拟机从常量池中获取类的引用。
- 验证引用(是否需要类加载):验证该类是否已加载、解析、初始化(通过方法)。未加载则触发类加载过程。
- 2、然后进行内存分配:(类加载后便可以确定对象的大小(例如,int占用4字节),所以可以直接进行空间分配)
-
2.1、分配策略:
- 指针碰撞(规整空间):移动指针分配内存,简单高效,O(1)(如:Serial、ParNew等带压缩整理过程的收集器)。
- 空闲列表(碎片化空间):从空闲列表中分配,需遍历空闲列表,所以O(n)(如:CMS GC,基于清除(Sweep)算法的收集器)。
- 注意: 其实,CMS设计了一个叫作Linear Allocation Buffer的分配缓冲区,在通过空闲列表拿到一大块分配缓冲区后,在这块缓冲区里可以使用指针碰撞方式来分配。
- 注意: 其实,CMS设计了一个叫作Linear Allocation Buffer的分配缓冲区,在通过空闲列表拿到一大块分配缓冲区后,在这块缓冲区里可以使用指针碰撞方式来分配。
-
2.2、如何保证分配时的并发安全?
- 当线程创建对象时,优先从TLAB(线程本地分配缓冲)分配(每个线程预分配内存块,减少全局锁竞争,内存连续性)
- 若TLAB不足,则通过**CAS重试(原子操作 无锁高效)**向Eden区申请新TLAB。
- 注意:是否使用TLAB,可以通过**-XX:+/-UseTLAB**参数来设定
-
- 3、再然后初始化对象
- 设置对象头:记录以下信息
- 类元数据指针:指向方法区的类元数据(32位JVM占用4字节,64位JVM占用8字节)
- GC年龄:Minor GC的次数(默认15次后升到老年代,可通过-XX:MaxTenuringThreshold调整)
- 哈希值:延迟计算,首次调用System.identityHashCode()时生成
- 锁标志位:偏向锁ID、轻量级锁、重量级锁等信息
- 零值填充(除对象头外设置初始值):分配内存后,除对象头外初始化为零值(如int默认为0)。
- 设置对象头:记录以下信息
- 4、最后调用构造器:字节码指令(new → dup → invokespecial)
- dup指令:复制引用,确保初始化后可被程序使用。
- 构造方法:通过invokespecial指令,使用dup复制的对象引用 去调用构造方法。
new vs 反射 vs 克隆创建对象时的异同
如何进行对象的内存布局?
对象内存布局时需要考虑的问题?
- 1、对象的基本信息有哪些?-> 运行时数据(哈希码、GC年龄、锁状态)、类元数据指针(如何找到对象的类信息)、数组的长度(对象可确定大小,但数组对象需根据对象和数量确定大小)
- 2、基本信息存在哪?-> 对象头。
- 3、对象数据如何存储?-> 按字段类型的大小排序后存储(longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs))
- 4、如何适配64位机器?-> 通过对齐填充保证为8字节的倍数。
Java对象内存详解
- 1、对象头:
- Mark Word:存储运行时数据(31bit哈希码、4bit代龄、2bit锁标志、26bit未使用)。
- 类元数据指针:指向方法区的类元数据。
- 数组长度:仅数组对象有。
- 2、实例数据:
- 存储类的字段(如int a; String b;)。
- 按字段类型和顺序排列(longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs))。
- 3、对齐填充:确保对象起始地址为8字节倍数(如long对齐)。
如何进行对象的访问定位?
java通过栈上的reference数据来操作堆上的具体对象.
访问对象的方式有哪些?各自怎么实现的?各自的优缺点?
-
1、句柄访问:通过reference找到java堆中句柄池内该对象的句柄(句柄 = 对象实例数据地址信息 + 类元数据地址信息)
-
优点:对象变动时,只需修改句柄中的地址信息,reference不需要变动
-
缺点:需先找到句柄,然后再找到对象,多了一次指针定位
-
-
2、直接指针访问:reference中直接存放对象地址
-
优点:相比句柄节省了一次指针定位的时间开销,更快
-
缺点:对象变动时,需修改reference。