连载中 3/20

对象的一生:从 new 一句到被回收

2026-09-12 · 181 阅读 · 0 评论 · 0 赞

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)就晋升老年代;大对象(大数组、长字符串)嫌新生代来回搬家折腾,直接进老年代。这套"按年龄分居"的逻辑,正是下一篇垃圾标记和回收算法的舞台布景——知道对象怎么生,才理解垃圾怎么死。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#对象内存布局#TLAB#对象头#对齐填充#对象访问定位

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞