- 对象的组成部分
- 参考
- JVM 源码分析之 Java 对象头实现">JVM 源码分析之 Java 对象头实现
- 探索C++虚函数在内存中的表现形式及运行机制">探索C++虚函数在内存中的表现形式及运行机制
- 从jvm虚拟机角度看Java多态 ->(重写override)的实现原理">从jvm虚拟机角度看Java多态 ->(重写override)的实现原理
- Java 的多态在 JVM 里原来是这样的">Java 的多态在 JVM 里原来是这样的
- Java的对象">Java的对象
- 深入探究 JVM | klass-oop对象模型研究">深入探究 JVM | klass-oop对象模型研究
- Java的对象模型——Oop-Klass模型(一)">Java的对象模型——Oop-Klass模型(一)
- Java的对象模型——Oop-Klass模型(二)">Java的对象模型——Oop-Klass模型(二)
- Java对象的内存布局(jol-core)">Java对象的内存布局(jol-core)
- 64位JVM的Java对象头详解,从hotspot源码中寻找答案">64位JVM的Java对象头详解,从hotspot源码中寻找答案
- Java 对象占用内存大小与 java 对象格式">Java 对象占用内存大小与 java 对象格式
- 每日五分钟,玩转JVM」:对象内存布局">每日五分钟,玩转JVM」:对象内存布局
在我们的日常开发中,经常会遇到这样的问题:比如在一些优化过程中,我们要去回收一些大的对象,如何去定义大对象,或者说怎样确定一个对象所占据的内存有多大?平常所说的对象锁,在对象中是怎样表示的?这些都和对象在内存中的布局有关。
对象的组成部分
从整体来看,对象在内存中分为3部分,分别为对象头,数据以及对齐填充

图 - JVM 对象内存布局
在 Java 中创建一个对象时,使用 new 关键字。对应的实现在 intercepterRuntime.app 中:
IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, ConstantPool* pool, int index))Klass* k_oop = pool->klass_at(index, CHECK);instanceKlassHandle klass (THREAD, k_oop);// Make sure we are not instantiating an abstract klassklass->check_valid_for_instantiation(true, CHECK);// Make sure klass is initializedklass->initialize(CHECK);// At this point the class may not be fully initialized// because of recursive initialization. If it is fully// initialized & has_finalized is not set, we rewrite// it into its fast version (Note: no locking is needed// here since this is an atomic byte write and can be// done more than once).//// Note: In case of classes with has_finalized we don't// rewrite since that saves us an extra check in// the fast version which then would call the// slow version anyway (and do a call back into// Java).// If we have a breakpoint, then we don't rewrite// because the _breakpoint bytecode would be lost.oop obj = klass->allocate_instance(CHECK);thread->set_vm_result(obj);IRT_END
new 指令的实现过程:
- pool 表示对象的常量池(constant pool),此时类已经加载到虚拟机中
- pool->klass_at 负责返回对象的 klassOop 对象
- klass->check_valid_for_instantiation(true, CHECK) 可以防止抽象类被初始化
- klass->initialize(CHECK) 保证对象的初始化
- klass->allocate_instance(CHECK) 为对象在堆中分配内存,并创建 instanceOop 对象
Hotspot VM 并没有根据 Java 实例对象直接创建通过虚拟机映射到新建的 C++ 对象,而是设计一个 oop-klass 模型。其中 oop(Ordinary Object Pointer,普通对象指针)用来表示对象的实例信息,看起来是一个指针,实际上却是一个隐藏在指针中的对象。而 klass 则包含元数据和方法信息,用来描述 Java 类。HotSopt JVM 的设计者设计 oop-klass 模型的原因是不想让每个对象中都含有一个vtable(虚函数表)
对象头
Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object’s layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.
每个 GC 管理的堆对象头部的公共结构(每个 oop 都指向一个对象头)。包括关于堆对象的布局、类型、GC 状态、同步状态和身份哈希码的基本信息。
对象头由两个词(word)组成,分别为
Mark Word和Klass Point。如果该对象是一个数组,还包括一个数组长度字段。请注意,Java 对象和 VM 内部对象都有一个共同的对象头格式。
oop 类型是 oopDesc*。Java 没创建一个新对象,就会相应对创建一个对应类型的 oop 对象。oopDesc 正是所有 oop 的基类。instanceOopDesc 和 arrayOopDesc 是 oopDesc 的子类,分别表示普通对象和数组对象。oopDesc 又称为对象头。对象头在 C++ 中的定义如下:
class oopDesc {friend class VMStructs;private:volatile markOop _mark;union _metadata {Klass* _klass;narrowKlass _compressed_klass;} _metadata;......}
mark word
markOop 描述了一个对象的头部,但不代表一个对象指针(oop),仅仅表示这是一个字(word)。markOop 包含了对象的存活年代信息,锁信息,hash,当前线程等。在32位和64位虚拟机上 markOop的布局分别如下:
// 32 bits:// --------// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)// size:32 ------------------------------------------>| (CMS free block)// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
| 锁状态 | 25bit | 4bit | 1bit | 2bit | |
|---|---|---|---|---|---|
| 23bit | 2bit | 是否偏向锁 | 锁标志位 | ||
| 无锁 | 对象的hashcode | 分代年龄 | 0 | 01 | |
| 偏向锁 | 线程ID | Epoch | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
| 重量级锁 | 指向互斥锁(重量级锁)的指针 | 10 | |||
| GC 标记 | 空 | 11 |
// 64 bits:// --------// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)// size:64 ----------------------------------------------------->| (CMS free block)//// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
| 锁状态 | 56bit | 1bit | 4bit | 1bit | 2bit | |
|---|---|---|---|---|---|---|
| 是否偏向锁 | 锁标志位 | |||||
| 无锁 | unused:25bit | 对象的hashcode:31bit | unused | 分代年龄 | 0 | 01 |
| 偏向锁 | 线程ID:54bit | Epoch:2bit | unused | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针(ptr_to_lock_record) | 00 | ||||
| 重量级锁 | 指向互斥锁(重量级锁)的指针(ptr_to_heavyweight_monitor) | 10 | ||||
| GC 标记 | 空 | 11 |
- hash:运行期间调用
System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果 31 位不够表示,在偏向锁,轻量锁,重量锁,hashcode 会被转移到 Monitor 中 - age:保存了对象的分代年龄,也就是被 GC 的次数,当该对象经历一定次数的 GC 后还没有被回收,对象就会被转移到老年代。对象提升的阈值是是通过JVM参数设定的:-XX:MaxTenuringThreshold ,默认值是15
- biased_lock: 是否将锁偏向给定线程。由于无锁和偏向锁的锁标识都是01,无法区分,所以引入一位偏向锁标识。当锁偏向给定线程时,该线程可以执行锁定和解锁,而无需使用原子操作。 当锁的偏见被撤销时,它会恢复到正常锁定方案
- lock:表示锁状态标志位
- JavaThread* :保存持有偏向锁的线程 ID
- epoch:保存偏向时间戳
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM 使用原子操作而不是 OS 互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM 通过 CAS 操作在对象的 mark word 中设置指向锁记录的指针
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器 Monitor 的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到 Monitor 以管理等待的线程。在重量级锁定的情况下,JVM 在对象的
ptr_to_heavyweight_monitor设置指向 Monitor 的指针
锁标记的值在 markOop 中使用一个枚举表示:
enum { locked_value = 0, // 00unlocked_value = 1, // 01monitor_value = 2, // 10marked_value = 3, // 11biased_lock_pattern = 5 // 101};
- 无锁表示一个对象没有被加锁时的状态
- 偏向锁,对象会偏向于第一个访问锁的线程,当同步锁只有一个线程访问时,JVM 会将其优化为偏向锁,此时就相当于没有同步语义;当发生多线程竞争时,偏向锁就会膨胀为轻量级锁
- 轻量级锁采用 CAS(Compare And Swap)实现,避免了用户态和内核态之间的切换。如果某个线程获取轻量级锁失败,该锁就会继续膨胀为重量级锁
- 重量级锁使得 JVM 会向操作系统申请互斥量,因此性能消耗是最高的
注意:JDK 15 已经废弃了偏向锁,具体见:JEP 374: Disable and Deprecate Biased Locking
klass pointer
union _metadata {Klass* _klass;narrowKlass _compressed_klass;} _metadata;
_metadata 是一个联合体(内部数据共用一块内存)。比如 _metadata 中存放两个指针数据:_klass 和 _compressed_klass。其中,_klass 在未采用指针压缩时使用,_compressed_klass 在采用指针压缩时使用。**klass pointer** 是对象指向他的类型元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据
| 数据类型 | 所占字节 | 存储数据量 |
|---|---|---|
| char | 2 | 存储 Unicode 码,用单引号赋值 |
| boolean | 1 | true / false |
| byte | 1 | 255(-128 ~ 127) |
| short | 2 | 65535(-32768 ~ 32767) |
| int | 4 | 232-1(-231 ~ 231-1) |
| long | 8 | 264-1(-263 ~ 263-1) |
| float | 4 | 3.4e-45 ~ 1.4e38 |
| double | 8 | 4.9e-324 ~ 1.8e-308 |
| ref | 4(32bit)/ 8(64bit)/ 4(64bit && -XX:UseCompressedOops) |
注:ref(引用)类型实际上是一个地址指针。在 32 位系统下为 4 个字节,在 64 位系统下为 8 个字节,在 64 位系统下且开启了指针压缩,为 4 个字节。
实例数据的排列规则如下:
- 字段默认分配顺序:long/double, int,short/char,byte/boolean,oops(普通对象指针,未开启指针压缩占8个字节,开启指针压缩占4个字节)
- 在规则1的基础上,先分配父类字段,再分配子类字段
- 当父类中最后一个成员和子类第一个成员的间隔如果不够4个字节的话,就必须扩展到4个字节的基本单位
- 父类的字段出现在子类变量之前,但是如果将 compactFields 参数设置为 true,将子类中较小的变量插入到父类大变量的空隙中。例如:

对齐填充
Hot Spot JVM中规定了对象的大小必须是8字节的整数倍,在C/C++中类似的功能被称之为内存对齐,内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
内存对齐遵循两个规则:
- 假设第一个成员的起始地址为 0,每个成员的起始地址(startpos)必须是其数据类型所占空间大小的整数倍
- 结构体的最终大小必须是其成员(基础数据类型成员)里最大成员所占大小的整数倍
为什么要对齐数据?字段内存对齐的其中一个原因,是让字段只出现在同一CPU的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。其实对其填充的最终目的是为了计算机高效寻址。
