JVM 整理(四) 堆

Java虚拟机(JVM)的内存管理是Java开发者必须掌握的核心知识,而**堆(Heap)**是JVM管理的最大一块内存区域,也是垃圾收集器(GC)工作的主要战场。几乎所有对象实例和数组都在堆上分配,堆的运行状况直接决定了应用程序的性能和稳定性。本文将带你全面了解JVM堆的结构、分代工作流程、GC触发机制以及如何通过参数调优避免OOM异常。

1. 堆、栈、方法区的关系

在深入堆之前,我们先理清JVM三大核心内存区域的关系:

  • 堆(Heap):存放对象实例和数组,所有线程共享。

  • 虚拟机栈(Stack):每个线程私有,存储方法调用的栈帧(局部变量表、操作数栈等)。

  • 方法区(Method Area):存储类元数据、常量、静态变量等,JDK 8后元空间替代永久代。

HotSpot虚拟机采用指针访问 对象的方式:Java栈中的reference存储指向堆中对象的地址,而堆中对象又包含指向方法区中类元数据的指针,从而通过对象就能找到它的类信息。

2. 堆空间概述

  • 堆在JVM启动时创建,大小可以通过参数调节。

  • 堆是内存管理的核心区域,几乎所有的对象都在这里分配。

  • 堆内存逻辑上划分为三部分(JDK 7和JDK 8略有不同):

区域 JDK 7及以前 JDK 8及以后
年轻代 Young Generation Young Generation
老年代 Old/Tenured Generation Old/Tenured Generation
永久代/元空间 Permanent Space (方法区实现) Meta Space (方法区实现)

年轻代进一步划分为:

  • 伊甸园区(Eden Space)

  • 两个幸存者区(Survivor Space):S0 和 S1,也叫 From 和 To。

注意:方法区(永久代/元空间)逻辑上属于堆,但HotSpot实现中将其与堆分开管理,因此也叫非堆(Non-Heap)。

3. 分代工作流程

JVM根据对象的生命周期长短,将堆分为年轻代和老年代,采用不同的垃圾回收算法,以提高GC效率。

3.1 新生代(Young Generation)

新生代是对象诞生的地方,几乎所有对象首先在伊甸园区分配。新生代GC称为Minor GC,发生频率高,回收速度快。

工作过程:

  1. 新对象创建在伊甸园区

  2. 当伊甸园空间不足时,触发Minor GC,回收不再被引用的对象。

  3. 将伊甸园中仍然存活的对象移动到空的幸存者区 (例如S0),并将这些对象的年龄计数器设为1。

  4. 下次再创建对象填满伊甸园后,再次触发Minor GC,此时对伊甸园和S0区 进行垃圾回收,存活对象移动到S1区。从S0区移来的对象年龄+1(变为2),从伊甸园移来的对象年龄为1。

  5. 如此反复,每次Minor GC都会将存活对象在S0和S1之间复制,并增加年龄。

  6. 当对象年龄达到阈值(默认15,可通过-XX:MaxTenuringThreshold设置),就会晋升到老年代。

关键点

  • 幸存者区采用复制算法,始终保持一个区为空(To区)。

  • 垃圾回收时,Eden + From 区的存活对象被复制到 To 区。

3.2 老年代(Old Generation)

老年代存放长期存活的对象(如缓存对象、Spring容器中的单例Bean等)。老年代GC称为Major GC(或Old GC),速度比Minor GC慢10倍以上,且会导致更长的STW(Stop The World)暂停。

  • 当老年代空间不足时,触发Major GC。

  • 如果Major GC后仍无法满足内存需求,就会抛出OOM(OutOfMemoryError)

进入老年代的两种常见情况

  1. 对象年龄达到阈值(默认15),晋升到老年代。

  2. 大对象(如很长的数组或字符串)直接在伊甸园放不下,会尝试直接进入老年代。如果老年代也放不下,则抛出OOM。

3.3 永久代 / 元空间

  • 永久代(JDK 7及以前) :存储类元数据、常量池等。大小固定,容易出现java.lang.OutOfMemoryError: PermGen space

  • 元空间(JDK 8及以后) :使用本地内存,默认无上限,可通过-XX:MetaspaceSize-XX:MaxMetaspaceSize调节。元空间溢出错误为java.lang.OutOfMemoryError: Meta space

方法区的回收效率很低,通常在Full GC时触发。如果程序加载了大量类(如动态代理、热部署),需要注意元空间大小。

3.4 GC类型总结

GC类型 回收区域 特点
Minor GC 年轻代(Eden + Survivor) 频繁、快速,STW时间短
Major GC 老年代 较慢,常伴随Full GC
Full GC 整个堆 + 方法区 非常慢,STW时间长
Mixed GC 年轻代 + 部分老年代(G1专用) G1垃圾收集器的特点

触发条件

  • Minor GC:年轻代Eden空间不足。

  • Major GC/Full GC

    • 老年代空间不足。

    • 调用System.gc()(建议触发Full GC)。

    • 方法区(元空间)空间不足。

    • Minor GC前判断老年代空间不足以容纳晋升对象时,可能提前触发Full GC。

4. JVM结构总结

  • :线程私有,存储栈帧。

  • :线程共享,分代管理。

  • 方法区:存储类元数据。

  • 程序计数器:线程私有,记录字节码执行地址。

  • 本地方法栈:为native方法服务。

5. 堆参数设置与调优

5.1 查看堆内存大小

使用Runtime类可以获取JVM堆内存信息:

java 复制代码
public class HeapInfo {
    public static void main(String[] args) {
        long total = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        long max = Runtime.getRuntime().maxMemory() / 1024 / 1024;
        System.out.println("-Xms: " + total + "M");
        System.out.println("-Xmx: " + max + "M");
    }
}
  • -Xms:堆起始大小,默认为物理内存的1/64。

  • -Xmx:堆最大大小,默认为物理内存的1/4。

  • 建议将-Xms-Xmx设为相同值,避免运行时动态扩容带来的性能开销。

5.2 常用堆参数

参数 作用
-Xms<size> 设置堆初始大小
-Xmx<size> 设置堆最大大小
-Xmn<size> 设置年轻代大小(一般占堆的1/3)
-XX:NewRatio 老年代与年轻代的比例(默认2:1)
-XX:SurvivorRatio Eden与Survivor的比例(默认8:1:1)
-XX:MaxTenuringThreshold 晋升老年代年龄阈值(默认15)
-XX:+PrintGCDetails 打印GC详细信息
-XX:+HeapDumpOnOutOfMemoryError 发生OOM时自动导出堆转储文件

5.3 OOM演示与堆转储分析

OOM常见场景

  • 老年代空间不足,Full GC后仍然无法容纳新对象。

  • 大对象直接进入老年代失败。

  • 元空间类加载过多(如热部署、反射生成大量代理类)。

示例代码(模拟OOM)

java 复制代码
/**
 * VM参数:-Xms30m -Xmx30m -XX:+HeapDumpOnOutOfMemoryError
 */
public class OOMDemo {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[1024 * 1024]); // 每次分配1MB
        }
    }
}

运行后将抛出java.lang.OutOfMemoryError: Java heap space,并在工作目录生成堆转储文件(.hprof)。使用**MAT(Memory Analyzer Tool)**分析该文件,可以快速定位内存泄漏的根源(例如哪个对象占用了最多内存,GC Roots链等)。

6. 监控工具:Java VisualVM + VisualGC

  • Java VisualVM(JDK自带)是一个强大的多合一工具,可以监控本地和远程JVM进程。

  • 通过安装VisualGC插件,可以实时查看堆各代的使用情况、GC次数和耗时。

安装步骤

  1. 启动VisualVM。

  2. 菜单栏:工具 → 插件 → 设置 → 编辑插件中心地址,改为对应JDK版本的插件地址(如https://visualvm.github.io/uc/8u131/updates.xml.gz)。

  3. 在"可用插件"中搜索并安装VisualGC。

  4. 重启后,打开本地JVM进程,即可看到VisualGC标签,直观展示Eden、S0、S1、Old、Metaspace的动态变化。

7. 总结

JVM堆是Java程序运行的基础,理解堆的分代结构和工作流程,有助于我们写出更高效的代码,并快速定位内存问题。关键点回顾:

  • 堆分年轻代和老年代,不同代采用不同GC算法。

  • 对象首先在Eden分配,经过多次Minor GC后晋升到老年代。

  • 大对象可能直接进入老年代,导致提前触发Major GC。

  • 合理设置堆参数(如-Xms-Xmx)能减少GC停顿,提升吞吐量。

  • 使用VisualVM、MAT等工具监控堆状态,是解决内存泄漏的有效手段。

在实际开发中,我们应关注对象的生命周期,避免不必要的大对象,及时释放不再使用的引用,让JVM的内存管理更加高效。

相关推荐
常利兵2 小时前
Room 3.0大变身:安卓开发的新挑战与机遇
android·jvm·oracle
NGC_66112 小时前
JVM堆分区详解
jvm
左左右右左右摇晃2 小时前
JVM 笔记 (一)介绍JVM
jvm·笔记
2401_832035342 小时前
使用Python处理计算机图形学(PIL/Pillow)
jvm·数据库·python
苦瓜小生2 小时前
【黑马点评学习笔记 | 实战篇 】| 5-分布式锁+初步秒杀优化
笔记·分布式·学习
金蕊泛流霞2 小时前
Spring AI Alibaba笔记
java·笔记·spring
dapeng28702 小时前
Django全栈开发入门:构建一个博客系统
jvm·数据库·python
小陳参上2 小时前
持久化数据库实现:确保数据持久性与可靠性
java·jvm·数据库
NGC_66112 小时前
四种引用解析
jvm