新对象在 JVM 中的内存分配是一个多层级协作的过程,其核心流程如下(以主流的 分代收集算法 为例):
一、分配流程总览
1 | graph TD |
二、关键步骤详解
1. 逃逸分析与栈上分配(优先尝试)
- 条件:
- 开启逃逸分析(
-XX:+DoEscapeAnalysis,默认开启) - 对象未逃出当前方法作用域(无外部引用)
- 开启逃逸分析(
- 分配位置:直接分配在 当前线程栈帧 上
- 优势:
- 自动随栈帧销毁回收(无GC压力)
- 无锁竞争(线程私有)
- 伪代码示例:
1
2
3void createLocalObj() {
Object localObj = new Object(); // 未逃逸 → 栈上分配
} // 方法结束,对象自动销毁
⚠️ 若对象逃逸(如作为返回值或赋值给静态变量),则跳过此步骤。
2. TLAB 快速分配(第二优先级)
- TLAB 机制:
- 每个线程在 Eden 区 预先划出私有内存块(Thread Local Allocation Buffer)
- 大小通过
-XX:TLABSize控制(默认自适应)
- 分配逻辑:
1
2
3
4
5if (对象大小 <= TLAB剩余空间) {
在TLAB内移动指针分配; // 无锁操作(仅原子指针后移)
} else {
尝试申请新TLAB或直接进入共享Eden区;
} - 优势:避免多线程竞争锁(提升高并发下分配效率)
3. 共享 Eden 区分配(常规路径)
- 流程:
- 若对象大小未超过
-XX:PretenureSizeThreshold(默认0),进入新生代 Eden 区 - 通过 指针碰撞(Bump The Pointer) 或 空闲列表(Free List) 分配:
- 指针碰撞(堆内存规整时):
1
2addr = eden_top; // 获取当前Eden顶部地址
eden_top += obj_size; // 指针后移(CAS操作保证原子性) - 空闲列表(内存不规整时):从空闲内存块链表中查找足够空间
- 指针碰撞(堆内存规整时):
- 若 Eden 区空间不足,触发 Minor GC(年轻代垃圾回收)
- 若对象大小未超过
4. 大对象直接入老年代(避免复制开销)
- 条件:
- 对象大小 ≥
-XX:PretenureSizeThreshold(默认0,需手动设置如-XX:PretenureSizeThreshold=4M) - 或对象为巨型数组(JVM 自动判断)
- 对象大小 ≥
- 分配位置:直接在 老年代 分配
- 原因:避免大对象在 Eden 区及 Survivor 区频繁复制带来的性能损耗
三、底层技术支撑
| 技术 | 作用 | 相关参数 |
|---|---|---|
| 逃逸分析 | 决定能否栈上分配 | -XX:+DoEscapeAnalysis |
| TLAB | 线程私有分配缓冲区减少锁竞争 | -XX:+UseTLAB, -XX:TLABSize |
| 指针碰撞 | 快速分配连续内存(需内存规整) | -XX:+UseCompactAtFullCollection |
| 空闲列表 | 管理碎片化内存(CMS收集器常用) | -XX:+UseCMSCompactAtFullCollection |
| 大对象阈值 | 直接进入老年代 | -XX:PretenureSizeThreshold |
四、对象分配流程图解
1 | graph LR |
五、关键注意事项
- TLAB 并非完全无锁:
- 当 TLAB 用尽时仍需竞争共享区的锁(通过
-XX:TLABWasteTargetPercent调节浪费比例)
- 当 TLAB 用尽时仍需竞争共享区的锁(通过
- 堆内存规整性依赖 GC 器:
- Serial/ParNew 等用 标记-复制 算法 → 内存规整 → 支持指针碰撞
- CMS 用 标记-清除 → 内存碎片 → 需空闲列表
- 空间分配担保风险:
- Full GC 后仍无法分配 →
OutOfMemoryError: Java heap space
- Full GC 后仍无法分配 →
💡 调优建议:在高并发应用中,通过
-XX:TLABSize增大 TLAB 可显著提升分配效率,同时监控-XX:+PrintTLAB日志优化浪费阈值。
空间分配担保
空间分配担保(Promotion Handling) 是 JVM 在 Minor GC 触发前,为确保存活对象能安全进入老年代而设计的 风险评估机制。其核心目的是在年轻代空间不足时,通过老年代的空间兜底来避免频繁 Full GC。
一、核心流程与规则
当发生 Minor GC 前,JVM 会执行如下判断:
1 | graph LR |
📌 关键阈值:
担保成功需满足:老年代连续可用空间 ≥ 新生代所有对象总大小
(若开启-XX:HandlePromotionFailure=true,则放宽为 ≥ 历次晋升老年代的平均大小)
二、三个核心阶段详解
阶段 1:GC 前风险检查
- 检查条件:
Minor GC前发现 Eden + From Survivor 区空间不足 - 评估标准:
判断老年代是否 有能力容纳以下两者的最大值:- 当前新生代所有存活对象(若全晋升)
- 历史平均晋升大小(若开启
HandlePromotionFailure)
- 决策逻辑:
- 若老年代空间 足够 → 允许进行 Minor GC
- 若老年代空间 不足 → 直接触发 Full GC(避免 Minor GC 失败)
⚠️ 注:JDK 7+ 默认忽略
HandlePromotionFailure参数(始终做严格检查)
阶段 2:Minor GC 执行
- 存活对象分配策略:
- 若存活对象 ≤ Survivor 区容量 → 进入 Survivor 区(年龄+1)
- 若存活对象 > Survivor 区容量 → 提前晋升至老年代
阶段 3:担保失败处理
若 Minor GC 后发现存活对象太多 → 老年代实际空间不足 → 触发 Full GC
(最坏场景:Full GC 后仍不足 → OOM: Java heap space)
三、参数与调优
| 参数 | 作用 | 默认值 |
|---|---|---|
-XX:HandlePromotionFailure |
是否允许根据历史记录放宽担保条件(JDK6用,7+废弃) | true (JDK6) |
-XX:MaxTenuringThreshold |
对象最大晋升年龄(超过直接进老年代) | 15 |
-XX:SurvivorRatio |
Eden 与 Survivor 区比例(如 8 表示 Eden:Survivor=8:1) |
8 |
四、空间分配担保的意义
- 避免无效 Minor GC
若老年代无法兜底存活对象,提前触发 Full GC 比 Minor GC 失败再 Full GC 更高效 - 减少 Full GC 频率
担保成功后 Minor GC 可安全执行,避免直接 Full GC - 防止过早 OOM
严格检查确保存活对象有去向(若完全不担保,Minor GC 后存活对象无处存放直接 OOM)
五、调优建议
- 监控老年代空闲比例
确保老年代剩余空间 > 新生代总容量 × 1.2(安全余量)1
jstat -gcutil <pid> 1000 # 监控 Old 区利用率
- 调整 Survivor 区容量
增大-XX:SurvivorRatio或使用-XX:+UseAdaptiveSizePolicy(默认启用) - 控制对象晋升速率
- 降低
-XX:MaxTenuringThreshold(减少对象在年轻代停留) - 排查内存泄漏(避免过多本应回收的对象进入老年代)
- 降低
💡 关键原则:空间分配担保是 JVM 的自救策略,但根治方案仍是 合理分配堆大小 与 优化代码内存占用。
