JVM堆内存

Java虚拟机(JVM)的堆内存(Heap)是Java程序运行时数据区中最大的一块,被所有线程共享。堆内存主要用于存放对象实例和数组,是垃圾收集器(GC)管理的主要区域。

堆内存特点

  • 由JVM启动时创建

  • 大小可以固定也可以动态调整

  • 物理上可以不连续但逻辑上要连续

  • 线程共享,需要考虑线程安全问题

  • 自动内存管理(GC)


堆内存结构

1
2
3
4
5
6
Young Generation (新生代)
├── Eden Space (伊甸园区)
├── Survivor Space 0 (幸存者0区)
└── Survivor Space 1 (幸存者1区)
Old Generation (老年代)
(可选) Permanent Generation / Metaspace (永久代/元空间)

新生代(Young Generation)

  • 存放新创建的对象

  • 分为Eden区和两个Survivor区(S0, S1)

  • 大多数对象在这里被创建和销毁

  • 使用复制算法进行垃圾回收(Minor GC)

老年代(Old Generation)

  • 存放长期存活的对象

  • 从新生代晋升过来的对象

  • 使用标记-清除或标记-整理算法进行垃圾回收(Major GC/Full GC)

永久代/元空间(PermGen/Metaspace)

  • Java 8之前是永久代(PermGen)

  • Java 8及之后是元空间(Metaspace)

  • 存储类元数据、方法区信息等

  • 不在堆内存中,而是使用本地内存‘


堆内存相关JVM参数

参数
说明
-Xms
初始堆大小
-Xmx
最大堆大小
-Xmn
新生代大小
-XX:NewRatio
老年代与新生代的比例
-XX:SurvivorRatio
年代与新生代的比例
-XX:MaxTenuringThreshold
象晋升老年代的年龄阈值
-XX:+PrintGCDetails
打印GC详细信息
-XX:+HeapDumpOnOutOfMemoryError
OOM时生成堆转储文件

堆内存代码示例

查看堆内存信息

可以通过以下代码查看设备的最大堆内存、初始堆内存、空闲堆内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HeapMemoryInfo {
public static void main(String[] args) {
// 返回JVM试图使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory();
// 返回JVM初始堆内存量
long totalMemory = Runtime.getRuntime().totalMemory();
// 返回JVM空闲堆内存量
long freeMemory = Runtime.getRuntime().freeMemory();

System.out.println("最大堆内存: " + maxMemory / 1024 / 1024 + "MB");
System.out.println("初始堆内存: " + totalMemory / 1024 / 1024 + "MB");
System.out.println("空闲堆内存: " + freeMemory / 1024 / 1024 + "MB");
}
}

运行结果(每个人的结果都可能不一样):

1
2
3
最大堆内存: 8152MB
初始堆内存: 512MB
空闲堆内存: 507MB

堆内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
static class OOMObject {}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}

运行时可添加JVM参数:

1
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

对象分配与GC过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ObjectAllocation {
private static final int _1MB = 1024 * 1024;

public static void main(String[] args) {
testAllocation();
}

public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC
}
}

运行时可添加JVM参数:

1
-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8

大对象直接进入老年代

1
2
3
4
5
6
7
8
9
10
11
12
public class BigObjectAllocation {
private static final int _1MB = 1024 * 1024;

public static void main(String[] args) {
testPretenureSizeThreshold();
}

public static void testPretenureSizeThreshold() {
byte[] allocation;
allocation = new byte[8 * _1MB]; // 直接分配在老年代
}
}

运行时可添加JVM参数:

1
-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:PretenureSizeThreshold=3145728

堆内存优化建议

  1. 合理设置堆大小:-Xms和-Xmx设置为相同值避免堆伸缩带来的性能损耗

  2. 新生代大小:通常为整个堆的1/3到1/4

  3. Survivor区比例:-XX:SurvivorRatio=8表示Eden与一个Survivor区的比例为8:1

  4. 对象晋升阈值:-XX:MaxTenuringThreshold控制对象晋升老年代的年龄

  5. 避免大对象:大对象会直接进入老年代,增加Full GC频率

  6. 减少临时对象:减少短生命周期对象的创建

  7. 使用对象池:对于频繁创建销毁的对象,考虑使用对象池


堆内存监控工具

  1. jvisualvm:JDK自带的可视化监控工具

  2. jconsole:JDK自带的监控和管理控制台

  3. jmap:生成堆转储快照

  4. jstat:监控JVM统计信息

  5. MAT (Memory Analyzer Tool):分析堆转储文件

  6. VisualGC:可视化查看GC过程


堆内存常见的面试QA

Q:什么是Minor GC和Full GC?

A:

  • Minor GC:只清理新生代的垃圾回收,频率较高,速度较快

  • Full GC:清理整个堆内存(包括新生代和老年代)的垃圾回收,通常伴随STW(Stop-The-World),影响较大


Q:对象是如何从新生代晋升到老年代的?

A:

  • 对象在Survivor区每经历一次Minor GC且存活,年龄就增加1

  • 当年龄超过阈值(默认15)时晋升到老年代

  • 大对象可能直接进入老年代

  • Survivor区空间不足时,部分对象可能提前晋升


Q:什么是内存泄漏?如何排查?

A:

内存泄漏是指对象不再被使用但无法被GC回收的情况。排查方法:

  • 使用jstat -gcutil观察GC情况

  • 使用jmap -histo查看对象分布

  • 使用MAT(Eclipse Memory Analyzer)分析堆转储文件

  • 检查集合类、静态集合、未关闭的资源等常见泄漏点