Skip to content

堆细分结构/大小设置/OOM案例

堆的完整结构

JVM 的堆在逻辑上被分为三个区域:年轻代(Young Generation)老年代(Old/Tenured Generation),以及 JDK 7 之前存在的永久代(Permanent Generation)

JDK 8 之后,永久代被移除,用元空间(Metaspace)替代——但元空间不在堆中,而是使用本地内存。

JDK 7 及之前:
┌──────────────────────────────────────────────────┐
│                      堆(Heap)                    │
│                                                    │
│  ┌────────────────┐     ┌──────────────────┐  │
│  │     年轻代      │     │   老年代           │  │
│  │  Eden │ S0 │ S1 │     │                   │  │
│  └────────────────┘     └──────────────────┘  │
│                                                    │
│  ┌────────────────────────────────────────────┐  │
│  │              永久代(PermGen)              │  │
│  │   类信息、常量、字符串常量池(JDK 7 及之前) │  │
│  └────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────┘

JDK 8+:
┌──────────────────────────────────────────────────┐
│                      堆(Heap)                    │
│                                                    │
│  ┌────────────────┐     ┌──────────────────┐  │
│  │     年轻代      │     │   老年代           │  │
│  │  Eden │ S0 │ S1 │     │                   │  │
│  └────────────────┘     └──────────────────┘  │
│                                                    │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│                   本地内存(Native Memory)          │
│                                                    │
│  ┌────────────────────────────────────────────┐  │
│  │              元空间(Metaspace)               │  │
│  │   类信息、运行时常量池、JIT 编译代码缓存        │  │
│  └────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────┘

各区域的详细说明

Eden 区:新对象的出生地

大多数新对象在 Eden 区分配。当 Eden 区满时,触发 Minor GC

java
public class EdenAllocation {
    public static void main(String[] args) {
        // 这些对象都在 Eden 区分配
        for (int i = 0; i < 100; i++) {
            String s = "item" + i;  // 字符串是对象
            byte[] data = new byte[1024];  // 小数组
        }
        // Eden 区满时,Minor GC 把存活对象移到 Survivor 区
    }
}

Survivor 区:对象的「中转站」

Survivor 区分为 S0S1 两块,大小相等。Minor GC 后,存活对象在两块 Survivor 区之间来回移动。

Minor GC 前:Eden 区满,S0 有一些对象
┌─────────────────────────────────────────────┐
│ Eden │        S0 (有对象)        │ S1 (空) │
└─────────────────────────────────────────────┘

Minor GC 后:Eden 和 S0 中的存活对象复制到 S1
┌─────────────────────────────────────────────┐
│  Eden (已清空) │ S0 (已清空) │    S1 (存活对象)  │
└─────────────────────────────────────────────┘

下次 Minor GC:角色互换
┌─────────────────────────────────────────────┐
│  Eden (已清空) │ S1 (存活对象)  │    S0 (已清空)  │
└─────────────────────────────────────────────┘

Survivor 区中,对象每经历一次 Minor GC,年龄计数器就加 1。当年龄超过阈值(默认 15),对象晋升到老年代。

老年代:长期存活的对象

老年代存储生命周期较长的对象。当老年代满时,触发 Major GCFull GC

进入老年代的条件:

  1. 年龄达标:对象经历多次 Minor GC 后仍然存活(年龄超过 -XX:MaxTenuringThreshold,默认 15)
  2. 大对象:超过 -XX:PretenureSizeThreshold 阈值的对象直接在老年代分配
  3. Survivor 区空间不足:如果 Survivor 区中相同年龄的所有对象之和超过 Survivor 区的一半,年龄 >= 该年龄的对象直接进入老年代

堆大小设置

基本参数

bash
# 初始堆大小
java -Xms256m MyApp

# 最大堆大小
java -Xmx4g MyApp

# 设置初始和最大堆为相同值(避免运行时调整)
java -Xms4g -Xmx4g MyApp

年轻代设置

bash
# 设置年轻代大小(JDK 8 及之前)
java -Xmn2g MyApp

# 设置 Eden 和 Survivor 的比例(JDK 8 及之前)
# 默认比例是 8:1:1(Eden : S0 : S1)
# -XX:SurvivorRatio=8 表示 Eden = Survivor * 8
java -XX:SurvivorRatio=4 MyApp  # Eden 是 Survivor 的 4 倍

# JDK 17+,可以用比例设置年轻代
java -XX:NewRatio=2 MyApp  # 年轻代 : 老年代 = 1 : 2

内存设置原则

场景建议配置
开发/测试环境-Xms512m -Xmx512m(固定大小,便于观察)
高并发服务器-Xms4g -Xmx4g -Xmn2g(固定年轻代,减少 GC 频率)
大内存机器-Xms8g -Xmx8g -XX:+UseG1GC -XX:MaxGCPauseMillis=200
容器环境-XX:+UseContainerSupport -XX:MaxRAMPercentage=75

OOM 实战案例

案例一:堆内存耗尽

java
public class HeapOOMCase {
    // 模拟不断增长的数据缓存
    private static final List&lt;byte[]&gt; CACHE = new ArrayList&lt;&gt;();

    public static void main(String[] args) {
        int size = 0;
        try {
            while (true) {
                byte[] buffer = new byte[1024 * 1024];  // 每次分配 1MB
                CACHE.add(buffer);
                size++;
                System.out.println("Allocated " + size + " MB");
            }
        } catch (OutOfMemoryError e) {
            System.out.println("OOM after " + size + " MB");
        }
    }
}
java -Xmx256m -XX:+PrintGCDetails HeapOOMCase
Allocated 1 MB
Allocated 2 MB
...
Allocated 248 MB
Allocated 249 MB
OOM after 249 MB

案例二:频繁 Full GC 但对象无法回收

java
public class MemoryLeak {
    // 静态集合持有对象引用,导致无法回收
    private static final Map&lt;String, byte[]&gt; LEAK_MAP = new HashMap&lt;&gt;();

    public static void main(String[] args) {
        int count = 0;
        try {
            while (true) {
                // key 不重复,所以每次都插入新条目
                LEAK_MAP.put(UUID.randomUUID().toString(), new byte[1024 * 1024]);
                count++;
                // 故意不清理,模拟内存泄漏
            }
        } catch (OutOfMemoryError e) {
            System.out.println("OOM after " + count + " inserts");
        }
    }
}

GC 日志会显示大量 Full GC,但每次 Full GC 后堆内存仍然接近最大值——这是典型的内存泄漏特征。

案例三:大对象直接进入老年代

java
public class LargeObjectCase {
    public static void main(String[] args) {
        // 超过 PretenureSizeThreshold 的大对象直接进入老年代
        // 默认阈值是 0,表示不启用
        // 设置阈值为 100KB
        // java -XX:PretenureSizeThreshold=100000 -XX:+PrintGCDetails ...

        // 这个 10MB 的数组会直接在老年代分配
        byte[] hugeArray = new byte[10 * 1024 * 1024];
        System.out.println("Large object allocated in old gen");
    }
}

排查工具

bash
# 1. 生成堆转储(OOM 时自动)
java -Xmx256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/ MyApp

# 2. 使用 jmap 查看堆概况
jmap -heap <pid>

# 3. 使用 jstat 监控 GC
jstat -gcutil <pid> 1000

本节小结

堆结构与配置的核心要点:

区域作用关键参数
Eden新对象分配-XX:SurvivorRatio
SurvivorMinorGC 中转-XX:MaxTenuringThreshold
老年代长期存活对象-XX:NewRatio / -Xmn
Metaspace类元信息-XX:MaxMetaspaceSize

OOM 的排查思路:转储 → 分析 → 定位泄漏点 → 修复引用链路

下一节,我们来看 新生代/老年代参数设置

基于 VitePress 构建