在 Java 的世界中,“一切皆对象”。当我们编写一行紧凑的业务代码或启动一个高并发的服务时,成千上万的对象在 JVM 内部如同细胞般经历着诞生、成长、流转和消亡的循环。理解对象在 JVM 物理疆域中的完整生命轨迹,不仅是打通底层看透程序运行本质的必经之路,更是进行线上高阶内存调优的理论准绳。本文将层层剥茧,带你走入 JVM 最核心的“对象流转工厂”。
一、 对象的诞生:new 对象的 5 步底层流程
当我们在 Java 代码中执行 new Student() 时,从 JVM 字节码指令的视角来看,这绝非一个单一的原子操作,而是严格遵循以下 5 个步骤执行的实例化全过程:
1. 类加载检查
-
JVM 接收到
new指令时,首先会去方法区(元空间)的高频常量池中搜寻该符号引用,检查该符号引用所代表的类是否已经被加载、解析和初始化过。若没有,则必须先强制触发该类的完整加载全生命周期。
2. 分配内存
-
类加载检查通过后,对象所需的内存大小在类元信息确定时便已完全确定。JVM 此时需要在堆内存中划分出一块确定大小的物理空间安放该对象。根据堆内存是否绝对规整,JVM 采用两种完全不同的内存分配算法:
-
指针碰撞 (Bump-the-pointer):如果堆内存是绝对规整的(即用过的内存放在一边,空闲的内存放在另一边,中间由一个指针作为分界点),JVM 只需将分界指针向空闲内存方向挪动一段与对象大小相等的物理距离即可。
-
空闲列表 (Free List):如果堆内存中存活对象与垃圾交织,物理空间碎片化严重,JVM 必须维护一个内部列表,记录哪些内存块是可用的。分配时从列表中搜寻一块足够大的连续空间划分给对象,并同步更新列表记录。
-
3. 初始化零值
-
内存分配完毕后,JVM 会将分配到的内存空间(不包括对象头)内部的所有实例字段全部强制初始化为默认的零值。例如
int自动清零为0,boolean设为false,所有的对象引用(Reference)统一赋予null。这一步确保了 Java 的实例变量即使在业务代码中不赋初值,也能被程序安全、直接地调用,避免了野指针带来的底层安全漏洞。
4. 设置对象头 (Object Header)
-
JVM 需要为该块物理内存打上底层的身份标记,将其包装为真正的 Java 对象。对象头包含两大核心布局:
-
Mark Word(标记词):存储对象自身的运行时高频状态数据,如哈希码(HashCode)、锁状态标志、偏向线程ID、以及在生存淘汰赛中至关重要的 GC 分代年龄。
-
Klass Pointer(类型指针):指向元空间内该对象所属类的
Klass元数据地址,表明该对象具体是哪个类的实例。
-
5. 执行 <init> 构造器
-
在完成前 4 步后,从 JVM 的微观底层来看,一个合法的对象确实已经在内存中正式建立完毕了。但从开发者的宏观程序视角来看,对象的初始化才刚刚拉开帷幕。此时,字节码会紧接着执行
<init>方法,按照程序员编写的代码意图调用对应的构造方法,注入真正的初始业务数值,完成对象实例化过程的最后闭环。
二、 对象的分配:打破常识的“三道关卡”
在许多人的传统认知里,“new 出来的对象一定都在堆里分配物理空间”。为了追求极致的性能并减轻垃圾回收器的清扫负担,现代 JVM 重新编排了内存分配链路,设立了打破常识的三道过滤关卡。
第一关:首选栈上分配(最高级别优化)
-
底层机制:在动态编译热点代码时,JIT 编译器会开启逃逸分析 (Escape Analysis) 技术,深入剖析对象在方法体内部的动态作用域。
-
标量替换(Scalar Replacement):如果经过深度分析,确认该对象在创建后绝对没有逃逸出当前方法体(即不可能被外部方法或其余线程访问到),JVM 就会施展激进优化:它不再在堆中为其创建完整对象,而是将其打散拆解成一个个无法再拆的基本标量(基本数据类型),直接安插在当前线程虚拟机栈活动栈帧的局部变量表中。
-
架构优势:当方法执行结束、栈帧弹出时,这部分打散的对象物理内存随之瞬间自动销毁,整个生命周期完全不需要垃圾回收器(GC)的介入,达成了零 GC 压力的极限系统吞吐。
第二关:次选 TLAB 分配(VIP 快速通道)
-
底层机制:如果对象不幸发生了方法逃逸,或者体积不满足栈上分配的要求,它就必须进入堆内存。但堆内存是全线程共享的,在多线程高并发高负载下,多个线程同时在年轻代申请内存时,为了防止内存地址发生覆盖冲突,JVM 必须引入加锁同步机制,这会极大拖慢分配速度。
-
全程无锁设计:为了攻克这一瓶颈,JVM 在年轻代的 Eden 区中,为每一个新创建的线程都单独预先划分了一块专属的、固定大小的私有缓存区域,即 TLAB (Thread Local Allocation Buffer,线程本地分配缓存区)。
-
架构优势:当线程需要创建对象时,优先在属于自己的 TLAB 区域内划出空间。由于这块地盘在分配动作上是线程私有的,所以全程无锁,分配速度极快。(注:TLAB 仅在内存分配动作上私有,一旦对象分配完成,它在物理和逻辑上依然是堆内对全线程可见的共享数据)。
第三关:兜底常规堆分配
-
底层机制:当对象的体积过大,超出了当前线程专属 TLAB 的剩余容量,或者对象本身属于巨型数据时,VIP 通道宣告失效。此时,对象会触发兜底策略,通过悲观锁或原子性加锁同步机制,在全线程共享的 Eden 区(或老年代)中进行常规的物理空间申请。
三、 对象的流转:分代模型与 5 大晋升法则
一旦对象在年轻代的 Eden 区中常规落脚,它便正式开启了在 JVM 内存代际之间的生存流转淘汰赛。JVM 依靠精密的分代收集理论,利用 5 大核心流转法则,坚决把 90% 以上的“短命垃圾”彻底拍死在年轻代,极力阻止它们污染沉重的老年代。
1. 日常流转:Minor GC 与 8:1:1 复制清理策略
-
触发机制:年轻代中真正参与对象繁衍的是 Eden 区和其中一个 Survivor 区(如 S0)。当新对象不断涌入导致 Eden 区空间彻底宣告客满时,JVM 就会勒令暂停用户线程,触发一次 Minor GC (Young GC)。
-
复制算法流转:Minor GC 爆发时,垃圾回收器会以 GC Roots 为起点扫描整个年轻代,将 Eden 区和当前正在使用的 S0 区中所有依然活着的存活对象一次性集中物理拷贝到另一块完全空置的 Survivor 区(S1)中,随后将 Eden 和 S0 瞬间实施一扫而空的铁腕清空,存活对象的 GC 年龄随之整齐地
+1。 -
比例精髓:年轻代默认保持
8:1:1的经典比例,每次只闲置10%的 Survivor 空间作为备用复制放大区,将原本内存直接对折减半的传统复制算法空间可用率从 50% 完美拔高到了 90%,实现了性能与空间的最佳平衡。
2. 晋升法则一:岁月的痕迹(常规年龄达标晋升)
-
机制:如果一个对象生命力极其顽强,在两个 Survivor 区域之间来回复制、搬迁了多次,每次 Minor GC 都能化险为夷活下来,其对象头中的分代年龄就会稳步递增。
-
晋升红线:当对象的年龄达到了系统设定的晋升阈值红线(默认值为 15 岁,由配置参数
-XX:MaxTenuringThreshold严格控制)时,它就会被判定为长期稳定的“长寿对象”,正式脱离年轻代,晋升搬迁到长久居住的老年代中。 -
物理极限冷知识:为什么默认最高是 15 岁?因为在对象的底层物理结构中,对象头(Mark Word)中用来记录 GC 分代年龄的存储空间只有极其可怜的 4 个比特位 (4 bits)。4 位二进制所能表达的最大十进制数值就是
1111即 15。因此,受到硬件物理结构的封顶限制,对象的最大年龄红线绝不可能超越 15。
3. 晋升法则二:Survivor 空间严重不足(空间分配担保)
-
机制:在高并发大流量的极端业务场景下,当 Eden 区满触发 Minor GC 后,由于存活下来的对象数量呈爆发式剧增,导致此时完全空闲的那个 Survivor 接收区(10% 的物理空间)哪怕被挤满也无法完全塞下这些活着的资产。
-
越级空降:此时,JVM 启动空间分配担保机制,这些 Survivor 区域实在装不下的、多出来的存活对象,根本无需等待年龄达标,直接通过绿色通道越级空降、直接晋升到老年代中。
-
担保校验逻辑:在发生 Minor GC 之前,JVM 会提前严密检查老年代最大连续可用的空间,是否大于历次年轻代晋升到老年代对象的平均大小。如果发现老年代“接不住”可能空降的大批对象,说明担保失败,JVM 会提前紧急改变策略,强行先触发一次代价沉重、回收全空间的 Full GC 彻底倒出空间,以防系统在 Minor GC 后因空降失败而直接发生严重的 OOM 崩溃。
4. 晋升法则三:动态年龄判断(高并发高频 Full GC 的隐形元凶)
-
触发公式:在年轻代的 Survivor 空间中,JVM 并不是机械地非要等到 15 岁才准许晋升。如果在当前使用的 Survivor 区域内,某一年龄段的所有存活对象体积的总大小,已经超过了该 Survivor 区域总容量的 50%(由参数
-XX:TargetSurvivorRatio控制),那么一条隐形的年龄红线瞬间生成:$$\text{Sum}(\text{Size of Objects with Age} \le X) > \text{SurvivorSize} \times 50\%$$
-
集体晋升:此时,年轻代中所有年龄大于或等于该年龄 $X$ 的存活对象,根本不需要等到 15 岁,全部集体触发绿色通道,直接越级晋升到老年代中。这一法则是为了防止长命对象在 Survivor 之间高频复制产生沉重的 CPU 物理拷贝损耗,但若 Survivor 分配过小,会导致大量短命对象混入老年代,强行把老年代提早撑爆,诱发频繁的 Full GC 线上惨剧。
5. 晋升法则四:巨型大对象直接降落
-
机制:当系统创建了一个体积十分庞大的巨型对象(例如极长的一段文本字符串,或者容量极深的一个海量实体类
List/ 字节数组)时,如果任由其呆在年轻代,由于年轻代 GC 频繁,这尊“大佛”在 Eden 和两个 Survivor 之间高频搬家会产生极其恐怖的 CPU 物理拷贝带宽开销。 -
一步到位:JVM 专门开辟了大对象直达专线:只要对象大小超过了指定的字节阈值(由参数
-XX:PretenureSizeThreshold控制),对象在创建诞生的一瞬间,就会直接绕过年轻代,一步到位分配在老年代空间中,稳居后方。
四、 对象的消亡与生命周期的 7 个状态阶段
从微观层面看,一个对象从诞生到物理空间被彻底擦除,在 HotSpot JVM 内部其实精细地横跨了以下 7 个完备的状态阶段:
-
Created(创建状态):物理空间已分配,默认零值已刷入,对象头打上标记,程序员自定义的
<init>构造器刚好执行完毕。 -
In Use(应用状态):对象至少被一个系统内的强引用(Strong Reference)链路牢牢持有,正在承载正常的业务逻辑访问。
-
Invisible(不可见状态):虽然对象还残留在堆内存中,但代码中的局部变量已经超出了作用域范围(例如退出了声明该变量的
if块),程序代码通过常规手段再也无法捕捉或访问到该引用,但它依然还没被垃圾回收器清扫。 -
Unreachable(不可达状态):对象的强引用链路与系统的核心根节点彻底断开,在垃圾回收器开启的可达性分析算法中,对象被判定为游离的孤岛,正式列入可回收的“垃圾候选人”名单。
-
Collected(收集状态):垃圾回收器执行清扫,该对象被明确标记,并锁定了其所在的物理内存块,等待执行最终的清除擦除动作。
-
Finalized(终结状态):对象的
finalize()方法被系统自动调度并执行。(注:在此阶段,对象极少见地可以通过在 finalize 方法中重新将自己挂载到某个活着的强引用变量上实现“诈尸复活”,但由于该方法执行线程优先级极低且不确定,Java 官方极不推荐使用并已废弃)。 -
Deallocated(释放状态):垃圾回收器切断最后一道物理防线,将该对象所占用的堆物理内存地址彻底擦除并重新宣告归还给系统,内存空间重回规整状态或重新写入空闲列表,生命周期正式宣告终结。
&spm=1001.2101.3001.5002&articleId=161409360&d=1&t=3&u=5cde1b44fe06409aaeec079e947aaab5)
730

被折叠的 条评论
为什么被折叠?



