一、堆内存整体结构与核心作用
堆内存是 JVM 在启动时创建的内存区域,唯一作用是存放对象实例,是垃圾收集器(GC)管理的核心区域。
从逻辑上,堆内存分为 年轻代(Young Generation) 和 老年代(Old Generation/Tenured Generation) 两大部分,两者默认比例为 1:2,可通过参数 -XX:NewRatio 调整(如 -XX:NewRatio=2 即强制年轻代:老年代 = 1:2)。
二、年轻代(Young Generation)
年轻代用于存储新创建的对象 ,其特点是 GC 频率高(Minor GC/Young GC)、对象存活率低(大部分对象会被快速回收)。
年轻代内部又细分为 1 个 Eden 区 和 2 个大小相等的 Survivor 区(From Survivor、To Survivor) ,默认空间比例为 8:1:1,可通过参数 -XX:SurvivorRatio=8 调整。
2.1 各区域核心功能
-
Eden 区
- 新对象优先在 Eden 区分配内存,是对象的 "诞生地"。
- 大对象可绕过 Eden 区直接进入老年代,阈值由参数
-XX:PretenureSizeThreshold控制。 - 当 Eden 区空间满时,会触发Minor GC。
-
Survivor 区(From Survivor + To Survivor)
- 两个 Survivor 区始终有一个为空,交替使用,核心作用是通过复制算法避免内存碎片。
- Minor GC 时,JVM 会将 Eden 区和当前非空的 From Survivor 区 中存活的对象,复制到空的 To Survivor 区。
- 每个存活对象会被分配一个年龄计数器 ,每次经历 Minor GC 并存活后,计数器值
+1。
2.2 年轻代对象晋升老年代的机制
当满足以下任一条件时,年轻代的对象会晋升至老年代:
- 对象年龄计数器达到阈值(默认 15,可通过
-XX:MaxTenuringThreshold调整); - Survivor 区中,相同年龄的对象总大小超过该区空间的一半,年龄≥该值的所有对象会直接晋升;
- To Survivor 区空间不足,无法容纳 Minor GC 后存活的对象,剩余对象会直接晋升。
2.3 Survivor 区交替工作流程
- 初始状态 :Eden 区满触发 Minor GC,Eden + From Survivor 的存活对象复制到 To Survivor,对象年龄
+1; - 角色切换:GC 完成后,From Survivor 变为空,To Survivor 存储存活对象;下一次 Minor GC 时,两者角色互换(From 变 To,To 变 From);
- 核心价值:通过复制算法让存活对象连续存储,彻底避免内存碎片;交替使用的设计无需额外空间存储全部存活对象,提升内存利用率。
三、老年代(Old Generation)
老年代用于存储存活时间较长的对象 (如缓存对象、单例对象),其特点是 GC 频率低(Major GC/Full GC)、对象存活率高。
3.1 对象进入老年代的场景
- 年轻代对象年龄达到晋升阈值;
- Survivor 区同年龄对象占比过半触发的批量晋升;
- 大对象直接进入老年代;
- Minor GC 后,Survivor 区空间不足导致的强制晋升。
3.2 老年代 GC 特点
- 老年代 GC 通常采用标记 - 整理算法,在回收垃圾的同时整理内存碎片,保证存活对象连续存储。
- 老年代 GC(Major GC)通常伴随 Full GC,会同时回收年轻代和老年代,STW(停止用户线程)时间较长,对应用性能影响较大。
四、线程本地分配缓存(TLAB)
TLAB 是 JVM 为每个线程 在新生代 Eden 区分配的私有缓存区域 ,核心目标是 减少多线程并发分配对象时的锁竞争。
4.1 底层实现机制
- JVM 启动时,为每个线程预分配一块 Eden 区的连续内存作为 TLAB,默认大小为 Eden 区的 1%,可通过
-XX:TLABSize手动调整。 - 线程创建对象时,优先在自己的 TLAB 内分配内存,仅需修改线程本地的指针,无锁操作,效率极高。
- 只有当 TLAB 空间不足时,线程才会去 Eden 区的共享区域分配内存,此时需要通过 CAS 操作或加锁保证线程安全。
4.2 TLAB 空间不足的处理流程
- 尝试扩容 TLAB:若 Eden 区剩余空间充足,JVM 会为当前线程扩容 TLAB,继续私有分配;
- 共享区域分配:若无法扩容,新对象直接在 Eden 区共享区域通过 CAS 操作分配内存;
- 触发 Minor GC:若 Eden 区共享区域也满,则触发 Minor GC,回收垃圾后再尝试分配;
- 晋升老年代:若 Minor GC 后仍无法分配(如对象过大),则将对象直接晋升至老年代;若老年代也无空间,触发 Full GC。
4.3 对象分配完整流程
线程创建对象(new XXX())
↓
检查当前线程的TLAB是否可用
↓ 可用
直接在TLAB内分配内存(无锁)→ 初始化对象头/OOP → 完成实例化
↓ 不可用(用完/不足)
向Eden区申请新的TLAB(CAS竞争)→ 分配成功则回到内存分配步骤
4.4 TLAB 与面向对象特性的关联
- 封装性:TLAB 是线程私有区域,内部对象内存物理隔离,外部线程无法直接访问,强化了对象的数据隐藏特性;JVM 严格控制 TLAB 内对象的内存布局(对象头 + 实例数据),保证数据不可被随意篡改。
- 继承性:子类对象需继承父类字段,实例化时需要更大的内存空间,TLAB 的无锁分配机制大幅提升子类对象的创建效率;TLAB 仅负责对象实例的内存分配,类的继承关系(父类指针)存储在方法区元数据中,两者解耦保证继承体系的灵活性。
- 多态性:多态的核心是运行时动态调用方法,而 TLAB 保证了高并发下大量子类实例的快速创建,为多态提供了 "实例基础";不同子类实例的对象头中,类元数据指针指向各自的元数据,JVM 可通过指针快速找到方法表,TLAB 从内存层面提升了动态分派的效率。
五、栈上分配与标量替换
栈上分配是 JVM 的编译期优化技术 ,核心目标是 避免对象分配到堆内存,减少 GC 压力。
5.1 栈上分配的核心条件
对象必须满足 未逃逸 的要求,即对象的作用域仅限于当前方法内部,未满足以下任一逃逸行为:
- 未被作为返回值返回给调用方;
- 未被存储到堆中的全局变量(如静态变量、集合);
- 未被外部方法引用或传递到其他线程。
同时还需满足:
- 对象体积较小,避免栈内存溢出;
- 对象所在方法为热点方法(被频繁调用),优化收益更高。
5.2 标量替换(栈上分配的底层实现)
- 标量定义 :标量是指不可再分的基本数据类型(如
int、long)或对象引用,与之相对的 "聚合量" 是指可以拆分为标量的对象。 - 核心逻辑:若对象未逃逸,且可拆分为标量,JVM 会直接将对象的字段拆分为局部变量,分配到当前线程的虚拟机栈中,而非创建完整的对象实例。
- 开启条件 :JDK 8 默认开启逃逸分析(
-XX:+DoEscapeAnalysis)和标量替换(-XX:+EliminateAllocations)。