堆概述(唯一性/对象创建/GC)
堆:Java 对象的「主战场」
堆是 JVM 运行时数据区中最大的一块内存区域,也是 GC 的主战场。几乎所有通过 new 创建的对象和数组都分配在这里。
理解堆的结构和行为,是掌握 Java 内存管理和性能调优的基础。
堆的三个核心特点
1. 线程共享
堆是被所有线程共享的区域。这意味着堆中分配的对象可以被所有线程访问——这也意味着并发安全问题必须考虑。
java
public class HeapSharing {
// 这个对象在堆中,被所有线程共享
private static final List<String> sharedList = new ArrayList<>();
public static void main(String[] args) {
// 100 个线程同时修改 sharedList
for (int i = 0; i < 100; i++) {
new Thread(() -> sharedList.add("item")).start();
}
// 需要考虑并发安全:ArrayList 不是线程安全的
}
}2. GC 管理
堆中的对象生命周期由 GC 管理。GC 会自动识别不再使用的对象并回收其内存,这就是 Java 区别于 C++ 的「自动内存管理」。
java
public class GCManaged {
public static void main(String[] args) {
createObjects();
// createObjects() 中创建的对象不再被引用
// GC 会自动回收它们占用的堆内存
}
static void createObjects() {
for (int i = 0; i < 1000; i++) {
Object obj = new Object(); // 分配在堆上
// 方法结束后,局部变量 obj 失效
// 但 obj 指向的 Object 实例是否被回收,取决于是否还有其他引用
}
}
}3. 物理上不连续
堆是逻辑上连续、物理上可以是不连续的内存空间。现代操作系统的虚拟内存机制使这一点成为可能——JVM 从 OS 申请一段虚拟地址空间,映射到物理内存,可以跨多个物理页。
堆是唯一的
每个 JVM 进程只有一个堆。这个堆被所有线程共享。
┌─────────────────────────────────────────┐
│ JVM 进程 │
│ │
│ ┌─────────────────────────────────┐ │
│ │ 唯一的堆 │ │
│ │ ┌───────────┬─────────────┐ │ │
│ │ │ 年轻代 │ 老年代 │ │ │
│ │ │ Eden S0 S1│ │ │ │
│ │ └───────────┴─────────────┘ │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────┘堆与栈的对比
这是面试中最经典的问题:
| 维度 | 堆(Heap) | 虚拟机栈(Stack) |
|---|---|---|
| 存储内容 | 对象实例、数组 | 方法调用、局部变量 |
| 线程关系 | 线程共享 | 线程私有 |
| 大小 | -Xms/-Xmx 设置,通常较大 | -Xss 设置,通常较小(1MB) |
| 异常 | OutOfMemoryError(堆溢出) | StackOverflowError/OOM(栈溢出/线程数过多) |
| GC | 需要垃圾回收器管理 | 无需 GC,线程结束即回收 |
| 分配方式 | 从堆上分配(大多数情况下很快) | 栈上分配(比堆更快) |
| 灵活性 | 固定或动态大小 | 固定大小 |
对象创建的完整过程
当你写 new Object() 时,JVM 内部经历了什么?
new Object()
│
▼
1. 检查常量池:Object 类是否已加载?
│
│ 否 → 类加载(Loading → Linking → Initialization)
│
▼
2. 分配内存:在堆上为 Object 实例分配空间
│
│ 分配方式:指针碰撞 / 空闲列表
│ 并发处理:CAS / TLAB
│
▼
3. 零值初始化:instance 实例字段设为默认值
│
▼
4. 设置对象头:Mark Word + 类型指针
│
▼
5. 执行构造器:<init> 方法
│
▼
6. 返回对象引用关于对象创建的详细字节码过程,我们会在后续的 字节码视角:对象创建过程 中深入讲解。
堆的分代结构
现代 JVM 的堆通常分为「年轻代」和「老年代」两个区域:
┌──────────────────────────────────────────────────────┐
│ 堆(Heap) │
│ │
│ ┌────────────────────┐ ┌─────────────────────┐ │
│ │ 老年代(Old Generation) │ │
│ │ 长期存活的对象 / 大对象 │ │
│ │ 容量:约堆的 2/3 │ │
│ └────────────────────┘ └─────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 年轻代(Young Generation) │ │
│ │ │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Survivor S1 │ Survivor S0 │ │ │
│ │ │ (空白) │ (使用中) │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Eden 区 │ │ │
│ │ │ 新对象在这里分配 │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘分代设计基于一个重要的经验观察:大多数对象的生命周期很短。通过把新对象放在年轻代并频繁回收,GC 可以只扫描少量数据,大幅提升效率。
关于分代的详细结构和对象流转过程,我们会在后续章节深入讲解。
堆溢出(OutOfMemoryError: heap space)
当堆无法为新对象分配空间时,会抛出 OutOfMemoryError: Java heap space。
java
public class HeapOOM {
public static void main(String[] args) {
// 不断创建对象,直到堆耗尽
List<Object> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 每次分配 1MB
}
}
}Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at HeapOOM.main(HeapOOM.java:8)排查方法:
- 添加
-XX:+HeapDumpOnOutOfMemoryError,生成堆转储文件 - 用 MAT/JProfiler 分析 dump,找出占用最大的对象
- 检查是否有内存泄漏(大对象无法回收)
本节小结
堆是 JVM 中最重要的内存区域:
- 线程共享:所有线程共用一个堆
- GC 管理:自动回收不再使用的对象
- 分代设计:年轻代(Eden + Survivor)+ 老年代
- 核心异常:
OutOfMemoryError: Java heap space
理解堆,是理解 Java 内存管理的第一步。接下来,我们来看 堆细分结构/大小设置/OOM案例。
