大对象因无法在eden区一次性分配而直接进入老年代,需满足连续内存且超pretenuresizethreshold阈值,该参数仅对serial/parallel gc有效,设置不当会加速老年代耗尽。

大对象为什么会被直接分配到老年代
Java虚拟机对“大对象”的定义很实际:只要单个对象占用的连续内存空间超过 PretenureSizeThreshold(默认0,即不启用),且该对象无法在新生代 Eden 区一次性放下,JVM 就会跳过 Eden 和 Survivor,直接把它分配到老年代。
典型的大对象包括:超长字符串(new String(new char[1024 * 1024]))、大数组(new byte[2 * 1024 * 1024])、ArrayList 底层数组扩容后超出阈值等。不是所有“看起来大”的对象都会触发——关键看是否满足「连续内存 + 超过阈值」两个条件。
-
PretenureSizeThreshold必须显式设置才生效,单位是字节,且只对 Serial 和 Parallel GC 有效;G1 和 ZGC 不识别这个参数 - 对象大小包含对象头、字段数据、对齐填充,不是简单按字段算;用
jol(Java Object Layout)工具可精确查看 - 即使设置了阈值,如果老年代剩余空间不足,仍会触发 Full GC 或分配失败(
java.lang.OutOfMemoryError: Java heap space)
怎么判断一个对象算不算“大对象”
不能靠肉眼或代码里写个 if (obj.size() > 1MB) —— JVM 在分配前就通过类元信息和字段布局静态估算大小。真正起作用的是编译后对象的“实例数据宽度”,由字段类型和数量决定。
常见误判场景:
立即学习“Java免费学习笔记(深入)”;
- 引用类型的字段(如
String、List)不计入本对象大小,只算引用本身(通常 4 或 8 字节) - 子类继承父类字段,总大小是所有字段之和,含父类字段
- 数组对象本身小(比如
int[1000000]对象头+长度字段),但其元素数据区单独计算,整体被认定为大对象
验证方式:加 JVM 参数 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps,观察 GC 日志中是否出现类似 Allocation Failure (promotion failed) 或直接标记为 PSYoungGen 未参与分配。
Parallel GC 下开启大对象直接入老年代的实际配置
Parallel GC(也就是 -XX:+UseParallelGC)是唯一默认支持 PretenureSizeThreshold 的经典垃圾收集器。其他 GC 如 CMS 已废弃,G1 默认走“大对象区域(Humongous Region)”机制,逻辑完全不同。
- 必须搭配
-XX:+UseParallelGC使用,单独设PretenureSizeThreshold无效 - 推荐值设为略小于 Eden 区大小的一半,例如 Eden 是 512MB,可设
-XX:PretenureSizeThreshold=200M - 注意单位:支持
k/m/g后缀,但不要写空格,如-XX:PretenureSizeThreshold=2m正确,2 m报错 - 该参数不影响对象晋升年龄(
MaxTenuringThreshold),只影响初始分配位置
容易被忽略的兼容性与副作用
最常被绕过的点是:大对象直入老年代 ≠ 避免 GC 压力,反而可能加速老年代填满。尤其在频繁创建生命周期短的大对象时,老年代碎片化加剧,CMS 可能退化为 Serial Old,G1 可能触发并发模式失败(Concurrent Mode Failure)。
- 没有 Survivor 区参与,意味着这类对象完全跳过了“年轻代 GC 自动清理”环节,哪怕 1 秒后就没人引用了,也要等下一次老年代 GC 才能回收
- 使用
ByteBuffer.allocateDirect()创建的堆外内存不受此策略影响,它压根不在堆里 - Spring AOP 生成的 CGLIB 代理类、JSON 库反序列化的深层嵌套结构,可能隐式构造出大对象,但不会触发
PretenureSizeThreshold(因为它们是多个小对象组成的图,而非单个大数组)
真正需要关注的,不是“能不能放进去”,而是“放进去之后,老年代撑不撑得住、GC 频率会不会突变”。调参前务必用真实流量压测,别信理论值。










