跳到主要内容

Java堆内存详解

核心概念

Java堆是JVM管理的内存中最大的一块,也是所有线程共享的内存区域,在虚拟机启动时创建。

堆的唯一目的就是存放对象实例,几乎所有的对象实例和数组都在这里分配内存。

不是"所有"对象都在堆上

随着JIT编译器和逃逸分析技术的成熟,一些优化技术可能改变对象的分配位置:

  • 栈上分配:如果对象未逃逸出方法,可以直接在栈上分配
  • 标量替换:将对象拆解为基本类型,直接在栈上分配

从JDK 1.7开始默认开启逃逸分析(-XX:+DoEscapeAnalysis)。

// 逃逸分析示例
public class EscapeAnalysisDemo {
public static void main(String[] args) {
for (int i = 0; i < 10000000; i++) {
createUser();
}
}

// user对象未逃逸出方法,可能在栈上分配
private static void createUser() {
User user = new User("张三", 25);
System.out.println(user.getName());
}
}

堆的分代结构

堆是垃圾收集器管理的主要区域,也称GC堆(Garbage Collected Heap)

由于现代垃圾收集器基本都采用分代收集算法,堆被划分为不同的区域:

新生代(Young Generation)

新生代用于存放新创建的对象,分为三个区域:

Eden区(伊甸园区)

  • 占新生代的80%
  • 新对象首先在此分配
  • 触发Minor GC的主要区域

Survivor区(幸存者区)

  • 分为From和To两个区,各占10%
  • 存放经过一次GC后存活的对象
  • 采用复制算法

老年代(Old Generation)

老年代存放长期存活的对象:

  • 经过多次Minor GC仍存活的对象
  • 大对象直接进入老年代
  • 占堆的2/3

为什么要分代

分代的核心思想基于一个观察:大部分对象都是朝生夕死的

通过分代,可以为不同区域设置不同的GC策略,提升垃圾回收效率:

特点GC类型GC频率
新生代对象存活率低Minor GC频繁
老年代对象存活率高Major GC低频

Minor GC与Full GC

Minor GC(新生代GC)

  • 发生在新生代
  • 触发条件:Eden区满
  • 速度快,频繁发生
  • 采用复制算法

Major GC/Full GC

  • 清理老年代(Major GC)或整个堆(Full GC)
  • 触发条件:老年代空间不足
  • 速度慢,STW时间长
  • 应尽量避免
// 触发Minor GC的示例
public class MinorGCDemo {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();

for (int i = 0; i < 1000; i++) {
// 不断创建对象,Eden满时触发Minor GC
byte[] data = new byte[1024 * 100]; // 100KB

// 保留部分对象,模拟存活
if (i % 10 == 0) {
list.add(data);
}
}
}
}

堆内存配置参数

基础配置

# 设置堆的初始大小
-Xms512m

# 设置堆的最大大小
-Xmx2g

# 推荐:Xms和Xmx设置相同,避免堆扩容带来的性能损耗
-Xms2g -Xmx2g

新生代配置

# 设置新生代大小
-Xmn512m

# 设置新生代和老年代的比例(默认1:2)
-XX:NewRatio=2

# 设置Eden和Survivor的比例(默认8:1:1)
-XX:SurvivorRatio=8

配置示例

# 生产环境推荐配置
-Xms4g -Xmx4g # 堆大小4G
-Xmn1536m # 新生代1.5G
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1
-XX:+UseG1GC # 使用G1收集器

堆内存溢出

堆最容易出现OutOfMemoryError,表现形式有多种:

Java heap space

创建对象时堆空间不足。

public class HeapOOMDemo {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
// 不断创建1MB的数组,最终导致堆内存溢出
list.add(new byte[1024 * 1024]);
}
}
}

// 运行参数: -Xms20m -Xmx20m
// 输出: java.lang.OutOfMemoryError: Java heap space

GC Overhead Limit Exceeded

JVM花费太多时间执行GC却只能回收很少的堆空间。

public class GCOverheadDemo {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
int i = 0;
while (true) {
// 不断添加数据,GC频繁但回收不了
map.put(i++, String.valueOf(i).intern());
}
}
}

// 输出: java.lang.OutOfMemoryError: GC overhead limit exceeded

堆内存监控

使用JVM参数

# 打印GC详情
-XX:+PrintGCDetails

# 打印GC时间戳
-XX:+PrintGCDateStamps

# GC日志输出到文件
-Xloggc:/path/to/gc.log

使用代码监控

public class HeapMonitor {
public static void printHeapInfo() {
Runtime runtime = Runtime.getRuntime();

long maxMemory = runtime.maxMemory(); // 最大堆内存
long totalMemory = runtime.totalMemory(); // 已分配堆内存
long freeMemory = runtime.freeMemory(); // 空闲内存
long usedMemory = totalMemory - freeMemory; // 已使用内存

System.out.printf("Max: %d MB%n", maxMemory / 1024 / 1024);
System.out.printf("Total: %d MB%n", totalMemory / 1024 / 1024);
System.out.printf("Used: %d MB%n", usedMemory / 1024 / 1024);
System.out.printf("Free: %d MB%n", freeMemory / 1024 / 1024);
}
}

使用JMX监控

public class JMXHeapMonitor {
public static void monitor() {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();

System.out.printf("Init: %d MB%n", heapUsage.getInit() / 1024 / 1024);
System.out.printf("Used: %d MB%n", heapUsage.getUsed() / 1024 / 1024);
System.out.printf("Committed: %d MB%n", heapUsage.getCommitted() / 1024 / 1024);
System.out.printf("Max: %d MB%n", heapUsage.getMax() / 1024 / 1024);
}
}

堆与元空间的关系

JDK 8之后,永久代被**元空间(Metaspace)**取代:

变化点:

  • 元空间使用本地内存,不再占用堆内存
  • 字符串常量池从永久代移到堆中
  • 静态变量也移到堆中

堆内存优化建议

合理设置堆大小

# 根据应用特点设置
# 响应优先型:较小堆,减少GC时间
-Xms1g -Xmx1g

# 吞吐量优先型:较大堆,减少GC频率
-Xms4g -Xmx4g

新生代与老年代比例

# 短生命周期对象多:增大新生代
-XX:NewRatio=1 # 新生代:老年代 = 1:1

# 长生命周期对象多:增大老年代
-XX:NewRatio=3 # 新生代:老年代 = 1:3

选择合适的GC器

场景推荐GC参数
小内存、低延迟Serial-XX:+UseSerialGC
多核、高吞吐Parallel-XX:+UseParallelGC
低延迟、大内存G1-XX:+UseG1GC
超低延迟ZGC-XX:+UseZGC

理解Java堆内存的结构和工作原理,是进行JVM调优、解决内存问题的基础。