目录
1、介绍
2、内存结构
2.1、普通对象
2.2、数组对象
2.3、数组长度作用
2.4、为什么 age 用 4 位?
3、对象头组成
3.1、Mark Word(标记字段)
3.2、Class Pointer(类指针)
4、GC 发生的位置
4.1、新生代
4.2、老年代
4.3、元空间(Metaspace,Java 8+)
5、对象晋升
5.1、15 次 Minor GC
5.2、动态年龄判定
5.3、Survivor区存活时间
5.4、修改晋升年龄阈值
6、更长的存活的方案
6.1、扩大 Survivor 区大小
6.2、G1 垃圾回收器
前言
在 Java 中,GC 年龄(GC Age 或 Tenuring Threshold) 是用于衡量对象在 年轻代(Young Generation) 中经历垃圾回收(GC)次数的指标。GC 年龄决定了对象何时从年轻代晋升到老年代(Old Generation),是 Java 垃圾回收机制中重要的优化策略之一。
不同垃圾回收器的处理方式如下:
1、Serial/Parallel Scavenge:
使用经典的 复制算法,对象在 Survivor 区之间来回复制,GC 年龄逐步增加。晋升到老年代的逻辑与上述规则一致。
2、G1(Garbage-First):
1.分区机制:
年轻代划分为多个 Region,对象晋升到老年代的 Region。
2.年龄表(Age Table):
G1 维护每个 Region 的年龄表,记录对象的存活情况和 GC 年龄分布。
3.动态调整晋升阈值:
G1 会根据 Region 的填充情况动态调整晋升策略,优化吞吐量和延迟。
3、CMS:
与 Serial/Parallel 类似,但 CMS 更关注减少 Full GC 的停顿时间,晋升策略会更保守。
1、介绍
关于java对象,存放在jvm的堆内存里面,在jdk1.8版本后,如下图所示:
通过合理配置和监控 GC 年龄,可以显著提升 Java 应用的性能和稳定性。
2、内存结构
如下图所示:
2.1、普通对象
- 对象头:
- Mark Word(64 位)
- Class Pointer(64 位,或 32 位,若启用指针压缩)
- 实例数据:
- 存储对象的字段值(如
int
、String
等)
- 存储对象的字段值(如
- 对齐填充:
- 补齐到 8 字节边界
2.2、数组对象
- 对象头:
- Mark Word(64 位)
- Class Pointer(64 位,或 32 位,若启用指针压缩)
- Array Length(32 位,固定 4 字节)
- 实例数据:
- 存储数组元素(如
int[10]
会存储 10 个int
)
- 存储数组元素(如
- 对齐填充:
- 补齐到 8 字节边界
区别:
2.3、数组长度作用
1.数组的元数据:
数组需要知道自己的长度(元素个数),而普通对象的字段数量和类型由类定义决定,不需要在对象头中存储。
2.内存管理:
数组的元素是连续存储的,JVM 需要通过长度字段快速访问元素(如 array[index]
)。
2.4、为什么 age
用 4 位?
如下图所示:
1. 4 位二进制数的限制
age
字段在 Mark Word 中仅占 4 位,因此最大可表示的值为2^4−1=15。- 当对象在 Survivor 区中经历 15 次 Minor GC 后,其
age
达到 15,会被晋升到老年代。
2. 设计权衡
- 节省空间:Mark Word 的总大小是固定的(64 位或 32 位),每个字段的位数需要精打细算。
age
只用 4 位,避免浪费。 - 满足需求:大多数应用场景中,对象的存活周期不会超过 15 次 Minor GC。如果超过,直接晋升到老年代即可。
- 兼容性:若
age
需要更多位(如 5 位),则需重新设计 Mark Word 的布局,可能影响其他字段(如哈希码、锁状态)的存储。
3、对象头组成
关于mark word的数据结构可参考:对于Synchronized和Volatile的深入理解-CSDN博客
Mark Word 的结构如下所示(以 64 位 JVM 为例):
锁标志位可分为:
-
00:轻量级锁
-
01:无锁/偏向锁
-
10:重量级锁
-
11:GC标记
对象头(Object Header)是对象在内存中的元数据区域,通常由两部分组成:
- Mark Word:存储对象的运行时数据,如哈希码、锁状态、GC 分代年龄(age)等。
- Class Pointer:指向对象所属类的元数据(如
java.lang.Class
)。
3.1、Mark Word(标记字段)
关于mark word可以参考:对于Synchronized和Volatile的深入理解-CSDN博客
- 作用:存储对象的运行时数据,例如:
- 哈希码(Hash Code)
- 锁状态(无锁、偏向锁、轻量级锁、重量级锁)
- GC 分代年龄(age)
- 偏向线程 ID
- 同步锁信息
- 大小:
- 32 位 JVM:32 位
- 64 位 JVM:64 位(可启用压缩指针优化为 32 位)
3.2、Class Pointer(类指针)
- 作用:指向对象所属类的元数据(即
java.lang.Class
对象),用于访问类的静态信息(如方法、字段等)。 - 大小:
- 32 位 JVM:32 位
- 64 位 JVM:64 位(可启用压缩指针优化为 32 位)
4、GC 发生的位置
在 JVM 中,内存被划分为不同的区域,不同区域的 GC 行为和触发条件不同:
如下图所示:
4.1、新生代
- 组成:Eden 区 + 两个 Survivor 区(From 和 To)。
- GC 类型:Minor GC(年轻代 GC)。
- 触发条件:
- 当 Eden 区分配对象时,空间不足。
- 新生代中的对象存活时间较短,Minor GC 频繁发生。
4.2、老年代
- 组成:存放长期存活的对象。
- GC 类型:Major GC / Full GC。
- 触发条件:
- 老年代空间不足。
- Minor GC 后对象无法放入 Survivor 区,需要晋升到老年代,但老年代空间不足。
- 元空间(Metaspace)不足(在 Java 8+ 中)。
- 显式调用
System.gc()
。
4.3、元空间(Metaspace,Java 8+)
- 作用:存储类的元数据(如类定义、方法信息等)。
- GC 类型:Full GC(当元空间不足时)。
5、对象晋升
而对于<jdk1.8:还有持久代。对于>=jdk1.8: metaspace。
如下图所示:堆结构在分配内存空间的时候:老年代占比2/3,新生代1/3。
而新生代里面eden和from、to区的内存占比分别为8:1:1。
关于 GC(垃圾回收)发生的位置 以及 “15次”的含义,可以从 JVM 的内存结构和 GC 策略两个角度来解释。
5.1、15 次 Minor GC
Minor GC 仅发生在 年轻代(Young Generation),即 Eden 区 + Survivor 区(From + To)。
15次 Minor GC 是对象在 Survivor 区 中存活的次数,不包括老年代的 Full GC。
1. 对象晋升规则
- 对象在 Eden 区分配后,经历 Minor GC:
- 如果存活,则被复制到 Survivor 区。
- 每次 Minor GC 后,对象的
age
加 1。 - 当
age
达到MaxTenuringThreshold
(默认 15)时,对象晋升到老年代。
Survivor1 和 Survivor2 区域之间的对象迁移是通过“标记-复制”算法实现的。
2. 4 位二进制数的硬性限制
age
最大只能是 15,因此 15 次 Minor GC 是 JVM 的硬性设计限制。- 如果需要更大的
age
,必须扩展 Mark Word 的位数,但会牺牲其他字段的空间。
5.2、动态年龄判定
触发条件:
如果某个年龄段的对象总大小超过 Survivor 区的 50%,即使未达到最大年龄阈值,也会被直接晋升到老年代。
例如:如果年龄为 3 的对象总大小超过 Survivor 区的 50%,则所有年龄 >=3 的对象都会晋升。
5.3、Survivor区存活时间
- 存活时间:对象在 Survivor 区的存活时间取决于其 年龄增长速度 和 晋升条件。
1.最短时间:
如果对象在第一次 Minor GC 后就晋升到老年代(如动态年龄判定触发),则仅存活 1 次 Minor GC。
2.最长时间:
如果对象一直存活到年龄达到 MaxTenuringThreshold
,则最多存活 MaxTenuringThreshold
次 Minor GC。
默认情况下,最多存活 15 次 Minor GC。
5.4、修改晋升年龄阈值
可以通过 JVM 参数调整对象晋升的年龄阈值:
-XX:MaxTenuringThreshold=15
- 如果尝试设置
MaxTenuringThreshold=20
,JVM 会将其 自动调整为 15,并可能输出警告或忽略该设置。 - 示例 JVM 输出:
[Warning] MaxTenuringThreshold is set to 20, but it will be adjusted to 15 due to 4-bit limit.
6、更长的存活的方案
JVM 会根据 Survivor 区的使用情况动态调整晋升阈值:
如果 Survivor 区使用率低,JVM 会适当提高阈值,让对象多经历几次 Minor GC 再晋升。
如果 Survivor 区使用率高,JVM 会降低阈值,让对象提前晋升到老年代。
可以通过以下方式间接实现:
6.1、扩大 Survivor 区大小
- 参数:
-XX:SurvivorRatio=N
- 默认值为 8,表示 Eden 区与 Survivor 区的比例为 8:1:1。
- 例如,
-XX:SurvivorRatio=4
会分配更大的 Survivor 区,容纳更多存活对象,从而减少晋升频率。
6.2、G1 垃圾回收器
G1(Garbage-First) 不依赖固定大小的 Survivor 区,而是通过分区(Region)管理内存,对对象晋升的控制更灵活。
可以通过 -XX:G1MixedGCCountTarget
等参数优化晋升策略。
G1通过统计每个Region的垃圾密度(Garbage Fraction),预测回收收益。存活时间长的对象所在的Region若垃圾密度低,回收优先级较低,从而减少不必要的回收操作。
G1在Full GC前会优先回收老年代中垃圾比例高的Region(Garbage-First策略)。
总结:
总结
通过这种设计,JVM 在对象头空间有限的情况下,平衡了性能、内存效率和功能需求。
参考文章:
1、对于Synchronized和Volatile的深入理解-CSDN博客
2、关于对JVM的知识整理_谈谈你对jvm的理解-CSDN博客