new 的五道工序
一行 new User() 在 JVM 里走五步:检查类加载(没加载就先触类加载流程,下一篇细讲)→ 分配内存(在堆里划出一块确定大小的空间)→ 零值初始化(字段全设默认值,所以字段总有个"出厂价")→ 设置对象头(属于哪个类、哈希码、分代年龄)→ 执行构造函数(这时候才轮到你写的代码)。
一个经典面试题由此有解:为什么构造函数里不要调用可被重写的方法?因为第四步之前字段还是零值,子类重写方法读到的字段可能全是默认值——对象的"出生"比你看到的 new 那一行早得多。
分配内存的两把尺子
堆内存规整(有指针碰撞:已用区和空闲区一分为二)就用"指针碰撞"分配,把分界指针挪一下即可;堆内存零散(有空闲列表:维护着可用块清单)就查清单找合适的一块。并发场景下两个线程抢同一块内存怎么办?两个办法:CAS 加失败重试,或者更优雅的 TLAB——每个线程在 Eden 里预先圈一小块自留地,分配在自己地盘里进行,互不打架。对象真的在 TLAB 里分配失败,才走共享区域的 CAS。
对象的三段身体
// 一个只有 int 字段的对象,实际占多大?
// 对象头:Mark Word 8字节 + 类型指针 4字节(压缩指针)= 12字节
// 实例数据:int 4字节
// 合计 16 字节,正好对齐,无需填充
//
// Mark Word 是个百宝箱(64位机器 8 字节),分时复用:
// 无锁态:对象哈希码 + 分代年龄(4位,最大15)+ 锁标志位
// 偏向锁:线程 ID + 分代年龄 + 锁标志位
// 轻量级/重量级锁:指向锁记录或 monitor 的指针
// 分代年龄为什么最大是 15?Mark Word 只有 4 位存它——这就是
// -XX:MaxTenuringThreshold 最大只能是 15 的底层原因8 字节对齐是另一条隐性规则:对象大小不足 8 的倍数就补填充位。这也是为什么"加一个 boolean 字段,对象未必变大"——可能只是吃掉了原本的填充空间。想亲眼验证可以用 OpenJDK 的 JOL 工具,一行 ClassLayout.parseInstance(obj).toPrintable() 把布局打出来。
怎么访问对象
栈上的引用怎么找到堆里的对象?两条路线:句柄(堆里放一个句柄池,引用存句柄地址,对象搬家只改句柄表,引用不动)和直接指针(引用直接指向对象,访问快一步,但对象搬家要改引用)。HotSpot 选了直接指针——日常访问频率远高于 GC 搬家频率,快的那一步更重要。
年龄与归宿
对象出生在新生代的 Eden,每熬过一次 Minor GC 年龄加一,攒够岁数(默认 15)就晋升老年代;大对象(大数组、长字符串)嫌新生代来回搬家折腾,直接进老年代。这套"按年龄分居"的逻辑,正是下一篇垃圾标记和回收算法的舞台布景——知道对象怎么生,才理解垃圾怎么死。
评论 (0)