对象生命周期:对象创建、对象头结构、锁状态与偏向锁

聊到ART虚拟机,对象生命周期是个绕不开的话题。说白了,Java里的一切都是对象,但对象在内存里到底怎么生、怎么活、怎么锁,很多人其实没深究过。我刚开始啃ART源码时,也被对象头那一串bit位搞得头晕。今天咱们就把这块掰开揉碎,讲清楚。

一、对象创建:从new指令说起

你写一行 Object obj = new Object(),ART在背后干了多少活?我数过,至少七八步。

  1. 类加载检查:虚拟机先看看这个类有没有被加载过。没加载?走类加载流程。
  2. 分配内存:ART根据对象大小,在堆上划一块连续空间。年轻代用bump-the-pointer,老年代用free-list。
  3. 内存置零:分配到的内存,除了对象头,其他字段全部置为0。这就是为什么Java的int默认是0,boolean默认是false。
  4. 设置对象头:这一步最关键,我们后面细讲。
  5. 执行构造方法:调用方法,完成用户定义的初始化逻辑。

嗯,这里要注意:ART对内存分配做了大量优化。比如TLAB(线程本地分配缓冲区),每个线程在堆上预申请一块小空间,分配对象时直接在自己TLAB里划,不用加锁。我在项目里遇到过,高并发场景下TLAB命中率能到95%以上,性能提升非常明显。

二、对象头结构:64位下的布局

对象头是对象的“身份证”。在64位ART里,一个普通对象的对象头占12字节(96位),分为两部分:

字段 大小 说明
Mark Word 8字节(64位) 存储锁信息、GC标记、哈希码等
Klass Pointer 4字节(压缩后) 指向类元数据的指针

Mark Word是核心。它的64个bit位,在不同状态下含义完全不同。我画了张图,你看一眼就明白了。

Mark Word 64位布局(不同锁状态) 无锁状态 | unused:25bit | identity_hashcode:31bit | cms_free:1bit | age:4bit | biased_lock:1bit | lock:2bit | 01 偏向锁状态 | thread:54bit | epoch:2bit | cms_free:1bit | age:4bit | biased_lock:1bit | lock:2bit | 01 轻量级锁 | ptr_to_lock_record:62bit | lock:2bit | 00 重量级锁 | ptr_to_monitor:62bit | lock:2bit | 10 锁标志位:01=无锁/偏向锁,00=轻量级锁,10=重量级锁,11=GC标记

看到没?最后两位是锁标志位。01代表无锁或偏向锁,00是轻量级锁,10是重量级锁,11是GC标记。我当年调试一个死锁问题,就是靠读Mark Word的锁标志位定位的——那感觉,就像在黑暗里找到了开关。

三、锁状态与偏向锁

Java的synchronized,在ART里经历了一个“进化”过程。从无锁到偏向锁,再到轻量级锁,最后到重量级锁。这个过程叫锁升级,而且不可逆

3.1 偏向锁

偏向锁是ART的“乐观”策略。它假设:一个对象大多数时候只被一个线程访问。那何必搞复杂的同步呢?直接把线程ID记在Mark Word里就行了。

偏向锁的加锁过程:

  • 线程A第一次访问同步块,ART检查Mark Word的biased_lock位是否为1。
  • 如果是,CAS操作把线程A的ID写入Mark Word。
  • 下次线程A再来,直接比对ID,匹配就进入,无需任何同步操作。

你想想看,这效率多高?完全就是“零成本”加锁。我在做性能优化时,发现很多业务场景其实天然就是单线程访问的,比如每个请求独享一个对象。这时候偏向锁能省掉大量CAS开销。

核心要点:偏向锁在JDK 15之后被默认禁用了,因为维护成本高,而且很多现代应用都是多线程高并发。但ART里仍然保留了这个机制,毕竟Android场景下很多对象确实是线程私有的。

3.2 锁升级过程

为什么会升级?因为出现了竞争。我举个例子:

// 线程A持有偏向锁
synchronized (obj) {
    // 线程B来了,也想拿这把锁
    // 偏向锁失效,升级为轻量级锁
}

升级路径是这样的:

  1. 偏向锁 → 轻量级锁:另一个线程尝试获取偏向锁,ART撤销偏向,升级为轻量级锁。轻量级锁用CAS自旋来抢锁,不阻塞线程。
  2. 轻量级锁 → 重量级锁:自旋超过阈值(默认10次),或者自旋线程数超过CPU核数的一半,升级为重量级锁。重量级锁会阻塞线程,进入等待队列。

避坑指南:我曾经遇到过一个诡异问题——某个同步方法在高并发下性能急剧下降。查了半天,发现是锁升级到了重量级锁,线程频繁阻塞唤醒。解决方案很简单:减少同步块粒度,或者用读写锁替代。记住,重量级锁是最后的手段,能不用就不用。

3.3 锁状态对比

锁状态 开销 适用场景 Mark Word特征
无锁 0 无竞争 锁标志位=01,biased_lock=0
偏向锁 极低(一次CAS) 单线程重复访问 锁标志位=01,biased_lock=1
轻量级锁 中等(CAS自旋) 低竞争,短同步块 锁标志位=00
重量级锁 高(线程阻塞) 高竞争,长同步块 锁标志位=10

我个人习惯是:写代码时先不考虑锁优化,用最清晰的同步方式。等性能分析工具告诉我瓶颈在哪,再针对性优化。过早优化是万恶之源,这话一点不假。

四、对象头里的其他秘密

Mark Word里还藏着两个重要信息:GC分代年龄identity hash code

  • 分代年龄:4个bit位,最大15。对象每熬过一次Minor GC,年龄+1。到15还没死?晋升到老年代。嗯,这里要注意,ART的分代年龄和HotSpot一样,都是4位。
  • identity hash code:调用 System.identityHashCode() 或未重写的 hashCode() 时生成。这个值一旦生成,就会写入Mark Word。但有个坑——如果对象已经处于偏向锁状态,生成hash code会导致偏向锁撤销。因为Mark Word的bit位不够用了。

警告:不要在同步块内调用 System.identityHashCode(),尤其是对象处于偏向锁状态时。这会导致偏向锁撤销,性能骤降。我踩过这个坑,线上服务RT从5ms飙到200ms,查了两天才定位到。

五、总结

对象生命周期这块,说白了就是ART怎么管内存、怎么管锁。对象头是核心,Mark Word是灵魂。理解了Mark Word在不同锁状态下的布局,你就能看懂synchronized的底层行为。

最后说一句:别把锁升级想得太神秘。它就是ART根据竞争情况做的自适应优化。你写代码时正常用synchronized就行,ART会帮你选最合适的锁状态。但如果你在做性能调优,了解这些细节能让你少走很多弯路。

一句话记住:对象头是对象的“户口本”,Mark Word是“状态机”,锁升级是“自适应策略”。三者配合,才有了Java高效的同步机制。

公众号:蓝海资料掘金营,微信deep3321