内存分配与回收原则
对象内存分配策略概述
JVM在为对象分配内存时,遵循一系列优化策略,这些策略旨在提高内存利用率、减少垃圾回收频率,并提升应用性能。
graph TB
CreateObj[创建对象] --> CheckEscape{逃逸分析}
CheckEscape -->|未逃逸| StackAlloc[栈上分配<br/>标量替换]
CheckEscape -->|发生逃逸| CheckSize{检查对象大小}
CheckSize -->|大对象| CheckThreshold{超过<br/>PretenureSizeThreshold?}
CheckSize -->|普通对象| CheckTLAB{TLAB有空间?}
CheckThreshold -->|是| OldGen[直接进入老年代]
CheckThreshold -->|否| CheckTLAB
CheckTLAB -->|是| TLAB[在TLAB中分配]
CheckTLAB -->|否| Eden[在Eden区分配]
TLAB --> YoungGC{Eden满?}
Eden --> YoungGC
YoungGC -->|是| GC[触发Young GC]
YoungGC -->|否| Success[分配成功]
GC --> Survive{对象存活?}
Survive -->|否| Reclaim[对象回收]
Survive -->|是| CheckAge{年龄判断}
CheckAge -->|达到阈值| OldGen
CheckAge -->|未达阈值| Survivor[移到Survivor区]
style StackAlloc fill:#51cf66,stroke:#2f9e44,stroke-width:2px,rx:15,ry:15
style OldGen fill:#ffa94d,stroke:#e67700,stroke-width:2px,rx:15,ry:15
style TLAB fill:#74c0fc,stroke:#1971c2,stroke-width:2px,rx:15,ry:15
style Eden fill:#b197fc,stroke:#7950f2,stroke-width:2px,rx:15,ry:15
style Survivor fill:#a9e34b,stroke:#74b816,stroke-width:2px,rx:15,ry:15
style Reclaim fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px,rx:15,ry:15内存分配原则
1. 对象优先在Eden区分配
绝大多数情况下,新创建的对象首先在新生代的Eden区分配内存。
基本分配流程
public class EdenAllocationDemo {
public static void main(String[] args) {
// JVM参数: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
// 堆大小20M,新生代10M,Eden:S0:S1 = 8:1:1,即Eden约8M
byte[] array1 = new byte[2 * 1024 * 1024]; // 2MB,在Eden分配
byte[] array2 = new byte[2 * 1024 * 1024]; // 2MB,在Eden分配
byte[] array3 = new byte[2 * 1024 * 1024]; // 2MB,在Eden分配
// 此时Eden已使用约6MB,还剩约2MB空间
byte[] array4 = new byte[3 * 1024 * 1024]; // 3MB
// Eden空间不足,触发Young GC
// array1、array2、array3无引用,被回收
// array4在Eden分配成功
}
}运行结果分析:
[GC (Allocation Failure) [PSYoungGen: 7127K->808K(9216K)] 7127K->6952K(19456K), 0.0034568 secs]解读:
PSYoungGen: 7127K->808K(9216K): 新生代从7127K降到808K7127K->6952K(19456K): 整个堆从7127K变为6952K0.0034568 secs: GC耗时3.4毫秒
graph LR
subgraph GC前
Eden1[Eden<br/>array1 2MB<br/>array2 2MB<br/>array3 2MB<br/>总计6MB]
S01[Survivor 0<br/>空]
S11[Survivor 1<br/>空]
end
subgraph GC后
Eden2[Eden<br/>array4 3MB]
S02[Survivor 0<br/>空]
S12[Survivor 1<br/>空]
end
Eden1 -.Young GC.-> Eden2
style Eden1 fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px,rx:15,ry:15
style Eden2 fill:#51cf66,stroke:#2f9e44,stroke-width:2px,rx:15,ry:15
style S01 fill:#e9ecef,stroke:#adb5bd,stroke-width:1px,rx:15,ry:15
style S11 fill:#e9ecef,stroke:#adb5bd,stroke-width:1px,rx:15,ry:15
style S02 fill:#e9ecef,stroke:#adb5bd,stroke-width:1px,rx:15,ry:15
style S12 fill:#e9ecef,stroke:#adb5bd,stroke-width:1px,rx:15,ry:15Eden分配的优势
优点:
- 新生代GC速度快(复制算法)
- 大部分对象生命周期短,可快速回收
- 减少老年代的垃圾积累
适用对象:
- 临时对象(方法内局部变量)
- 生命周期短的业务对象
- 小对象(< PretenureSizeThreshold)
2. 大对象直接进入老年代
大对象定义: 需要大量连续内存空间的对象,如长字符串、大数组。
大对象处理机制
大对象直接进入老年代是JVM的动态优化策略,旨在避免:
- Eden区频繁GC
- 对象在Survivor区之间来回复制(复制成本高)
- Survivor区空间不足
public class LargeObjectDemo {
public static void main(String[] args) {
// JVM参数: -Xms20M -Xmx20M -XX:+PrintGCDetails
// -XX:PretenureSizeThreshold=3145728 (3MB)
// 创建4MB的大对象
byte[] largeArray = new byte[4 * 1024 * 1024];
// 超过阈值,直接在老年代分配,不触发Young GC
}
}INFO
-XX:PretenureSizeThreshold参数仅对Serial和ParNew收集器有效,对Parallel Scavenge收集器无效。Parallel Scavenge会根据运行时数据动态决定大对象阈值。
不同收集器的大对象处理
Serial / ParNew收集器:
# 显式设置大对象阈值为3MB
-XX:+UseParNewGC
-XX:PretenureSizeThreshold=3145728Parallel Scavenge收集器:
- 无固定阈值参数
- 根据堆内存情况和历史数据动态决定
- 更智能但不可精确控制
G1收集器:
# G1根据Region大小决定
-XX:G1HeapRegionSize=4M # 设置Region大小
# 对象大小超过Region的50%,进入Humongous区(特殊老年代区域)graph TB
subgraph Serial_ParNew
Check1{对象大小 > 阈值?}
Check1 -->|是| Old1[直接进老年代]
Check1 -->|否| Eden1[进入Eden区]
end
subgraph Parallel_Scavenge
Check2{动态阈值判断}
Check2 -->|大对象| Old2[直接进老年代]
Check2 -->|普通对象| Eden2[进入Eden区]
end
subgraph G1
Check3{对象大小 > Region的50%?}
Check3 -->|是| Humongous[进入Humongous区]
Check3 -->|否| Eden3[进入Eden区]
end
style Old1 fill:#ffa94d,stroke:#e67700,stroke-width:2px,rx:15,ry:15
style Old2 fill:#ffa94d,stroke:#e67700,stroke-width:2px,rx:15,ry:15
style Humongous fill:#ff8787,stroke:#e03131,stroke-width:2px,rx:15,ry:15
style Eden1 fill:#74c0fc,stroke:#1971c2,stroke-width:2px,rx:15,ry:15
style Eden2 fill:#74c0fc,stroke:#1971c2,stroke-width:2px,rx:15,ry:15
style Eden3 fill:#74c0fc,stroke:#1971c2,stroke-width:2px,rx:15,ry:15避免大对象的实践
// ❌ 不推荐: 创建大字符串
public String processLargeData() {
String result = "";
for (int i = 0; i < 100000; i++) {
result += "data_" + i; // 频繁创建大字符串
}
return result;
}
// ✅ 推荐: 使用StringBuilder
public String processLargeData() {
StringBuilder sb = new StringBuilder(100000 * 10);
for (int i = 0; i < 100000; i++) {
sb.append("data_").append(i);
}
return sb.toString();
}
// ✅ 推荐: 分批处理大数组
public void processLargeArray() {
int batchSize = 1024;
int totalSize = 1024 * 1024;
for (int i = 0; i < totalSize; i += batchSize) {
byte[] batch = new byte[batchSize]; // 小批次处理
process(batch);
// batch在方法结束后可被快速回收
}
}3. 长期存活的对象进入老年代
JVM通过**对象年龄(Age)**机制,判断对象是否应晋升到老年代。
对象年龄机制
年龄计数:
- 对象在Eden区创建,年龄为0
- 经历一次Young GC存活后,移到Survivor区,年龄设为1
- 在Survivor区每经历一次Young GC,年龄+1
- 年龄达到阈值时,晋升到老年代
public class AgingDemo {
// JVM参数: -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
private static List<byte[]> keepAlive = new ArrayList<>();
public static void main(String[] args) {
// 创建对象并保持引用,使其经历多次GC
for (int i = 0; i < 20; i++) {
byte[] array = new byte[256 * 1024]; // 256KB
keepAlive.add(array);
// 触发Young GC
byte[] temp = new byte[5 * 1024 * 1024]; // 5MB
System.out.println("Round " + i + " completed");
}
}
}年龄阈值参数:
# 设置最大晋升年龄(默认15)
-XX:MaxTenuringThreshold=15
# CMS收集器默认为6
-XX:+UseConcMarkSweepGC # CMS默认MaxTenuringThreshold=6
# 打印对象年龄分布
-XX:+PrintTenuringDistributionINFO
对象头的Mark Word中,GC年龄字段仅占4位,最大值为15(二进制1111)。
graph LR
subgraph 对象生命周期
Eden[Eden区<br/>年龄:0] --> GC1[经历GC 1]
GC1 --> S1[Survivor区<br/>年龄:1]
S1 --> GC2[经历GC 2]
GC2 --> S2[Survivor区<br/>年龄:2]
S2 --> GC3[...]
GC3 --> S3[Survivor区<br/>年龄:15]
S3 --> GC4[经历GC]
GC4 --> Old[老年代]
end
style Eden fill:#74c0fc,stroke:#1971c2,stroke-width:2px,rx:15,ry:15
style S1 fill:#a9e34b,stroke:#74b816,stroke-width:2px,rx:15,ry:15
style S2 fill:#a9e34b,stroke:#74b816,stroke-width:2px,rx:15,ry:15
style S3 fill:#ffd43b,stroke:#f59f00,stroke-width:2px,rx:15,ry:15
style Old fill:#ffa94d,stroke:#e67700,stroke-width:2px,rx:15,ry:15动态年龄判定
JVM并非严格按MaxTenuringThreshold晋升对象,而是采用动态年龄判定机制:
判定规则: 如果Survivor区中相同年龄所有对象的总大小 > Survivor空间 × TargetSurvivorRatio(默认50%),则将该年龄及以上的对象晋升到老年代。
// HotSpot源码中的动态年龄计算逻辑(简化版)
int computeTenuringThreshold(Survivor survivor) {
int maxThreshold = MaxTenuringThreshold;
long targetSize = survivor.capacity() * TargetSurvivorRatio / 100;
long cumulativeSize = 0;
for (int age = 1; age <= maxThreshold; age++) {
cumulativeSize += survivor.getSizeOfAge(age);
// 累计大小超过目标大小,提前晋升
if (cumulativeSize > targetSize) {
return Math.min(age, maxThreshold);
}
}
return maxThreshold;
}示例场景:
假设:
- Survivor区大小: 10MB
TargetSurvivorRatio: 50%- 目标大小: 10MB × 50% = 5MB
对象年龄分布:
- 年龄1: 2MB
- 年龄2: 2MB
- 年龄3: 2MB (累计6MB > 5MB)
- 年龄4: 1MB
结果: 年龄阈值动态调整为3,年龄≥3的对象晋升到老年代。
graph TB
Start[Survivor对象] --> Age1[年龄1: 2MB<br/>累计:2MB]
Age1 --> Age2[年龄2: 2MB<br/>累计:4MB]
Age2 --> Age3[年龄3: 2MB<br/>累计:6MB]
Age3 --> Check{累计 > 5MB?}
Check -->|是| Promote[年龄≥3的对象<br/>晋升到老年代]
Age3 --> Age4[年龄4: 1MB]
Age4 --> Promote
style Age1 fill:#74c0fc,stroke:#1971c2,stroke-width:2px,rx:15,ry:15
style Age2 fill:#74c0fc,stroke:#1971c2,stroke-width:2px,rx:15,ry:15
style Age3 fill:#ffd43b,stroke:#f59f00,stroke-width:2px,rx:15,ry:15
style Age4 fill:#ff8787,stroke:#e03131,stroke-width:2px,rx:15,ry:15
style Promote fill:#ffa94d,stroke:#e67700,stroke-width:3px,rx:15,ry:15调整参数:
# 设置Survivor目标使用率(默认50%)
-XX:TargetSurvivorRatio=60
# 降低值:对象更容易提前晋升,减少Survivor压力
# 提高值:对象在新生代停留更久,可能增加Young GC次数4. 空间分配担保机制
目的: 确保在Minor GC之前,老年代有足够的空间容纳新生代可能晋升的所有对象。
担保机制原理
空间分配担保是JVM在进行Minor GC前的一项安全检查机制。由于新生代采用复制算法,存活对象可能需要晋升到老年代,因此必须确保老年代有足够的空间来接收这些对象。
为什么需要担保?
在Minor GC发生前,JVM无法准确预知有多少对象会存活并晋升到老年代。如果贸然执行Minor GC,可能出现:
- Survivor区无法容纳所有存活对象
- 老年代空间不足以接收晋升对象
- 导致担保失败,触发Full GC
JDK版本差异
空间分配担保的检查策略在不同JDK版本中有所变化:
JDK 6 Update 24之前的策略:
- 第一步检查: 老年代最大可用连续空间 > 新生代所有对象总空间?
- 是 → Minor GC安全,直接执行
- 否 → 进入第二步
- 第二步检查:
-XX:HandlePromotionFailure参数是否允许担保失败?- 否 → 直接执行Full GC
- 是 → 进入第三步
- 第三步检查: 老年代最大可用连续空间 > 历次晋升到老年代对象的平均大小?
- 是 → 冒险执行Minor GC(有风险)
- 否 → 执行Full GC
# JDK 6中的参数配置
-XX:+HandlePromotionFailure # 允许担保失败,进行风险评估
-XX:-HandlePromotionFailure # 不允许担保失败,直接Full GCJDK 6 Update 24及以后的策略:
-XX:HandlePromotionFailure参数被移除,采用更简化的检查规则:
- 检查条件: 老年代连续空间 > 新生代对象总大小 或 老年代连续空间 > 历次晋升平均大小
- 满足任一条件 → 执行Minor GC
- 都不满足 → 执行Full GC
这种简化策略更加智能,无需手动配置参数,JVM自动进行风险评估。
graph TB
subgraph JDK_6_Update_24之前
Start1[准备Minor GC] --> Check1_1{老年代空间 ><br/>新生代对象总大小?}
Check1_1 -->|是| Safe1[安全执行Minor GC]
Check1_1 -->|否| Check1_2{HandlePromotionFailure<br/>允许担保失败?}
Check1_2 -->|否| Full1[执行Full GC]
Check1_2 -->|是| Check1_3{老年代空间 ><br/>历次晋升平均大小?}
Check1_3 -->|是| Risk1[冒险执行Minor GC]
Check1_3 -->|否| Full1
end
subgraph JDK_6_Update_24之后
Start2[准备Minor GC] --> Check2{老年代空间 > 新生代总大小<br/>或<br/>老年代空间 > 历次晋升平均?}
Check2 -->|是| Minor2[执行Minor GC]
Check2 -->|否| Full2[执行Full GC]
end
style Safe1 fill:#51cf66,stroke:#2f9e44,stroke-width:2px,rx:15,ry:15
style Risk1 fill:#ffd43b,stroke:#f59f00,stroke-width:2px,rx:15,ry:15
style Full1 fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px,rx:15,ry:15
style Minor2 fill:#51cf66,stroke:#2f9e44,stroke-width:2px,rx:15,ry:15
style Full2 fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px,rx:15,ry:15担保流程详解(现代JVM)
graph TB
Start[准备Young GC] --> Check1{老年代最大连续空间<br/>> 新生代所有对象总大小?}
Check1 -->|是| Safe[担保成功<br/>安全执行Young GC]
Check1 -->|否| Check2{老年代最大连续空间<br/>> 历次晋升平均大小?}
Check2 -->|是| Risk[担保通过<br/>冒险执行Young GC]
Check2 -->|否| FullGC[执行Full GC<br/>清理老年代空间]
Risk --> Execute[执行Young GC]
Execute --> Result{实际晋升对象大小}
Result -->|≤ 老年代空间| Success[Young GC成功]
Result -->|> 老年代空间| PromotionFailed[担保失败<br/>Promotion Failed]
PromotionFailed --> FullGC2[执行Full GC]
Safe --> Success
FullGC --> Retry[重新尝试分配]
FullGC2 --> Retry
style Safe fill:#51cf66,stroke:#2f9e44,stroke-width:2px,rx:15,ry:15
style Risk fill:#ffd43b,stroke:#f59f00,stroke-width:2px,rx:15,ry:15
style Success fill:#51cf66,stroke:#2f9e44,stroke-width:2px,rx:15,ry:15
style FullGC fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px,rx:15,ry:15
style FullGC2 fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px,rx:15,ry:15
style PromotionFailed fill:#ff8787,stroke:#e03131,stroke-width:2px,rx:15,ry:15历次晋升平均大小的计算
统计维度: JVM在运行过程中,会记录每次Minor GC后晋升到老年代的对象总大小。
计算公式:
历次晋升平均大小 = Σ(每次Minor GC的晋升对象大小) / Minor GC次数示例数据:
| Minor GC次数 | 晋升对象大小 | 累计晋升大小 | 平均大小 |
|---|---|---|---|
| 1 | 5MB | 5MB | 5MB |
| 2 | 8MB | 13MB | 6.5MB |
| 3 | 6MB | 19MB | 6.3MB |
| 4 | 7MB | 26MB | 6.5MB |
| 5 | 9MB | 35MB | 7MB |
使用场景: 在第6次Minor GC前,JVM会用7MB作为历次晋升平均大小来判断老年代空间是否足够。
担保成功与失败的场景
场景1: 担保成功
public class GuaranteeSuccessDemo {
public static void main(String[] args) {
// JVM参数: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
// 堆20M,新生代10M,老年代10M
// 老年代当前可用空间: 10MB
// 新生代对象总大小: 约8MB
// 历次晋升平均: 2MB
// 检查: 10MB > 8MB (满足条件)
// 结果: 安全执行Minor GC
byte[] array1 = new byte[2 * 1024 * 1024]; // 2MB
byte[] array2 = new byte[2 * 1024 * 1024]; // 2MB
byte[] array3 = new byte[2 * 1024 * 1024]; // 2MB
// 触发Minor GC,担保成功
byte[] array4 = new byte[4 * 1024 * 1024]; // 4MB
}
}场景2: 担保失败,触发Full GC
public class GuaranteeFailureDemo {
private static List<byte[]> oldGenObjects = new ArrayList<>();
public static void main(String[] args) {
// JVM参数: -Xms20M -Xmx20M -Xmn8M -XX:+PrintGCDetails
// 堆20M,新生代8M,老年代12M
// 先填满老年代大部分空间
for (int i = 0; i < 5; i++) {
byte[] old = new byte[2 * 1024 * 1024]; // 2MB
oldGenObjects.add(old);
}
// 老年代已使用10MB,剩余2MB
// 新生代创建对象
byte[] young1 = new byte[2 * 1024 * 1024]; // 2MB
byte[] young2 = new byte[2 * 1024 * 1024]; // 2MB
byte[] young3 = new byte[2 * 1024 * 1024]; // 2MB
// 检查: 老年代剩余2MB < 新生代6MB
// 检查: 老年代剩余2MB < 历次晋升平均3MB
// 结果: 先执行Full GC,清理老年代空间
byte[] young4 = new byte[2 * 1024 * 1024]; // 2MB,触发GC
}
}担保失败的后果
Promotion Failed(晋升失败):
- Survivor区对象无法进入老年代
- 触发Full GC,整理老年代碎片
- 应用停顿时间延长
预防措施:
// ❌ 导致频繁晋升失败的代码
public class PromotionFailureDemo {
private static List<byte[]> cache = new ArrayList<>();
public static void main(String[] args) {
while (true) {
// 持续创建对象,导致老年代快速填满
for (int i = 0; i < 100; i++) {
byte[] array = new byte[1024 * 1024]; // 1MB
cache.add(array);
}
// 定期清理,但老年代已碎片化
if (cache.size() > 500) {
cache.subList(0, 250).clear();
}
}
}
}
// ✅ 改进: 控制对象生命周期
public class ImprovedDemo {
private static final int MAX_CACHE_SIZE = 100;
private static LinkedHashMap<String, byte[]> cache =
new LinkedHashMap<String, byte[]>(MAX_CACHE_SIZE, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_CACHE_SIZE;
}
};
public static void main(String[] args) {
// LRU缓存,自动淘汰老对象,减少老年代压力
for (int i = 0; i < 10000; i++) {
cache.put("key_" + i, new byte[1024 * 1024]);
}
}
}JDK版本差异
JDK 6:
需要显式开启担保:
-XX:+HandlePromotionFailureJDK 7及以后:
HandlePromotionFailure参数被移除,JVM自动进行智能担保检查,无需手动配置。
Survivor区的必要性
为什么需要两个Survivor区?
如果只有一个Survivor区,会导致严重的问题:
问题1: 内存碎片化
graph LR
subgraph 单Survivor问题
Eden1[Eden<br/>死亡对象+存活对象] --> GC1[GC后]
GC1 --> Survivor1[Survivor<br/>存活对象1 空闲 存活对象2 空闲]
end
subgraph 双Survivor优势
Eden2[Eden<br/>死亡对象+存活对象] --> GC2[GC后]
GC2 --> SurvivorTo[Survivor To<br/>存活对象连续排列]
end
style Survivor1 fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px,rx:15,ry:15
style SurvivorTo fill:#51cf66,stroke:#2f9e44,stroke-width:2px,rx:15,ry:15问题2: 无法实现复制算法
复制算法需要两块相同大小的内存区域:
- From区: 保存上次GC存活的对象
- To区: 接收本次GC存活的对象
双Survivor工作流程:
// 第一次Young GC
Eden(8MB) + Survivor0(1MB) -> Survivor1(1MB存活对象)
// Survivor0和Eden清空
// 第二次Young GC
Eden(8MB) + Survivor1(1MB) -> Survivor0(1MB存活对象)
// Survivor1和Eden清空
// 如此往复,始终保持一个Survivor为空Eden与Survivor的比例
默认比例: Eden:Survivor0:Survivor1 = 8:1:1
# 调整比例
-XX:SurvivorRatio=8 # Eden占新生代的8/(8+1+1) = 80%
# 示例: 新生代10MB
# Eden: 8MB
# Survivor0: 1MB
# Survivor1: 1MB为什么是8:1:1?
基于大量实际应用的统计数据:
- 约90%的对象在第一次GC时就死亡
- Eden占80%可容纳大部分新对象
- 10%的Survivor区足以容纳存活对象
graph TB
subgraph 对象存活率统计
Total[100个新创建对象] --> FirstGC[经历第1次GC]
FirstGC --> Dead1[约90个死亡]
FirstGC --> Alive1[约10个存活]
Alive1 --> SecondGC[经历第2次GC]
SecondGC --> Dead2[约8个死亡]
SecondGC --> Alive2[约2个存活]
end
style Total fill:#74c0fc,stroke:#1971c2,stroke-width:2px,rx:15,ry:15
style Dead1 fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px,rx:15,ry:15
style Dead2 fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px,rx:15,ry:15
style Alive1 fill:#51cf66,stroke:#2f9e44,stroke-width:2px,rx:15,ry:15
style Alive2 fill:#51cf66,stroke:#2f9e44,stroke-width:2px,rx:15,ry:15极端情况处理:
如果Survivor区无法容纳所有存活对象:
// 触发空间分配担保
// 存活对象直接进入老年代内存分配优化实践
1. 合理设置堆内存大小
# 初始堆大小和最大堆大小设置为相同值,避免动态扩容
-Xms4G -Xmx4G
# 新生代大小(建议占堆的1/3到1/2)
-Xmn2G
# 或使用比例设置
-XX:NewRatio=2 # 老年代:新生代 = 2:12. 调整对象晋升策略
# 降低晋升年龄,让长生命周期对象快速进入老年代
-XX:MaxTenuringThreshold=5
# 提高Survivor使用率阈值,减少提前晋升
-XX:TargetSurvivorRatio=703. 避免创建大对象
// ✅ 推荐: 分批加载数据
public List<User> loadUsers(int offset, int limit) {
return userRepository.findAll(offset, limit);
}
// 分页处理
for (int offset = 0; offset < totalCount; offset += 1000) {
List<User> batch = loadUsers(offset, 1000);
processBatch(batch);
}
// ❌ 不推荐: 一次性加载所有数据
List<User> allUsers = userRepository.findAll(); // 可能几十万条数据4. 监控GC日志
# 开启详细GC日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-Xloggc:/var/log/gc.log
# 分析工具推荐
# - GCEasy (在线分析)
# - GCViewer (本地工具)
# - JProfiler (商业工具)5. 使用合适的垃圾收集器
# 低延迟优先: G1或ZGC
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
# 高吞吐量优先: Parallel GC
-XX:+UseParallelGC -XX:GCTimeRatio=99
# 响应时间敏感: ZGC (JDK 11+)
-XX:+UseZGC -XX:ZCollectionInterval=5常见内存问题诊断
频繁Young GC
现象: Young GC频率过高(每秒多次)
原因:
- Eden区过小
- 对象创建速率过高
- 无用对象未及时释放
解决方案:
# 增大新生代
-Xmn4G
# 优化代码,减少对象创建
# 使用对象池、缓存复用频繁Full GC
现象: Full GC频繁(每小时多次)
原因:
- 老年代空间不足
- 大对象过多
- 内存泄漏
解决方案:
# 增大堆内存
-Xms8G -Xmx8G
# 调整新生代比例
-XX:NewRatio=1 # 老年代:新生代 = 1:1
# 使用内存分析工具定位泄漏
jmap -dump:live,format=b,file=heap.bin <pid>Promotion Failed
现象: 日志出现"promotion failed"
原因:
- 老年代碎片化
- 晋升对象过多
解决方案:
# 使用压缩式GC算法
-XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection
# 或切换到G1
-XX:+UseG1GCINFO
- 优先在Eden区分配: 利用新生代GC的高效性
- 控制大对象: 避免大对象直接进入老年代
- 合理设置晋升年龄: 平衡新生代和老年代的压力
- 监控空间分配担保: 预防Promotion Failed
- 定期分析GC日志: 及时发现和解决内存问题
更新: 2025-12-04 17:35:27
原文: https://www.yuque.com/u22210564/zoxfmt/doc-05-jvm-02-07