前言
堆是JVM中最重要的一块内存区域,也是垃圾回收的主战场。几乎所有Java对象实例、数组全部分配在堆上。面试经常会问:对象如何分配?什么时候晋升到老年代?Minor GC和Full GC区别?OOM有哪些类型?
本篇从基础概念、JVM启动参数、分代模型、对象流转、各类GC、OOM实战演示完整梳理。
回顾上一篇运行时数据区知识点:堆属于线程共享区域,JVM启动时创建。
一、堆内存基础概念
- 一个Java进程对应一个JVM实例,一个JVM只有一块堆内存,所有线程共享堆。
- 几乎所有对象、数组都在堆分配。
- 堆的大小可以通过JVM启动参数进行调整。
- 堆会发生GC,也是OOM内存溢出最常发生的区域。
注意:方法区(永久代/元空间)HotSpot实现称之为Non‑Heap非堆。逻辑上属于堆概念,物理内存上和Java堆相互独立。
1.1 堆核心JVM参数
| 参数 | 说明 | 默认值 |
|---|---|---|
-Xms | 堆初始内存,等价‑XX:InitialHeapSize | 物理内存的1/64 |
-Xmx | 堆最大内存,等价‑XX:MaxHeapSize | 物理内存的1/4 |
-Xmn | 新生代总大小(Eden+两个Survivor总和) | 新生代占整个堆1/3,老年代2/3 |
-XX:MaxTenuringThreshold | 对象晋升老年代年龄阈值,复制GC的计数,默认15 | 15 |
生产环境最佳实践:‑Xms和-Xmx设置为相同的值
如果Xms小于Xmx,GC完成后JVM会动态扩容/缩容堆,这个过程会带来性能损耗。生产环境固定大小,避免运行时调整堆容量。
1.2 Java代码查看当前堆内存大小
Runtime类提供API,可以查看程序运行时堆的实际情况。
public class HeapSpaceDemo { public static void main(String[] args) { // totalMemory:当前已经分配给JVM堆的总内存 long totalMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; // maxMemory:JVM堆能申请到的最大内存,对应-Xmx long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; // freeMemory:堆中空闲可用内存 long freeMemory = Runtime.getRuntime().freeMemory() /1024 /1024; System.out.println("totalMemory(当前堆): " + totalMemory + " MB"); System.out.println("maxMemory(-Xmx): " + maxMemory + " MB"); System.out.println("freeMemory(空闲): " + freeMemory + " MB"); } }启动参数示例:‑Xms600m ‑Xmx600m ‑Xmn200m
- 堆总大小固定600M
- 新生代总共200M,老年代400M
二、堆逻辑分代模型
分代是逻辑划分,物理内存不一定连续。 HotSpot传统分代堆分为两大块:
- Young 新生代:存放生命周期短、朝生夕死的对象;GC频繁。
- Eden(伊甸园区)
- Survivor 0(From)
- Survivor 1(To)
默认比例 Eden:Survivor0:Survivor1 =
8:1:1
- Old 老年代:存放存活时间长、多次GC仍然存活的对象;GC频率低。
Survivor永远有一块是空的,叫做To区;另一块叫From区。谁空谁就是To。
2.1 对象分配与完整流转流程
- 新创建对象优先分配到Eden区。
- Eden空间填满,触发Minor GC(Young GC),发生STW,暂停业务线程。
- 将Eden中存活对象复制到To Survivor区,对象年龄 + 1。
- Eden区全部清空;From区存活对象同样复制到To区,年龄+1。
- 交换From与To身份,原来To变成新的From,原来From清空变为To。
- 继续在Eden分配新对象;反复Minor GC,对象在两个Survivor来回拷贝,年龄不断增长。
- 对象年龄达到
‑XX:MaxTenuringThreshold=15,晋升到老年代。
ASCII示意图模拟一次Minor GC
# GC前 ┌──────────────┬─────────┬─────────┐ │ Eden │ From(S0)│ To(S1) │ │ 大量新对象 │存活对象 │ 空 │ └──────────────┴─────────┴─────────┘ # Minor GC执行,存活对象复制到To ┌──────────────┬─────────┬─────────┐ │ Eden │ From(S0)│ To(S1) │ │ 清空 │ 清空 │存活对象 │ └──────────────┴─────────┴─────────┘ # 交换身份:S1变成From,S0变成To(空) ┌──────────────┬─────────┬─────────┐ │ Eden │ From(S1)│ To(S0) │ │ 分配新对象 │存活对象 │ 空 │ └──────────────┴─────────┴─────────┘2.2 对象直接进入老年代两种场景
面试高频考点:不是只有年龄到15才会进老年代
大对象直接分配到老年代对象太大,Eden放不下,Survivor也装不下,直接分配到老年代。
什么是大对象:很长的数组、大字节数组。大对象尽量避免,会提前耗尽老年代,频繁Full GC。
- Survivor空间不足,分配担保机制Minor GC之后存活对象总大小 > To Survivor容量,无法放入Survivor,对象直接晋升到老年代。
总结: 情况1:经历15次Minor GC仍然存活,年龄阈值触发晋升; 情况2:大对象 / Survivor放不下,直接晋升老年代。
2.3 老年代GC流程
老年代对象生命周期长,不会频繁GC。 当老年代内存不足,触发Major GC,Major GC通常会连带触发Full GC。
- Full GC会回收整个堆(新生代+老年代)+方法区元空间。
- Full GC STW停顿时间很长,业务系统应当尽量减少Full GC发生。
- 如果Full GC完成之后,内存仍然不够存放对象 →抛出OOM
Java heap space。
三、GC类型详解
3.1 Minor GC(Young GC)
- 触发条件:Eden区内存耗尽
- 只回收新生代(Eden + Survivor)
- 执行频繁,STW时间短,速度快。
- 会发生STW,暂停全部业务用户线程。
3.2 Major GC(Old GC)
- 只回收老年代;HotSpot中Major GC一般伴随Full GC一起发生。
- 速度比Minor GC慢10倍以上,STW停顿时间更长。
3.3 Mixed GC(G1收集器特有)
传统Serial/Parallel/CMS没有Mixed GC,G1专属。 收集全部新生代,同时回收部分老年代Region,目标控制停顿时间。
3.4 Full GC 整堆收集
Full GC会收集:新生代、老年代、元空间(方法区)。STW时间很长,线上尽量避免。
Full GC常见触发场景(面试必背)
- 老年代空间不足,对象晋升失败;
- 元空间Metaspace内存耗尽;
- 手动调用
System.gc(),只是建议JVM执行GC,不保证立刻Full GC; - CMS并发模式失败;
- G1分配失败。
注意:
System.gc()不要在业务代码随便调用,会引发不必要Full GC,严重影响性能。
四、OOM OutOfMemoryError内存溢出
重要理解:OOM不等于堆溢出,OOM是一类错误总称,不同原因会报不同提示。
java.lang.OutOfMemoryError,内存溢出:JVM无法再分配足够内存给新对象。
4.1 OOM不同错误类型
| 错误提示 | 发生位置 | 产生原因 |
|---|---|---|
Java heap space | Java堆 | Full GC之后,堆内存仍然放不下新对象;内存泄漏 / 创建超大对象 |
PermGen space | 永久代(JDK7‑) | 加载类过多,动态生成大量class;Tomcat部署大量war包 |
Metaspace | 元空间(JDK8+) | 动态代理、CGLIB生成大量类,元空间耗尽 |
Direct Memory | 直接内存堆外 | NIO ByteBuffer分配堆外内存,未释放 |
4.2 堆OOM代码演示
JVM启动参数设置:‑Xms30m ‑Xmx30m
堆最大只能30M,不断往集合添加字节数组,耗尽堆内存。
import java.util.ArrayList; public class OOMHeapDemo { public static void main(String[] args) throws InterruptedException { ArrayList<byte[]> list = new ArrayList<>(); while(true){ //打印堆状态 long max = Runtime.getRuntime().maxMemory() /1024 /1024; long total = Runtime.getRuntime().totalMemory() /1024 /1024; long free = Runtime.getRuntime().freeMemory() /1024 /1024; System.out.printf("Xmx=%dM total=%dM free=%dM%n",max,total,free); //每次添加1M字节数组 list.add(new byte[1024 * 1024]); Thread.sleep(100); } } }运行一段时间抛出异常:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space4.3 OOM自动生成堆快照 dump文件
线上发生OOM,程序直接崩溃,来不及现场调试。可以配置JVM参数,OOM发生自动导出堆快照.hprof文件,事后分析内存快照。
# OOM的时候自动dump堆快照 ‑XX:+HeapDumpOnOutOfMemoryError # 指定hprof输出路径,生产写服务器磁盘路径 ‑XX:HeapDumpPath=/data/jvm/dump.hprof注意:dump文件大小几乎等于堆最大内存,磁盘需要预留足够存储空间。
4.4 hprof快照分析工具
- IDEA内置hprof分析工具,直接打开
.hprof文件; - Eclipse MAT(Memory Analyzer Tool),专业分析内存泄漏;
- JDK自带VisualVM;
- YourKit(商业Profiler工具)。
分析重点:
- 哪些对象占用内存最大;
- 对象引用链,定位是谁持有对象没有释放;
- 判断是瞬时大对象,还是长期内存泄漏。
五、面试高频题汇总
- 新生代Eden、Survivor默认比例,Survivor from/to交换机制?
- 对象什么时候晋升到老年代?两种情况。
- Minor GC、Major GC、Full GC分别区别,触发条件?
- 为什么生产环境
-Xms和-Xmx设置为相同数值? - OOM有哪几种报错,分别对应什么原因?
- 什么是分配担保?
- 如何配置JVM参数,OOM自动生成堆转储dump文件?
- System.gc()作用,能不能保证立刻Full GC?
小贴士
堆分代是理解GC收集器的基础,下一篇我们学习GC理论算法、四种引用、CMS/G1/ZGC垃圾收集器。