游乐游手机版
首页/编程语言/文章详情

JVM垃圾回收机制与分代收集算法详解

时间:2026-07-21 20:10
JVM内存划分为堆、元数据区、栈和程序计数器。垃圾回收聚焦堆与元数据区,通过可达性分析标记不可达对象。分代收集将堆分为新生代和老年代,新生代采用复制算法,老年代采用标记-整理算法。常见的垃圾收集器如Serial、ParNew、CMS、G1等各有适用场景。

一、JVM基础与内存划分

1.1 什么是JVM

JVM,全称Java Virtual Machine,简而言之是用软件模拟出来的虚拟计算机。其核心价值在于:屏蔽底层操作系统的差异,让Java真正实现“一次编写,处处运行”。

1.2 JVM完整执行流程

从一份.java文件到最终被CPU执行,需要四个模块协同工作:

  1. 类加载子系统 ClassLoader:负责读取.class字节码文件,并将其加载到内存中;
  2. 运行时数据区 Runtime Data Area:程序运行期间的所有内存数据都存放在此区域;
  3. 执行引擎 Execution Engine:解析或编译字节码,将其转化为操作系统可识别的指令;
  4. 本地接口 Native Interface:用于调用C/C++本地库,完成底层操作。

完整流程如下:.java编译成.class字节码文件 → 类加载器将字节码加载到运行时数据区 → 执行引擎解析字节码 → 最终通过本地方法接口调用操作系统资源,由CPU执行。

1.3 内存区域划分

真实操作系统会对进程的地址空间进行功能区域划分,JVM的设计思路正是模仿真实机器和操作系统。JVM从操作系统申请一块内存供自己使用,然后将这部分内存按不同功能进一步细分——就好比把一栋大楼分成若干层,每层再细致划分功能区。

核心区域如下:

Java虚拟机垃圾回收GC怎么工作?分代收集算法详解

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

  1. 根据类的全限定名(包名+类名,如java.lang.String)找到对应的.class文件;
  2. 打开文件,读取文件内容到内存中。

验证 Verification:解析并校验.class文件的合规性,防止恶意字节码破坏虚拟机,同时把.class里的内容转换为符合虚拟机规范的结构化数据。

准备 Preparation:给类对象申请内存空间,此时的空间相当于“全0”状态。

解析 Resolution:初始化字符串常量。字符串常量本身包含在.class中,解析阶段需要把解析出的字符串常量放到内存中(元数据区的常量池)。

初始化 Initialization:针对类对象进行最后的初始化,为类对象的各个属性填充数据,包括静态成员变量。如果这个类有父类,也会触发父类的加载。

注意:一个进程中,一个类只加载一次。并不是Java程序一启动就加载所有类,类加载也是典型的懒汉模式(懒加载)。

触发类加载的时机:

  1. 构造这个类的实例;
  2. 调用类的静态属性或静态方法;
  3. 使用某个类时,如果父类还没加载,也会触发父类的加载。

2.2 双亲委派模型

类加载器

JVM中有专门的模块负责类加载,默认提供了三种类加载器:

  1. BootstrapClassLoader
  2. ExtensionClassLoader
  3. ApplicationClassLoader

ApplicationClassLoader中的parent引用指向ExtensionClassLoaderExtensionClassLoader中的parent引用指向BootstrapClassLoader,而BootstrapClassLoaderparent指向null,构成了类似链表的关系。

这三个类加载器的主要任务都是找.class文件,但各自负责的目录不同:

  • BootstrapClassLoader负责找Java标准库中的类;
  • ExtensionClassLoader负责找Java扩展库中的类(可以理解为JVM厂商对Java库做的扩充);
  • ApplicationClassLoader负责找第三方库或当前项目中的类(比如通过Maven下载的依赖)。

双亲委派模型过程

  1. 进行类加载时,通过全限定名找.class文件,从ApplicationClassLoader作为入口开始,把“加载类”的任务委托给它的父加载器ExtensionClassLoader
  2. ExtensionClassLoader也不会立即干活,继续把任务委托给它的父加载器BootstrapClassLoader
  3. BootstrapClassLoaderparent为null,只能自己干活——从标准库中查找匹配的.class文件。如果没找到,就把任务“还”给ExtensionClassLoader
  4. ExtensionClassLoader在扩展库中继续查找,找到就加载,没找到就继续“还”给ApplicationClassLoader
  5. ApplicationClassLoader负责查找第三方库或项目目录,找到就加载,没找到就抛出ClassNotFoundException异常。

开发者也可以自定义类加载器,参与到双亲委派模型中。

双亲委派模型的两大优势:

  • 避免类重复加载;
  • 安全防护:防止开发者自定义java.lang.Object来篡改核心API。

三、垃圾回收GC原理

GC只关注堆和方法区,虚拟机栈、本地方法栈、程序计数器的生命周期随线程自动释放,不需要回收。

3.1 GC工作流程

GC主要做两件事:

  1. 找到垃圾(不再使用的对象);
  2. 释放垃圾(释放对应的内存)。

1. 找到垃圾

(1)引用计数算法

每个对象在创建时,会分配一小块内存空间保存一个整数,表示有多少个引用指向它。每次进行引用赋值时,都会触发引用计数的修改。在Java中,使用对象必须通过引用,当引用计数为0时,说明没有引用指向这个对象,可以判断为垃圾。

缺陷:

  1. 内存消耗更多,尤其对象本身较小时,引用计数消耗的占比就更大;
  2. 可能出现循环引用。

举个例子:

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
    }
}

此时两个对象实例各有两个引用指向。

Java虚拟机垃圾回收GC怎么工作?分代收集算法详解

如果把a和b都置为null:

Java虚拟机垃圾回收GC怎么工作?分代收集算法详解

程序计数器各减1,不为0,但两个对象都不能被调用了。PHP、Python都采用引用计数算法,但会搭配其他方案来解决循环引用问题。

(2)可达性分析

以代码中特定的对象作为遍历起点(称为GC Roots),进行遍历,判定某个对象是否还能被访问到。每遍历一个对象就标记为“可达”,剩余没有被标记的就认为是不可达,也就是接下来要回收的垃圾。

可作为GC Roots的对象:

  1. 栈上的局部变量(引用类型);
  2. 常量池中引用指向的对象;
  3. 静态成员(引用类型)。

2. 释放垃圾

垃圾清除有四种常见算法:

  1. 标记-清除:直接把垃圾对象的内存释放掉。
    缺陷:会产生“内存碎片问题”,很难找到大块连续的内存空间。

  2. 复制算法:把内存分为两部分,起始都保存在A区域;垃圾回收时,把不释放的数据复制到B区域,再把A区域整体释放。这样空闲内存就是连续的。
    缺陷:内存空间利用率低,直接砍半;如果不需要释放的数据很多,复制开销很大。

  3. 标记-整理:类似于顺序表删除,把不需要释放的数据重新搬运整理。
    缺陷:内存搬运数据的操作开销也很大,只解决了空间成本问题。

  4. 分代收集:综合前三种方案。“代”指的是对象的年龄,单位是GC的轮次。

    1. 每个对象初始年龄为0,每经过一轮GC的可达性分析,不是垃圾的对象年龄+1;
    2. 根据年龄,把对象分为新生代和老年代。统计发现,新生儿对象大概率很快死亡,而“老油条”对象大概率能活很长时间;
    3. 对象在新生代反复被GC筛选,年龄增长,最终进入老年代。

堆区域划分

  • 新生代 Young:新建对象存放的地方,98%的对象朝生夕死。新生代进一步划分:
    • Eden 伊甸园:占新生代80%,所有新对象诞生于此。从伊甸园到幸存区采用复制算法,因为绝大部分对象活不过一轮GC,复制对象少,开销可控;
    • S0/S1 Survivor幸存者:各占10%。幸存区的对象也要经历GC扫描,每轮GC消灭一大批对象,然后通过复制算法把存活的对象复制到另一个幸存区;如果一个对象经过多次复制都存活下来,就可以晋升到老年代了。
  • 老年代 Old:存放长期存活的对象和大对象。GC频次比较低,采用标记-整理算法。

一个对象经历的典型区域顺序:伊甸园 → 幸存区 → 幸存区 → … → 幸存区 → 老年代。新生代统一采用复制算法,老年代采用标记整理。

Java虚拟机垃圾回收GC怎么工作?分代收集算法详解

特殊情况:如果某个对象非常大,复制成本很高,可以跳过新生代,直接进入老年代。

3.2 垃圾收集器

在JVM中,由垃圾收集器来实现上述分代回收策略。分代回收只是最基本的设计,落实到具体的收集器上,还有更进阶的策略。

收集器分代特点使用场景
Serial新生代单线程串行,STWClient客户端、小内存程序
ParNew新生代Serial多线程版,唯一可配合CMS老年代CMS搭配使用
Parallel Scavenge新生代吞吐量优先,自适应调参后台计算、无交互服务
Serial Old老年代单线程标记整理Client、CMS失败后备方案
Parallel Old老年代多线程吞吐量优先高吞吐离线任务
CMS老年代并发低停顿,标记清除,碎片多JDK8前互联网低延迟服务
G1全堆分区Region可预测停顿,自动压缩,替代CMS大内存服务端,JDK9默认

总结

以上是核心知识的梳理,希望能帮助大家更好地理解JVM的底层机制。

来源:https://www.jb51.net/program/367750bu7.htm
上一篇Java后端三层架构理解及与MVC模式的区别 下一篇一文读懂C语言函数栈帧结构与调用过程
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
FileZilla断点续传设置与操作指南
编程语言 · 2026-07-25

FileZilla断点续传设置与操作指南

FileZilla支持断点续传,需客户端与服务器均开启REST命令。设置中确保启用断点续传及继续传输选项。中断后自动或手动从断点恢复。注意服务器支持、传输模式匹配及文件完整性校验。

Debian系统C++编译器位置查找方法
编程语言 · 2026-07-25

Debian系统C++编译器位置查找方法

在Debian系统中,通过apt安装的C++编译器g++默认位于 usr bin g++,可使用which或whereis命令验证路径。g++属于build-essential软件包,若未安装则需执行sudoaptinstallbuild-essential。该包还包含gcc、make等编译工具链,g++是GNUC++编译器,实际是符号链接指向具体版本,验证

Debian系统安装C++环境的方法
编程语言 · 2026-07-25

Debian系统安装C++环境的方法

在Debian系统安装C++开发环境:先sudoaptupdate更新包列表,再sudoaptinstallbuild-essential安装编译工具链,或单独安装g++。用g++--version验证。可选安装VSCode、GDB、CMake等工具并配置默认编译器版本。

Debian系统C++开发环境配置指南
编程语言 · 2026-07-25

Debian系统C++开发环境配置指南

在Debian系统中,先执行aptupdate更新软件包列表,再安装build-essential元包即可获得GCC、G++、Make和GDB。通过运行g++--version命令验证编译器安装成功。可选安装VisualStudioCode、CLion等编辑器及CMake构建工具,并编写一个简单的HelloWorld程序,使用g++编译运行以验证环境配置正确

通过cpustat工具查看CPU状态的具体方法与详细步骤
编程语言 · 2026-07-25

通过cpustat工具查看CPU状态的具体方法与详细步骤

cpustat是sysstat包中的CPU监控工具,可按固定间隔输出带时间戳的CPU使用率统计。安装后运行cpustat即可实时显示各核心信息,常用指标包括%usr、%sys、%iowait、%steal和%idle,用于定位用户态、内核态或I O瓶颈。高级选项-c可显示单核统计,-m可同时查看内存使用,适合脚本采集和性能分析。