一句话总结
一个Java对象的生命周期,从被创建到被访问,其背后是JVM复杂的内存管理机制在支撑。
一、对象创建:深入JVM内部流程
当我们在Java代码中执行 new 指令时,JVM并不仅仅是简单地分配一块内存,而是一系列严谨且高效的底层操作。这整个过程可以分为五个核心步骤,每一步都至关重要。
1. 类加载检查
在真正创建对象之前,JVM会检查对应的类是否已经被加载、解析和初始化。这个检查是必要的,因为JVM需要确保类的元数据(如字段、方法等)已经准备就绪,以便为新对象分配正确的内存空间。这个过程由HotSpot JVM的 InterpreterRuntime::_new() 函数触发,它会检查类在方法区的元数据是否合法。
2. 内存分配
对象所需内存大小在类加载完成后便可确定。接下来,JVM需要从堆中分配一块大小合适的内存。这个过程并非简单的“找到一块空地”,而是根据堆内存是否规整而采用不同的策略。
- 指针碰撞(Pointer Bumping) :当堆内存中的对象是紧凑排列时,JVM只需要移动一个指向空闲区域的指针来分配内存。这种方式简单高效,常用于带有**标记-整理(Mark-Compact)**算法的垃圾收集器,例如
Serial或ParNew。 - 空闲列表(Free List) :如果堆内存碎片化严重,JVM会维护一个列表来记录所有可用的内存块。分配时,它会从列表中找到足够大的内存块,更新列表,并将这块内存分配给新对象。例如,
CMS收集器就使用这种方法。
为了解决多线程并发分配内存时可能产生的冲突(如多个线程同时向同一块内存区域分配),JVM引入了本地线程分配缓冲区(TLAB) 。每个线程在Java堆中预先分配一块私有的小内存区域,这样大部分对象创建都在各自的TLAB中完成,避免了同步开销,显著提升了分配效率。只有当TLAB用完时,才会回到共享堆上进行慢速分配。
3. 对象头(Object Header)初始化
内存分配完成后,JVM会立即填充新对象的对象头。对象头包含了关于这个对象的关键元数据,可以看作是对象的“身份证”和“户型图”。
- Mark Word:这是对象头的核心部分,通常在64位JVM中占用8字节。它用于存储对象的运行时数据,如哈希码、GC分代年龄,以及最重要的锁状态信息(如偏向锁、轻量级锁、重量级锁标记)。不同的锁状态会改变Mark Word的内部结构。
- 类指针(Klass Pointer) :这个指针指向方法区中该对象对应的类元数据(
Klass),JVM通过这个指针来确定对象的类型,从而知道如何访问它的字段和方法。在开启压缩指针的情况下,这个指针通常只占用4字节。
4. 实例数据初始化
对象头初始化完毕后,JVM会为对象的实例变量分配内存,并根据其数据类型将它们设置为零值(如 int 为0,boolean 为 false,引用类型为 null)。这个过程确保了即使在构造函数执行前,对象也处于一个可预期的状态。JVM还对字段的排列进行了优化,通常按照 long/double、int/float、short/char、boolean/byte、引用类型的顺序排列,以减少填充(Padding)并最大化利用内存。
5. <init> 方法执行
最后,JVM会执行对象的构造函数(<init> 方法)。这个步骤是程序员定义的,用于完成对实例变量的个性化赋值,例如为字段赋予初始值,或者执行其他复杂的初始化逻辑。
二、内存布局与访问定位
一个完整的Java对象在堆中的内存布局由三部分构成:对象头、实例数据和对齐填充。
1. 内存布局
- 对象头(Header) :如前所述,包含 Mark Word 和 类指针。
- 实例数据(Instance Data) :包括对象自身以及从父类继承的所有字段。为了保证内存利用率,JVM会按照一定的策略对这些字段进行排序。
- 对齐填充(Padding) :由于JVM的内存管理通常以8字节(或更大)的倍数进行,为了确保对象的总大小是对齐的,JVM会在对象末尾自动添加填充字节。
2. 访问定位
在程序中,栈中的引用变量如何找到堆中的对象实例?主要有两种方式:
- 句柄访问(Handle Access) :在Java堆中划分出一块专门的区域作为句柄池。栈中的引用变量存储句柄池的地址,而句柄池则存储着对象实例和类元数据各自的地址。这种方式的好处是,当对象因垃圾回收而移动时,只需要更新句柄池中的地址,栈中的引用保持不变。但缺点是多了一次指针寻址开销。
- 直接指针访问(Direct Pointer Access) :这是HotSpot JVM所采用的方式。栈中的引用直接存储着对象在堆中的地址。对象头中包含了指向其类元数据的指针。这种方式减少了一次寻址操作,速度更快。尽管对象移动时需要更新所有指向它的栈引用,但HotSpot通过指针碰撞和
TLAB等优化手段,大大减少了对象移动的频率,使其成为更优的选择。
3. 压缩指针
在64位系统中,对象的指针通常占用8字节,这会极大地增加内存消耗。HotSpot JVM通过**压缩指针(Compressed Oops)**技术来解决这个问题。它将原本8字节的指针压缩为4字节,通过地址对齐(通常是8字节对齐),将指针的地址值右移3位来存储。这样,4字节的指针就可以寻址 232×8=32GB 的内存空间。这是一种在性能和内存消耗之间取得平衡的巧妙设计。