Skip to content

内存分配与回收原则

对象内存分配策略概述

JVM在为对象分配内存时,遵循一系列优化策略,这些策略旨在提高内存利用率、减少垃圾回收频率,并提升应用性能。

mermaid
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区分配内存。

基本分配流程

java
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分配成功
    }
}

运行结果分析:

plain
[GC (Allocation Failure) [PSYoungGen: 7127K->808K(9216K)] 7127K->6952K(19456K), 0.0034568 secs]

解读:

  • PSYoungGen: 7127K->808K(9216K): 新生代从7127K降到808K
  • 7127K->6952K(19456K): 整个堆从7127K变为6952K
  • 0.0034568 secs: GC耗时3.4毫秒
mermaid
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:15

Eden分配的优势

优点:

  • 新生代GC速度快(复制算法)
  • 大部分对象生命周期短,可快速回收
  • 减少老年代的垃圾积累

适用对象:

  • 临时对象(方法内局部变量)
  • 生命周期短的业务对象
  • 小对象(< PretenureSizeThreshold)

2. 大对象直接进入老年代

大对象定义: 需要大量连续内存空间的对象,如长字符串、大数组。

大对象处理机制

大对象直接进入老年代是JVM的动态优化策略,旨在避免:

  • Eden区频繁GC
  • 对象在Survivor区之间来回复制(复制成本高)
  • Survivor区空间不足
java
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收集器:

bash
# 显式设置大对象阈值为3MB
-XX:+UseParNewGC
-XX:PretenureSizeThreshold=3145728

Parallel Scavenge收集器:

  • 无固定阈值参数
  • 根据堆内存情况和历史数据动态决定
  • 更智能但不可精确控制

G1收集器:

bash
# G1根据Region大小决定
-XX:G1HeapRegionSize=4M  # 设置Region大小
# 对象大小超过Region的50%,进入Humongous区(特殊老年代区域)
mermaid
graph TB
    subgraph Serial_ParNew
        Check1{对象大小 &gt; 阈值?}
        Check1 -->|是| Old1[直接进老年代]
        Check1 -->|否| Eden1[进入Eden区]
    end
    
    subgraph Parallel_Scavenge
        Check2{动态阈值判断}
        Check2 -->|大对象| Old2[直接进老年代]
        Check2 -->|普通对象| Eden2[进入Eden区]
    end
    
    subgraph G1
        Check3{对象大小 &gt; 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

避免大对象的实践

java
// ❌ 不推荐: 创建大字符串
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
  • 年龄达到阈值时,晋升到老年代
java
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");
        }
    }
}

年龄阈值参数:

bash
# 设置最大晋升年龄(默认15)
-XX:MaxTenuringThreshold=15

# CMS收集器默认为6
-XX:+UseConcMarkSweepGC  # CMS默认MaxTenuringThreshold=6

# 打印对象年龄分布
-XX:+PrintTenuringDistribution

INFO

对象头的Mark Word中,GC年龄字段仅占4位,最大值为15(二进制1111)。

mermaid
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%),则将该年龄及以上的对象晋升到老年代。

java
// 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的对象晋升到老年代。

mermaid
graph TB
    Start[Survivor对象] --> Age1[年龄1: 2MB<br/>累计:2MB]
    Age1 --> Age2[年龄2: 2MB<br/>累计:4MB]
    Age2 --> Age3[年龄3: 2MB<br/>累计:6MB]
    
    Age3 --> Check{累计 &gt; 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

调整参数:

bash
# 设置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之前的策略:

  1. 第一步检查: 老年代最大可用连续空间 > 新生代所有对象总空间?
    • 是 → Minor GC安全,直接执行
    • 否 → 进入第二步
  2. 第二步检查: -XX:HandlePromotionFailure参数是否允许担保失败?
    • 否 → 直接执行Full GC
    • 是 → 进入第三步
  3. 第三步检查: 老年代最大可用连续空间 > 历次晋升到老年代对象的平均大小?
    • 是 → 冒险执行Minor GC(有风险)
    • 否 → 执行Full GC
bash
# JDK 6中的参数配置
-XX:+HandlePromotionFailure  # 允许担保失败,进行风险评估
-XX:-HandlePromotionFailure  # 不允许担保失败,直接Full GC

JDK 6 Update 24及以后的策略:

-XX:HandlePromotionFailure参数被移除,采用更简化的检查规则:

  • 检查条件: 老年代连续空间 > 新生代对象总大小 老年代连续空间 > 历次晋升平均大小
    • 满足任一条件 → 执行Minor GC
    • 都不满足 → 执行Full GC

这种简化策略更加智能,无需手动配置参数,JVM自动进行风险评估。

mermaid
graph TB
    subgraph JDK_6_Update_24之前
        Start1[准备Minor GC] --> Check1_1{老年代空间 &gt;<br/>新生代对象总大小?}
        Check1_1 -->|是| Safe1[安全执行Minor GC]
        Check1_1 -->|否| Check1_2{HandlePromotionFailure<br/>允许担保失败?}
        Check1_2 -->|否| Full1[执行Full GC]
        Check1_2 -->|是| Check1_3{老年代空间 &gt;<br/>历次晋升平均大小?}
        Check1_3 -->|是| Risk1[冒险执行Minor GC]
        Check1_3 -->|否| Full1
    end
    
    subgraph JDK_6_Update_24之后
        Start2[准备Minor GC] --> Check2{老年代空间 &gt; 新生代总大小<br/>或<br/>老年代空间 &gt; 历次晋升平均?}
        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)

mermaid
graph TB
    Start[准备Young GC] --> Check1{老年代最大连续空间<br/>&gt; 新生代所有对象总大小?}
    
    Check1 -->|是| Safe[担保成功<br/>安全执行Young GC]
    Check1 -->|否| Check2{老年代最大连续空间<br/>&gt; 历次晋升平均大小?}
    
    Check2 -->|是| Risk[担保通过<br/>冒险执行Young GC]
    Check2 -->|否| FullGC[执行Full GC<br/>清理老年代空间]
    
    Risk --> Execute[执行Young GC]
    Execute --> Result{实际晋升对象大小}
    
    Result -->|≤ 老年代空间| Success[Young GC成功]
    Result -->|&gt; 老年代空间| 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后晋升到老年代的对象总大小。

计算公式:

plain
历次晋升平均大小 = Σ(每次Minor GC的晋升对象大小) / Minor GC次数

示例数据:

Minor GC次数晋升对象大小累计晋升大小平均大小
15MB5MB5MB
28MB13MB6.5MB
36MB19MB6.3MB
47MB26MB6.5MB
59MB35MB7MB

使用场景: 在第6次Minor GC前,JVM会用7MB作为历次晋升平均大小来判断老年代空间是否足够。

担保成功与失败的场景

场景1: 担保成功

java
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

java
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,整理老年代碎片
  • 应用停顿时间延长

预防措施:

java
// ❌ 导致频繁晋升失败的代码
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:

需要显式开启担保:

bash
-XX:+HandlePromotionFailure

JDK 7及以后:

HandlePromotionFailure参数被移除,JVM自动进行智能担保检查,无需手动配置。

Survivor区的必要性

为什么需要两个Survivor区?

如果只有一个Survivor区,会导致严重的问题:

问题1: 内存碎片化

mermaid
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工作流程:

java
// 第一次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

bash
# 调整比例
-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区足以容纳存活对象
mermaid
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区无法容纳所有存活对象:

java
// 触发空间分配担保
// 存活对象直接进入老年代

内存分配优化实践

1. 合理设置堆内存大小

bash
# 初始堆大小和最大堆大小设置为相同值,避免动态扩容
-Xms4G -Xmx4G

# 新生代大小(建议占堆的1/3到1/2)
-Xmn2G

# 或使用比例设置
-XX:NewRatio=2  # 老年代:新生代 = 2:1

2. 调整对象晋升策略

bash
# 降低晋升年龄,让长生命周期对象快速进入老年代
-XX:MaxTenuringThreshold=5

# 提高Survivor使用率阈值,减少提前晋升
-XX:TargetSurvivorRatio=70

3. 避免创建大对象

java
// ✅ 推荐: 分批加载数据
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日志

bash
# 开启详细GC日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-Xloggc:/var/log/gc.log

# 分析工具推荐
# - GCEasy (在线分析)
# - GCViewer (本地工具)
# - JProfiler (商业工具)

5. 使用合适的垃圾收集器

bash
# 低延迟优先: 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区过小
  • 对象创建速率过高
  • 无用对象未及时释放

解决方案:

bash
# 增大新生代
-Xmn4G

# 优化代码,减少对象创建
# 使用对象池、缓存复用

频繁Full GC

现象: Full GC频繁(每小时多次)

原因:

  • 老年代空间不足
  • 大对象过多
  • 内存泄漏

解决方案:

bash
# 增大堆内存
-Xms8G -Xmx8G

# 调整新生代比例
-XX:NewRatio=1  # 老年代:新生代 = 1:1

# 使用内存分析工具定位泄漏
jmap -dump:live,format=b,file=heap.bin <pid>

Promotion Failed

现象: 日志出现"promotion failed"

原因:

  • 老年代碎片化
  • 晋升对象过多

解决方案:

bash
# 使用压缩式GC算法
-XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection

# 或切换到G1
-XX:+UseG1GC

INFO

  1. 优先在Eden区分配: 利用新生代GC的高效性
  2. 控制大对象: 避免大对象直接进入老年代
  3. 合理设置晋升年龄: 平衡新生代和老年代的压力
  4. 监控空间分配担保: 预防Promotion Failed
  5. 定期分析GC日志: 及时发现和解决内存问题

更新: 2025-12-04 17:35:27
原文: https://www.yuque.com/u22210564/zoxfmt/doc-05-jvm-02-07

Java 后端面试知识库