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 GenerationYoung Generation老年代Old/Tenured GenerationOld/Tenured Generation永久代/元空间Permanent Space (方法区实现)Meta Space (方法区实现)年轻代进一步划分为伊甸园区Eden Space两个幸存者区Survivor SpaceS0 和 S1也叫 From 和 To。注意方法区永久代/元空间逻辑上属于堆但HotSpot实现中将其与堆分开管理因此也叫非堆Non-Heap。3. 分代工作流程JVM根据对象的生命周期长短将堆分为年轻代和老年代采用不同的垃圾回收算法以提高GC效率。3.1 新生代Young Generation新生代是对象诞生的地方几乎所有对象首先在伊甸园区分配。新生代GC称为Minor GC发生频率高回收速度快。工作过程新对象创建在伊甸园区。当伊甸园空间不足时触发Minor GC回收不再被引用的对象。将伊甸园中仍然存活的对象移动到空的幸存者区例如S0并将这些对象的年龄计数器设为1。下次再创建对象填满伊甸园后再次触发Minor GC此时对伊甸园和S0区进行垃圾回收存活对象移动到S1区。从S0区移来的对象年龄1变为2从伊甸园移来的对象年龄为1。如此反复每次Minor GC都会将存活对象在S0和S1之间复制并增加年龄。当对象年龄达到阈值默认15可通过-XX:MaxTenuringThreshold设置就会晋升到老年代。关键点幸存者区采用复制算法始终保持一个区为空To区。垃圾回收时Eden From 区的存活对象被复制到 To 区。3.2 老年代Old Generation老年代存放长期存活的对象如缓存对象、Spring容器中的单例Bean等。老年代GC称为Major GC或Old GC速度比Minor GC慢10倍以上且会导致更长的STWStop The World暂停。当老年代空间不足时触发Major GC。如果Major GC后仍无法满足内存需求就会抛出OOMOutOfMemoryError。进入老年代的两种常见情况对象年龄达到阈值默认15晋升到老年代。大对象如很长的数组或字符串直接在伊甸园放不下会尝试直接进入老年代。如果老年代也放不下则抛出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 GCFull 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堆内存信息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 常用堆参数参数作用-Xmssize设置堆初始大小-Xmxsize设置堆最大大小-Xmnsize设置年轻代大小一般占堆的1/3-XX:NewRatio老年代与年轻代的比例默认2:1-XX:SurvivorRatioEden与Survivor的比例默认8:1:1-XX:MaxTenuringThreshold晋升老年代年龄阈值默认15-XX:PrintGCDetails打印GC详细信息-XX:HeapDumpOnOutOfMemoryError发生OOM时自动导出堆转储文件5.3 OOM演示与堆转储分析OOM常见场景老年代空间不足Full GC后仍然无法容纳新对象。大对象直接进入老年代失败。元空间类加载过多如热部署、反射生成大量代理类。示例代码模拟OOM/** * VM参数-Xms30m -Xmx30m -XX:HeapDumpOnOutOfMemoryError */ public class OOMDemo { public static void main(String[] args) { Listbyte[] list new ArrayList(); while (true) { list.add(new byte[1024 * 1024]); // 每次分配1MB } } }运行后将抛出java.lang.OutOfMemoryError: Java heap space并在工作目录生成堆转储文件.hprof。使用MATMemory Analyzer Tool分析该文件可以快速定位内存泄漏的根源例如哪个对象占用了最多内存GC Roots链等。6. 监控工具Java VisualVM VisualGCJava VisualVMJDK自带是一个强大的多合一工具可以监控本地和远程JVM进程。通过安装VisualGC插件可以实时查看堆各代的使用情况、GC次数和耗时。安装步骤启动VisualVM。菜单栏工具 → 插件 → 设置 → 编辑插件中心地址改为对应JDK版本的插件地址如https://visualvm.github.io/uc/8u131/updates.xml.gz。在“可用插件”中搜索并安装VisualGC。重启后打开本地JVM进程即可看到VisualGC标签直观展示Eden、S0、S1、Old、Metaspace的动态变化。7. 总结JVM堆是Java程序运行的基础理解堆的分代结构和工作流程有助于我们写出更高效的代码并快速定位内存问题。关键点回顾堆分年轻代和老年代不同代采用不同GC算法。对象首先在Eden分配经过多次Minor GC后晋升到老年代。大对象可能直接进入老年代导致提前触发Major GC。合理设置堆参数如-Xms、-Xmx能减少GC停顿提升吞吐量。使用VisualVM、MAT等工具监控堆状态是解决内存泄漏的有效手段。在实际开发中我们应关注对象的生命周期避免不必要的大对象及时释放不再使用的引用让JVM的内存管理更加高效。
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 GenerationYoung Generation老年代Old/Tenured GenerationOld/Tenured Generation永久代/元空间Permanent Space (方法区实现)Meta Space (方法区实现)年轻代进一步划分为伊甸园区Eden Space两个幸存者区Survivor SpaceS0 和 S1也叫 From 和 To。注意方法区永久代/元空间逻辑上属于堆但HotSpot实现中将其与堆分开管理因此也叫非堆Non-Heap。3. 分代工作流程JVM根据对象的生命周期长短将堆分为年轻代和老年代采用不同的垃圾回收算法以提高GC效率。3.1 新生代Young Generation新生代是对象诞生的地方几乎所有对象首先在伊甸园区分配。新生代GC称为Minor GC发生频率高回收速度快。工作过程新对象创建在伊甸园区。当伊甸园空间不足时触发Minor GC回收不再被引用的对象。将伊甸园中仍然存活的对象移动到空的幸存者区例如S0并将这些对象的年龄计数器设为1。下次再创建对象填满伊甸园后再次触发Minor GC此时对伊甸园和S0区进行垃圾回收存活对象移动到S1区。从S0区移来的对象年龄1变为2从伊甸园移来的对象年龄为1。如此反复每次Minor GC都会将存活对象在S0和S1之间复制并增加年龄。当对象年龄达到阈值默认15可通过-XX:MaxTenuringThreshold设置就会晋升到老年代。关键点幸存者区采用复制算法始终保持一个区为空To区。垃圾回收时Eden From 区的存活对象被复制到 To 区。3.2 老年代Old Generation老年代存放长期存活的对象如缓存对象、Spring容器中的单例Bean等。老年代GC称为Major GC或Old GC速度比Minor GC慢10倍以上且会导致更长的STWStop The World暂停。当老年代空间不足时触发Major GC。如果Major GC后仍无法满足内存需求就会抛出OOMOutOfMemoryError。进入老年代的两种常见情况对象年龄达到阈值默认15晋升到老年代。大对象如很长的数组或字符串直接在伊甸园放不下会尝试直接进入老年代。如果老年代也放不下则抛出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 GCFull 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堆内存信息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 常用堆参数参数作用-Xmssize设置堆初始大小-Xmxsize设置堆最大大小-Xmnsize设置年轻代大小一般占堆的1/3-XX:NewRatio老年代与年轻代的比例默认2:1-XX:SurvivorRatioEden与Survivor的比例默认8:1:1-XX:MaxTenuringThreshold晋升老年代年龄阈值默认15-XX:PrintGCDetails打印GC详细信息-XX:HeapDumpOnOutOfMemoryError发生OOM时自动导出堆转储文件5.3 OOM演示与堆转储分析OOM常见场景老年代空间不足Full GC后仍然无法容纳新对象。大对象直接进入老年代失败。元空间类加载过多如热部署、反射生成大量代理类。示例代码模拟OOM/** * VM参数-Xms30m -Xmx30m -XX:HeapDumpOnOutOfMemoryError */ public class OOMDemo { public static void main(String[] args) { Listbyte[] list new ArrayList(); while (true) { list.add(new byte[1024 * 1024]); // 每次分配1MB } } }运行后将抛出java.lang.OutOfMemoryError: Java heap space并在工作目录生成堆转储文件.hprof。使用MATMemory Analyzer Tool分析该文件可以快速定位内存泄漏的根源例如哪个对象占用了最多内存GC Roots链等。6. 监控工具Java VisualVM VisualGCJava VisualVMJDK自带是一个强大的多合一工具可以监控本地和远程JVM进程。通过安装VisualGC插件可以实时查看堆各代的使用情况、GC次数和耗时。安装步骤启动VisualVM。菜单栏工具 → 插件 → 设置 → 编辑插件中心地址改为对应JDK版本的插件地址如https://visualvm.github.io/uc/8u131/updates.xml.gz。在“可用插件”中搜索并安装VisualGC。重启后打开本地JVM进程即可看到VisualGC标签直观展示Eden、S0、S1、Old、Metaspace的动态变化。7. 总结JVM堆是Java程序运行的基础理解堆的分代结构和工作流程有助于我们写出更高效的代码并快速定位内存问题。关键点回顾堆分年轻代和老年代不同代采用不同GC算法。对象首先在Eden分配经过多次Minor GC后晋升到老年代。大对象可能直接进入老年代导致提前触发Major GC。合理设置堆参数如-Xms、-Xmx能减少GC停顿提升吞吐量。使用VisualVM、MAT等工具监控堆状态是解决内存泄漏的有效手段。在实际开发中我们应关注对象的生命周期避免不必要的大对象及时释放不再使用的引用让JVM的内存管理更加高效。