第25篇:Java JVM入门:内存模型与垃圾回收,理解JVM底层

📌 专栏:Java底层核心进阶

💡 前言

很多Java开发者日常CRUD开发,熟练使用各种语法和框架,却始终对JVM底层一知半解。遇到内存溢出、程序卡顿、GC频繁、服务雪崩等线上问题时,完全无从下手,更别说JVM调优。

读完本文,你将彻底搞懂:

  • JVM整体架构及各模块核心职责

  • 堆、栈、方法区、程序计数器等内存区域的作用与特点

  • 垃圾回收GC的核心逻辑与触发条件

  • 三大经典GC算法的原理、优缺点及适用场景

  • 内存泄漏与内存溢出的区别、成因及解决思路


一、JVM 整体架构概述

JVM(Java Virtual Machine,Java虚拟机)是一款跨平台的虚拟机,是Java程序实现一次编译,到处运行的核心。Java源码编译为.class字节码文件后,由JVM解析、加载、执行,屏蔽了底层操作系统、硬件的差异。

标准JVM整体架构分为三大核心模块 + 运行时内存区,四大模块协同完成程序运行:

1. 类加载器(ClassLoader)

负责加载磁盘中的.class字节码文件,将类信息、方法信息、常量等资源加载到JVM内存中,分为启动类加载器、扩展类加载器、应用类加载器,遵循双亲委派模型。

2. 执行引擎

JVM的“CPU”,负责解析字节码指令并执行,包含解释器(即时解释执行)、JIT编译器(热点代码即时编译优化,提升运行效率)、垃圾回收器(负责内存垃圾回收)。

3. 本地方法接口(JNI)

对接本地Native方法(C/C++编写的方法),弥补Java底层操作短板,比如系统调用、硬件操作等。

4. 运行时数据区(核心重点)

也就是我们常说的JVM内存模型,程序运行过程中所有数据、变量、对象、指令信息均存储在此区域,是本文核心讲解内容。


二、JVM 运行时内存模型(五大区域)

根据《Java虚拟机规范》,JVM运行时内存划分为五大核心区域,按照线程归属可分为两类:线程私有区域(随线程创建而生、线程销毁而回收)、线程共享区域(全局共享,JVM启动创建、关闭销毁)。

1. 程序计数器(Program Counter Register)

归属:线程私有

核心作用:记录当前线程正在执行的字节码指令地址,用于线程切换后恢复执行位置。

Java是多线程语言,CPU通过时间片轮转切换线程,当线程暂停执行时,程序计数器会记录当前执行进度,线程重新获取CPU时间片后,精准接续执行。

核心特点

  • JVM中唯一不会发生OutOfMemoryError(内存溢出)的区域

  • 内存占用极小、结构简单、线程隔离、无垃圾回收

  • 生命周期与当前线程完全一致

2. Java虚拟机栈(JVM栈/栈内存)

归属:线程私有

核心作用:存储方法执行的临时数据,每调用一个方法就会创建一个栈帧,方法执行完毕栈帧自动出栈销毁。

栈帧中包含:局部变量表、操作数栈、动态链接、方法返回地址等信息,我们日常定义的局部变量、基本数据类型变量均存储在栈中。

核心特点

  • 先进后出的栈结构,执行效率极高

  • 生命周期跟随线程,线程结束栈内存立即释放

  • 栈空间固定,可通过JVM参数-Xss设置大小

  • 栈溢出会抛出StackOverflowError(常见于递归死循环、方法嵌套过深)

3. 本地方法栈

归属:线程私有

核心作用:作用与虚拟机栈类似,专门为JVM调用的Native本地方法服务,存储本地方法执行的栈帧信息。

核心特点

  • 同样会抛出StackOverflowError和OutOfMemoryError

  • 底层由C/C++实现,Java开发者基本无需手动操作

4. 堆内存(Heap)—— GC主战场

归属:线程共享

核心作用:JVM中最大的内存区域,所有通过new关键字创建的对象、数组均存储在堆中,是垃圾回收的核心区域。

为了提升GC效率,堆内存被细分为:新生代(Young)、老年代(Old)。其中新生代又分为Eden区、From Survivor、To Survivor,默认比例8:1:1。

核心特点

  • 全局线程共享,所有线程均可访问堆中的对象

  • 内存空间大、生命周期长,随JVM启动和关闭

  • 频繁发生GC,用于回收失效对象

  • 堆内存不足会抛出OutOfMemoryError(堆内存溢出)

5. 方法区(Method Area)

归属:线程共享

核心作用:存储JVM加载的类元数据,包含类名、方法名、字段信息、常量池、静态变量、即时编译代码等全局静态资源。

JDK1.8是重要分水岭:JDK1.8之前方法区存在永久代,JDK1.8之后移除永久代,使用元空间(Metaspace)替代,元空间使用本地内存,不再占用JVM堆内存,大幅降低了方法区溢出概率。

核心特点

  • 全局共享,存储静态、全局、类级别数据

  • 极少触发GC,仅回收部分废弃常量和无用类

  • 元空间溢出会抛出OutOfMemoryError


三、垃圾回收(GC)核心原理

Java语言最大的优势之一就是自动垃圾回收,无需开发者手动申请和释放内存,由JVM的垃圾回收器自动识别、回收内存中无效对象,避免内存浪费。

1. GC核心定义

GC(Garbage Collection)即垃圾回收,指JVM自动扫描堆内存,识别没有任何有效引用指向的对象,清空其占用的内存空间,实现内存复用,防止内存堆积。

2. 垃圾判定依据:GC Roots

JVM通过可达性分析算法判定对象是否存活:以GC Roots为起始节点遍历内存,能遍历到的对象为存活对象,无法遍历到的对象即为垃圾对象,可被回收。

常见GC Roots:

  • 虚拟机栈中引用的对象

  • 方法区中静态变量、常量引用的对象

  • 本地方法栈Native引用的对象

  • 活跃线程的引用对象

3. GC的触发条件

GC不会随意触发,仅在满足特定条件时自动执行,分为Minor GC(新生代GC)Full GC(全局GC)

(1)Minor GC 触发条件

新生代Eden区内存空间不足,无法为新对象分配内存时,触发Minor GC,回收新生代无效对象,频率高、速度快、耗时短。

(2)Full GC 触发条件
  • 老年代内存空间不足

  • Minor GC后存活对象过多,无法放入Survivor区,直接进入老年代

  • 方法区/元空间内存不足

  • 手动调用System.gc()(仅建议,不保证立即执行)

  • JVM出现内存预警、并发GC失败等异常场景

注意:Full GC会回收整个堆内存(新生代+老年代),耗时极长、会造成程序STW(暂停所有用户线程),线上项目需尽量避免频繁Full GC。


四、三大经典GC回收算法详解

GC算法是垃圾回收的核心,所有垃圾回收器均基于三大基础算法优化迭代而来,分别是:标记-清除、标记-复制、标记-整理。

1. 标记-清除算法(Mark-Sweep)

执行流程:分为两步,第一步标记,遍历内存标记所有存活对象;第二步清除,清空所有未被标记的垃圾对象。

优点:算法简单、实现容易,无需移动对象位置。

缺点

  • 产生大量内存碎片,回收后的内存分散不连续

  • 内存碎片过多时,大对象无法分配连续内存,容易触发OOM

  • 两次遍历内存,效率偏低

适用场景:老年代低频GC场景

2. 标记-复制算法(Mark-Copy)

执行流程:将内存划分为两个大小相等的区域,平时只使用其中一块。GC时标记所有存活对象,将存活对象完整复制到另一块空白内存中,复制完成后直接清空原内存区域。

新生代8:1:1的分区设计,就是为了适配标记复制算法,大幅提升回收效率。

优点

  • 无内存碎片,内存空间连续规整

  • 只需标记存活对象,回收效率极高

缺点

  • 内存利用率低,永久浪费一半内存空间

  • 存活对象较多时,复制成本极高、效率下降

适用场景:新生代高频GC(存活对象少、垃圾对象多)

3. 标记-整理算法(Mark-Compact)

执行流程:第一步标记所有存活对象,第二步将所有存活对象向内存一端移动、紧凑排列,最后直接清空边界外的全部垃圾内存。

优点

  • 无内存碎片,内存连续规整

  • 内存利用率100%,无空间浪费

缺点

  • 需要移动所有存活对象,开销大、速度慢

  • STW时间更长,对程序性能影响较大

适用场景:老年代GC(存活对象多、垃圾少,不适合复制算法)


五、内存泄漏 vs 内存溢出:成因与解决方案

日常开发中最常见的两个内存问题:内存泄漏(Memory Leak)内存溢出(OOM),二者因果关联,内存泄漏长期堆积最终必然导致内存溢出。

1. 内存泄漏(Memory Leak)

定义:程序中存在无效对象被持续引用,GC无法回收这些本该销毁的对象,导致内存被无效占用、无法释放,内存使用率持续升高。

常见成因

  • 静态集合类(static List/Map)长期持有对象引用

  • 未关闭资源:IO流、数据库连接、Redis连接、线程池未关闭

  • 匿名内部类、非静态内部类持有外部类引用

  • 全局变量滥用,对象使用完毕未置空

  • 自定义缓存无过期策略,数据无限堆积

解决思路

  • 及时关闭所有IO、连接、线程等资源,使用try-with-resources自动关闭流

  • 缓存设置过期时间、淘汰策略,使用弱引用存储缓存对象

  • 避免静态集合无限存储数据,定期清理无效数据

  • 使用内存分析工具(MAT、JProfiler)定位泄漏对象

2. 内存溢出(OOM,OutOfMemoryError)

定义:JVM内存空间全部被占满,没有多余内存分配给新对象,程序无法继续运行,直接抛出异常崩溃。

常见成因

  • 长期内存泄漏堆积,耗尽可用内存

  • 一次性创建超大对象、超大集合,批量读取超大文件

  • 死循环不断创建新对象,无销毁逻辑

  • JVM内存参数配置过小,无法支撑业务并发量

解决思路

  • 优先排查内存泄漏问题,修复代码漏洞

  • 优化代码逻辑,避免循环创建对象、一次性加载海量数据

  • 调整JVM参数,合理扩容堆内存、元空间大小

  • 分批次、分页处理大数据,避免一次性加载入内存


六、总结与后续预告

本文核心总结

  1. JVM内存分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)两大类型,各区域各司其职

  2. 堆是GC主战场,分为新生代、老年代,不同区域适配不同回收算法

  3. 三大GC算法各有优劣:复制算法适配新生代,标记整理适配老年代,标记清除易产生内存碎片

  4. 内存泄漏是缓慢堆积的隐患,内存溢出是最终爆发的结果,调优核心是杜绝泄漏、优化GC

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐