Java堆
Java 堆是 JVM 中最重要的内存区域之一。绝大多数对象实例和数组都在堆上分配,垃圾回收也主要围绕堆展开。
# 1. 堆的作用
堆用于存放对象实例。
User user = new User();
int[] array = new int[1024];
user 这个引用变量可能在栈帧的局部变量表中,而真正的 User 对象在堆中。
# 2. 堆是线程共享的
堆被所有线程共享,因此多个线程可能访问同一个对象。
static User sharedUser = new User();
这也是并发安全问题产生的重要原因之一:对象在堆上共享,多个线程可能同时读写它的字段。
# 3. 堆的分代
传统分代模型中,堆可以分为:
- 新生代。
- 老年代。
新生代又通常包括:
- Eden 区。
- Survivor From。
- Survivor To。
大多数对象朝生夕死,优先在 Eden 分配。经历多次 Minor GC 仍然存活的对象可能晋升到老年代。
# 4. G1 的堆布局
G1 不再把堆物理划分为连续的新生代和老年代,而是划分为多个 Region。
每个 Region 可以扮演不同角色:
- Eden Region。
- Survivor Region。
- Old Region。
- Humongous Region。
G1 仍然保留分代思想,但堆布局更加灵活。
# 5. 堆参数
常见堆参数:
-Xms512m
-Xmx512m
含义:
-Xms:初始堆大小。-Xmx:最大堆大小。
生产环境中经常把二者设置为相同值,减少运行时堆扩缩容带来的抖动。
# 6. Java heap space
堆空间不足时可能出现:
java.lang.OutOfMemoryError: Java heap space
常见原因:
- 一次性加载过多数据。
- 集合无限增长。
- 缓存没有淘汰。
- 内存泄漏。
- 堆配置过小。
- 对象创建速度超过 GC 回收能力。
# 7. 阶段小结
Java 堆是对象存活和垃圾回收的主战场。理解堆,要同时理解对象分配、分代思想、GC 行为和内存泄漏。线上大部分 JVM 内存问题,第一现场通常就是堆。
# 8. 堆内对象生命周期图
对象创建
│
▼
优先进入 Eden / TLAB
│
├─ Young GC 后死亡
│ └─ 直接回收
│
└─ Young GC 后存活
│
▼
进入 Survivor,年龄 +1
│
├─ 后续 Young GC 死亡
│ └─ 回收
│
└─ 多次存活或 Survivor 放不下
▼
晋升老年代
│
├─ Old GC / Mixed GC 后死亡
└─ 长期存活或泄漏
这个流程解释了很多问题:新生代回收频繁通常和对象分配速度有关,老年代持续增长通常和对象存活时间、缓存、泄漏或晋升过快有关。
# 9. 堆容量和业务容量的关系
堆大小不是越大越好。
| 堆配置 | 好处 | 风险 |
|---|---|---|
| 较小堆 | 内存占用低,GC 单次处理数据少 | GC 频繁,容易 OOM |
| 较大堆 | 能容纳更多对象,GC 频率可能降低 | 单次回收成本高,容器内存压力大 |
Xms=Xmx | 避免运行时扩缩容 | 启动即占用较多内存 |
容量评估时要看:
- 峰值 QPS。
- 单请求对象分配量。
- 缓存大小。
- 会话数量。
- 批处理并发。
- GC 后稳定水位。
# 10. 堆内存泄漏常见形态
# 10.1 静态集合泄漏
private static final List<Object> DATA = new ArrayList<>();
如果不断 add 且不清理,集合会一直从 GC Roots 可达。
# 10.2 缓存无上限
缓存如果没有容量和过期策略,本质上就是一个慢性泄漏。
推荐使用 Caffeine 这类成熟缓存库,明确:
- 最大容量。
- 过期时间。
- 淘汰策略。
- 命中率监控。
# 10.3 队列堆积
生产速度大于消费速度时,队列对象持续堆积。
常见于:
- MQ 消费慢。
- 异步任务线程池太小。
- 下游接口慢。
- 无界队列。
# 11. Tips 快问快答
Q:堆越大,性能越好吗?
A:不一定。大堆可以减少部分 GC 频率,但可能增加单次回收成本,也会挤压堆外和容器内存。
Q:Full GC 后堆占用降不下来说明什么?
A:说明大量对象仍然可达,可能是正常长生命周期对象,也可能是内存泄漏。
Q:新对象一定先进 Eden 吗?
A:大多数小对象优先进入 Eden 或 TLAB。大对象、特殊分配路径、逃逸分析优化可能改变实际行为。
Q:堆 OOM 第一反应是加内存吗?
A:不是。先看是否泄漏、是否一次性加载过多数据、是否缓存无界,再决定是否扩容。