哈喽大家好,我是小米,一个爱喝咖啡也爱debug的31岁程序员!
今天我们不聊业务、不聊面试题,咱们一起坐上时间穿梭机,潜入JVM内部——去探一探当我们 new 一个对象时,它在HotSpot虚拟机里究竟经历了什么!对象的内存是怎么分配的?分在哪?怎么找到?有没有可能出差错?
别急,我慢慢讲给你听——这是一段对象的“出生之旅”。
新世界的起点:对象是怎么诞生的?
上周项目组在优化线上接口响应时间,我盯着一段高频调用代码,突然脑子里闪过一个问题:
“我们每次 new 一个对象,它到底在哪儿被创建出来?JVM 里是怎么安排的?”
这个问题,可能很多人都跟我一样——听说过堆内存、栈内存、对象头、TLAB,但模模糊糊的。今天我就带你从 JVM 的角度,捋清楚对象创建背后的底层流程。
我们从一个熟悉的 Java 语句开始:
这看似简单的代码背后,其实发生了很多魔法——HotSpot JVM 会负责完成对象的内存分配、初始化、关联变量引用等一系列工作。
我们先说第一步:内存分配。
为对象分配空间:是在堆上!
在 HotSpot 虚拟机中,对象默认会分配在 Java 堆(Heap)中——它是JVM最大的一块内存区域,专门用于存放对象实例。
但问题来了:
"这么大的堆空间,怎么知道哪个地方是空的?怎么分配?又如何保证线程安全?"
别急,JVM其实早想好了两种策略:
1. 指针碰撞(Bump the Pointer)
如果 Java 堆中的内存是规整的、空闲内存区域全都挨着,就像一个排好队的队列。那么分配对象就非常简单:
- JVM 维护一个指针 alloc_ptr,指向当前空闲内存的起始位置;
- 每次分配对象时,只需要把 alloc_ptr 向后移动一段大小(对象所需内存),就完成了分配!
是不是很像我们订酒店房间?一间接一间,依次入住。
这个分配方式非常高效,但它有前提条件:堆内存是规整的,没有碎片。这通常是使用Serial或ParNew GC的“新生代”才具备的条件,因为它们会采用“复制算法”进行垃圾回收,保证空间整洁。
2. 空闲列表(Free List)
如果内存不是规整的,堆里东一块、西一块有空闲区域,那怎么办?
这时候 JVM 会使用“空闲列表”的策略。它维护一个链表,记录哪些内存块是空闲的,每次创建对象时,从列表中挑出一块“刚刚好”或“足够大”的内存区域进行分配。
这种方式的成本稍高一些,因为可能需要遍历链表、甚至碎片整理。但在使用CMS或G1 GC的老年代中,这种方式更合适。
线程安全吗?TLAB 来帮忙!
你可能会想:“多线程环境下同时分配对象,不会冲突吗?”
是的,JVM在设计的时候也考虑到了这个问题。
于是引入了一个高效的机制:TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区。
它的思想很简单:
“既然大家都想分配对象,那干脆每个线程给你一小块私有堆内存,别抢了!”
每个线程在创建时,JVM都会从堆中划出一小块内存,作为它的TLAB。这个TLAB由线程独占,分配对象时就只需要修改自己的指针,不涉及锁操作,效率非常高。
当一个线程的TLAB不够用了怎么办?可以申请新的 TLAB,或者退而求其次,进入全局堆中分配(这个时候就会涉及锁或者原子操作了)。
TLAB 不是必须的,可以通过 -XX:-UseTLAB 关闭。但在高并发场景下,它几乎是性能提升的刚需。
对象头里藏了什么秘密?
对象分配好内存之后,还有一个重要的步骤:初始化对象头。
每一个 Java 对象在 HotSpot JVM 中都有一个“对象头”,它是对象存储结构的一部分,包含了很多元数据。
通常来说,对象头分为两个部分:
1、Mark Word(标记字段)
这里存储了对象的哈希码、GC年龄、锁状态(轻量锁、偏向锁、重量锁)等信息。
是的,synchronized 的锁信息就藏在这!
2、类型指针
指向对象的 Class 元数据,用于确定这个对象属于哪个类。
构造函数并不负责“分配”!
这里要特别提醒一下:
很多朋友会误解,以为 new 的时候调用构造函数就“创建对象”了。但实际上:
- 构造函数并不负责分配内存;
- 它只是初始化对象的字段值(尤其是用户自定义的属性);
- 真正的内存分配是在构造函数之前就完成的!
所以,构造函数不是创建者,而是“整理者”。
那些意想不到的坑
在一次线上事故中,我们排查内存泄漏时发现一个诡异的现象:某个线程竟然疯狂 new 对象,最终 OOM!
但奇怪的是:这个线程一直在做“短命对象”的操作啊?
排查半天,原来是:
- TLAB太小,频繁退回全局堆中分配;
- 全局堆因为碎片太多,导致分配失败频繁触发 GC;
- GC 又无法及时清理,导致“堆内存耗尽”。
这让我对对象分配机制产生了新的敬畏——分配策略虽然底层,但对性能和稳定性的影响不可忽视!
总结归纳:对象出生的全流程
我们来快速总结一下 HotSpot 对象分配的全流程:
- 类加载完成,准备好元数据;
- 内存分配:
-
- 否则在堆中通过指针碰撞或空闲列表分配;
-
- 如果开启TLAB,先在本地线程缓存中分配;
- 对象头初始化;
- 执行构造函数,初始化字段值;
- 引用变量赋值,让 user 指向对象。
对象从“nothing”变成“活蹦乱跳的Java实例”,只用了短短几毫秒,却穿越了 JVM 的千山万水!
小彩蛋:如何观测对象分配?
如果你也像我一样好奇,可以加上这些JVM参数,观察对象的分配细节:
- -XX:+PrintGCDetails
- -XX:+PrintTLAB
- -XX:+UseTLAB
尤其是 -XX:+PrintTLAB,能清楚看到线程是否使用了 TLAB、是否退回到了堆中。
推荐再配合一个神器:JOL (Java Object Layout),它可以帮你精确查看每个对象在内存中的布局结构,包括对象头、字段偏移等,非常直观!
尾声:JVM世界的浪漫
小时候玩乐高,总觉得搭建世界很有趣。长大后写 Java,看着一行 new 背后也能搭建出一个对象的“生命”,这种感觉也挺浪漫的。
JVM 就像一个“对象托儿所”,每天处理成千上万的新生儿。而我们程序员,就像它的故事讲述者。
今天我们只是揭开了对象出生的一角。接下来,还可以聊聊:
- 对象逃逸分析是什么?
- 为什么有的对象能“栈上分配”?
- 对象什么时候进入老年代?
如果你也感兴趣,留言告诉我,下期我们继续聊!
我是小米,我们下次对象“探秘”再见!
END
如果你觉得本文对你有帮助,欢迎点赞、分享、在看,让更多人一起感受 JVM 的魅力!
我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!