对象生命周期:对象创建、对象头结构、锁状态与偏向锁
聊到ART虚拟机,对象生命周期是个绕不开的话题。说白了,Java里的一切都是对象,但对象在内存里到底怎么生、怎么活、怎么锁,很多人其实没深究过。我刚开始啃ART源码时,也被对象头那一串bit位搞得头晕。今天咱们就把这块掰开揉碎,讲清楚。
一、对象创建:从new指令说起
你写一行 Object obj = new Object(),ART在背后干了多少活?我数过,至少七八步。
- 类加载检查:虚拟机先看看这个类有没有被加载过。没加载?走类加载流程。
- 分配内存:ART根据对象大小,在堆上划一块连续空间。年轻代用bump-the-pointer,老年代用free-list。
- 内存置零:分配到的内存,除了对象头,其他字段全部置为0。这就是为什么Java的int默认是0,boolean默认是false。
- 设置对象头:这一步最关键,我们后面细讲。
- 执行构造方法:调用
方法,完成用户定义的初始化逻辑。
嗯,这里要注意:ART对内存分配做了大量优化。比如TLAB(线程本地分配缓冲区),每个线程在堆上预申请一块小空间,分配对象时直接在自己TLAB里划,不用加锁。我在项目里遇到过,高并发场景下TLAB命中率能到95%以上,性能提升非常明显。
二、对象头结构:64位下的布局
对象头是对象的“身份证”。在64位ART里,一个普通对象的对象头占12字节(96位),分为两部分:
| 字段 | 大小 | 说明 |
|---|---|---|
| Mark Word | 8字节(64位) | 存储锁信息、GC标记、哈希码等 |
| Klass Pointer | 4字节(压缩后) | 指向类元数据的指针 |
Mark Word是核心。它的64个bit位,在不同状态下含义完全不同。我画了张图,你看一眼就明白了。
看到没?最后两位是锁标志位。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来了,也想拿这把锁
// 偏向锁失效,升级为轻量级锁
}
升级路径是这样的:
- 偏向锁 → 轻量级锁:另一个线程尝试获取偏向锁,ART撤销偏向,升级为轻量级锁。轻量级锁用CAS自旋来抢锁,不阻塞线程。
- 轻量级锁 → 重量级锁:自旋超过阈值(默认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高效的同步机制。