JVM垃圾回收机制与分代收集算法详解
JVM内存划分为堆、元数据区、栈和程序计数器。垃圾回收聚焦堆与元数据区,通过可达性分析标记不可达对象。分代收集将堆分为新生代和老年代,新生代采用复制算法,老年代采用标记-整理算法。常见的垃圾收集器如Serial、ParNew、CMS、G1等各有适用场景。
一、JVM基础与内存划分
1.1 什么是JVM
JVM,全称Java Virtual Machine,简而言之是用软件模拟出来的虚拟计算机。其核心价值在于:屏蔽底层操作系统的差异,让Java真正实现“一次编写,处处运行”。
1.2 JVM完整执行流程
从一份.java文件到最终被CPU执行,需要四个模块协同工作:
- 类加载子系统 ClassLoader:负责读取.class字节码文件,并将其加载到内存中;
- 运行时数据区 Runtime Data Area:程序运行期间的所有内存数据都存放在此区域;
- 执行引擎 Execution Engine:解析或编译字节码,将其转化为操作系统可识别的指令;
- 本地接口 Native Interface:用于调用C/C++本地库,完成底层操作。
完整流程如下:.java编译成.class字节码文件 → 类加载器将字节码加载到运行时数据区 → 执行引擎解析字节码 → 最终通过本地方法接口调用操作系统资源,由CPU执行。
1.3 内存区域划分
真实操作系统会对进程的地址空间进行功能区域划分,JVM的设计思路正是模仿真实机器和操作系统。JVM从操作系统申请一块内存供自己使用,然后将这部分内存按不同功能进一步细分——就好比把一栋大楼分成若干层,每层再细致划分功能区。
核心区域如下:

1. 程序计数器:记录当前线程执行的字节码行号,执行Java方法时存储指令地址。
注意:这里的程序寄存器指的是内存上的空间,与计算机组成原理中CPU上的程序寄存器并非同一概念。
- 它有一个独特特性:在JVM规范中,这是唯一不会发生OOM的内存区域;
- 线程私有:多线程切换时,靠程序计数器恢复执行位置。
2. 元数据区:保存当前类加载后的数据。
.java文件经过javac编译变成.class文件,要运行它,就必须将字节码文件加载到内存中,这个加载到的位置就是元数据区。元信息指的是类名称、访问权限、继承自哪些类或实现了哪些接口,以及方法名称、参数、返回值等。Java 8之前,元数据区也被称为“方法区”。
3. 栈:保存方法调用关系。
每次调用方法时,程序会进入方法内部执行;方法执行完毕后,返回调用位置继续执行。这里的栈与数据结构中的栈逻辑类似,都是后进先出(LIFO)。
栈与操作系统中的栈有什么关系?
- JVM进程本身是用C++编写的程序,内部存在一系列C++方法调用,维护这些C++方法的栈就是操作系统原生的栈;
- 这些C++方法调用构成了JVM虚拟机程序,通过它们解释执行
.class文件中的字节码,这个过程又涉及Java方法调用,于是构造出了Java的栈。
栈区可能出现的两种异常:
StackOverflow:栈空间通常只有几十MB,如果递归函数出口逻辑错误,导致大量方法调用而不返回,栈深度超出范围就会溢出;- OOM:多线程下大量创建线程,导致虚拟机无法分配栈内存。
4. 堆:保存对象实例,是JVM中最大的空间区域。
举个例子:Test t = new Test()
- 如果t是局部变量,t保存在栈中,函数调用完毕即销毁;
- 如果t是成员变量,t保存在堆中;
- 如果t是静态成员变量,t保存在元数据区,随类加载创建;
- 而
new Test()这个对象一定保存在堆上。
堆上的对象不再使用时就会被释放,这就是垃圾回收要处理的事情。
另外,元数据区和堆是整个Java进程共用的,而程序计数器和栈在一个进程中可能有多份——每个线程各有一份。
二、类加载机制
2.1 类完整生命周期
一个类的完整生命周期包括:加载 → 连接(验证、准备、解析) → 初始化 → 使用 → 卸载。其中前三个阶段合称为类加载过程。
加载 Loading
- 根据类的全限定名(包名+类名,如
java.lang.String)找到对应的.class文件; - 打开文件,读取文件内容到内存中。
验证 Verification:解析并校验.class文件的合规性,防止恶意字节码破坏虚拟机,同时把.class里的内容转换为符合虚拟机规范的结构化数据。
准备 Preparation:给类对象申请内存空间,此时的空间相当于“全0”状态。
解析 Resolution:初始化字符串常量。字符串常量本身包含在.class中,解析阶段需要把解析出的字符串常量放到内存中(元数据区的常量池)。
初始化 Initialization:针对类对象进行最后的初始化,为类对象的各个属性填充数据,包括静态成员变量。如果这个类有父类,也会触发父类的加载。
注意:一个进程中,一个类只加载一次。并不是Java程序一启动就加载所有类,类加载也是典型的懒汉模式(懒加载)。
触发类加载的时机:
- 构造这个类的实例;
- 调用类的静态属性或静态方法;
- 使用某个类时,如果父类还没加载,也会触发父类的加载。
2.2 双亲委派模型
类加载器
JVM中有专门的模块负责类加载,默认提供了三种类加载器:
BootstrapClassLoaderExtensionClassLoaderApplicationClassLoader
ApplicationClassLoader中的parent引用指向ExtensionClassLoader,ExtensionClassLoader中的parent引用指向BootstrapClassLoader,而BootstrapClassLoader的parent指向null,构成了类似链表的关系。
这三个类加载器的主要任务都是找.class文件,但各自负责的目录不同:
BootstrapClassLoader负责找Java标准库中的类;ExtensionClassLoader负责找Java扩展库中的类(可以理解为JVM厂商对Java库做的扩充);ApplicationClassLoader负责找第三方库或当前项目中的类(比如通过Maven下载的依赖)。
双亲委派模型过程
- 进行类加载时,通过全限定名找
.class文件,从ApplicationClassLoader作为入口开始,把“加载类”的任务委托给它的父加载器ExtensionClassLoader; ExtensionClassLoader也不会立即干活,继续把任务委托给它的父加载器BootstrapClassLoader;BootstrapClassLoader的parent为null,只能自己干活——从标准库中查找匹配的.class文件。如果没找到,就把任务“还”给ExtensionClassLoader;ExtensionClassLoader在扩展库中继续查找,找到就加载,没找到就继续“还”给ApplicationClassLoader;ApplicationClassLoader负责查找第三方库或项目目录,找到就加载,没找到就抛出ClassNotFoundException异常。
开发者也可以自定义类加载器,参与到双亲委派模型中。
双亲委派模型的两大优势:
- 避免类重复加载;
- 安全防护:防止开发者自定义
java.lang.Object来篡改核心API。
三、垃圾回收GC原理
GC只关注堆和方法区,虚拟机栈、本地方法栈、程序计数器的生命周期随线程自动释放,不需要回收。
3.1 GC工作流程
GC主要做两件事:
- 找到垃圾(不再使用的对象);
- 释放垃圾(释放对应的内存)。
1. 找到垃圾
(1)引用计数算法
每个对象在创建时,会分配一小块内存空间保存一个整数,表示有多少个引用指向它。每次进行引用赋值时,都会触发引用计数的修改。在Java中,使用对象必须通过引用,当引用计数为0时,说明没有引用指向这个对象,可以判断为垃圾。
缺陷:
- 内存消耗更多,尤其对象本身较小时,引用计数消耗的占比就更大;
- 可能出现循环引用。
举个例子:
class Test{
Test t = null;
}
class Main{
public static void main(String[] args){
Test a = new Test(); // 计数器+1
Test b = new Test(); // 计数器+1
a.t = b; // 计数器+1
b.t = a; // 计数器+1
}
}
此时两个对象实例各有两个引用指向。

如果把a和b都置为null:

程序计数器各减1,不为0,但两个对象都不能被调用了。PHP、Python都采用引用计数算法,但会搭配其他方案来解决循环引用问题。
(2)可达性分析
以代码中特定的对象作为遍历起点(称为GC Roots),进行遍历,判定某个对象是否还能被访问到。每遍历一个对象就标记为“可达”,剩余没有被标记的就认为是不可达,也就是接下来要回收的垃圾。
可作为GC Roots的对象:
- 栈上的局部变量(引用类型);
- 常量池中引用指向的对象;
- 静态成员(引用类型)。
2. 释放垃圾
垃圾清除有四种常见算法:
标记-清除:直接把垃圾对象的内存释放掉。
缺陷:会产生“内存碎片问题”,很难找到大块连续的内存空间。复制算法:把内存分为两部分,起始都保存在A区域;垃圾回收时,把不释放的数据复制到B区域,再把A区域整体释放。这样空闲内存就是连续的。
缺陷:内存空间利用率低,直接砍半;如果不需要释放的数据很多,复制开销很大。标记-整理:类似于顺序表删除,把不需要释放的数据重新搬运整理。
缺陷:内存搬运数据的操作开销也很大,只解决了空间成本问题。分代收集:综合前三种方案。“代”指的是对象的年龄,单位是GC的轮次。
- 每个对象初始年龄为0,每经过一轮GC的可达性分析,不是垃圾的对象年龄+1;
- 根据年龄,把对象分为新生代和老年代。统计发现,新生儿对象大概率很快死亡,而“老油条”对象大概率能活很长时间;
- 对象在新生代反复被GC筛选,年龄增长,最终进入老年代。
堆区域划分:
- 新生代 Young:新建对象存放的地方,98%的对象朝生夕死。新生代进一步划分:
- Eden 伊甸园:占新生代80%,所有新对象诞生于此。从伊甸园到幸存区采用复制算法,因为绝大部分对象活不过一轮GC,复制对象少,开销可控;
- S0/S1 Survivor幸存者:各占10%。幸存区的对象也要经历GC扫描,每轮GC消灭一大批对象,然后通过复制算法把存活的对象复制到另一个幸存区;如果一个对象经过多次复制都存活下来,就可以晋升到老年代了。
- 老年代 Old:存放长期存活的对象和大对象。GC频次比较低,采用标记-整理算法。
一个对象经历的典型区域顺序:伊甸园 → 幸存区 → 幸存区 → … → 幸存区 → 老年代。新生代统一采用复制算法,老年代采用标记整理。

特殊情况:如果某个对象非常大,复制成本很高,可以跳过新生代,直接进入老年代。
3.2 垃圾收集器
在JVM中,由垃圾收集器来实现上述分代回收策略。分代回收只是最基本的设计,落实到具体的收集器上,还有更进阶的策略。
| 收集器 | 分代 | 特点 | 使用场景 |
|---|---|---|---|
| Serial | 新生代 | 单线程串行,STW | Client客户端、小内存程序 |
| ParNew | 新生代 | Serial多线程版,唯一可配合CMS | 老年代CMS搭配使用 |
| Parallel Scavenge | 新生代 | 吞吐量优先,自适应调参 | 后台计算、无交互服务 |
| Serial Old | 老年代 | 单线程标记整理 | Client、CMS失败后备方案 |
| Parallel Old | 老年代 | 多线程吞吐量优先 | 高吞吐离线任务 |
| CMS | 老年代 | 并发低停顿,标记清除,碎片多 | JDK8前互联网低延迟服务 |
| G1 | 全堆分区Region | 可预测停顿,自动压缩,替代CMS | 大内存服务端,JDK9默认 |
总结
以上是核心知识的梳理,希望能帮助大家更好地理解JVM的底层机制。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
FileZilla断点续传设置与操作指南
FileZilla支持断点续传,需客户端与服务器均开启REST命令。设置中确保启用断点续传及继续传输选项。中断后自动或手动从断点恢复。注意服务器支持、传输模式匹配及文件完整性校验。
Debian系统C++编译器位置查找方法
在Debian系统中,通过apt安装的C++编译器g++默认位于 usr bin g++,可使用which或whereis命令验证路径。g++属于build-essential软件包,若未安装则需执行sudoaptinstallbuild-essential。该包还包含gcc、make等编译工具链,g++是GNUC++编译器,实际是符号链接指向具体版本,验证
Debian系统安装C++环境的方法
在Debian系统安装C++开发环境:先sudoaptupdate更新包列表,再sudoaptinstallbuild-essential安装编译工具链,或单独安装g++。用g++--version验证。可选安装VSCode、GDB、CMake等工具并配置默认编译器版本。
Debian系统C++开发环境配置指南
在Debian系统中,先执行aptupdate更新软件包列表,再安装build-essential元包即可获得GCC、G++、Make和GDB。通过运行g++--version命令验证编译器安装成功。可选安装VisualStudioCode、CLion等编辑器及CMake构建工具,并编写一个简单的HelloWorld程序,使用g++编译运行以验证环境配置正确
通过cpustat工具查看CPU状态的具体方法与详细步骤
cpustat是sysstat包中的CPU监控工具,可按固定间隔输出带时间戳的CPU使用率统计。安装后运行cpustat即可实时显示各核心信息,常用指标包括%usr、%sys、%iowait、%steal和%idle,用于定位用户态、内核态或I O瓶颈。高级选项-c可显示单核统计,-m可同时查看内存使用,适合脚本采集和性能分析。
- 热门数据榜
相关攻略
2026-07-25 22:29
2026-07-25 22:29
2026-07-25 22:29
2026-07-25 22:29
2026-07-25 22:18
2026-07-25 22:18
2026-07-25 22:18
2026-07-25 22:18
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程

