JVM 垃圾回收机制

一、什么是 JVM 垃圾回收?

首先,我们思考一个问题:

在 Java 中,我们创建对象时,通常不需要手动释放内存。

例如:

java 复制代码
public class Main {
    public static void main(String[] args) {
        User user = new User();

        user = new User();

        user = null;
    }
}

这里发生了什么?

  1. new User():创建第一个 User 对象。

  2. user = new User():创建第二个 User 对象,第一个对象不再被变量 user 引用。

  3. user = null:第二个对象也不再被 user 引用。

那么问题来了:

这两个 User 对象占用的内存什么时候释放?

这就是 JVM 垃圾回收机制要解决的问题。

简单来说:

JVM 垃圾回收(GC)就是自动识别不再使用的对象,并回收它们占用的内存空间。

它有两个核心任务:

  • 找出哪些对象已经没有用了。

  • 回收这些对象占用的内存。

二、JVM 如何判断一个对象是不是垃圾

这是 GC 最核心的问题。

JVM 主要涉及两种判断思路:

1. 引用计数法

原理非常简单:

给每个对象维护一个引用计数器。

每当有一个引用指向该对象,计数器就加 1。

每当有一个引用失效,计数器就减 1。

当计数器为 0 时,就认为对象可以被回收。

例如:

java 复制代码
User user1 = new User();

User user2 = user1;

此时:

user1 和 user2 两个变量都指向同一个 Use r对象

两个变量都指向同一个对象,因此引用计数为 2。

但是引用计数法有一个严重问题:无法解决循环引用问题。

例如:

java 复制代码
class A {
    B b;
}

class B {
    A a;
}

public class Main {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();

        a.b = b;
        b.a = a;

        a = null;
        b = null;
    }
}

此时:

  • A 对象引用着 B 对象。

  • B 对象引用着 A 对象。

  • 外部已经没有任何变量引用它们。

但是两个对象的引用计数都不为 0。

所以,引用计数法无法判断它们是否已经成为垃圾。

2. 可达性分析算法

这是 HotSpot JVM 判断对象是否存活的核心方法。

它的基本思想是:

从一组被称为 GC Roots 的根对象出发,向下搜索所有能够到达的对象。

能够被访问到的对象,说明仍然可能被使用。

无法被访问到的对象,就可以被判定为不可达对象。

看下面这个示意图:

常见的 GC Roots 有哪些?

1、虚拟机栈中引用的对象(例如局部变量引用的对象)。

2、方法区中类静态属性引用的对象。

3、方法区中常量引用的对象。

4、本地方法栈中 JNI 引用的对象。

5、JVM 内部的一些引用对象,例如活动线程、系统类加载器等。

注意:对象不可达不代表一定会立即被回收,还需要考虑对象的终结处理等特殊情况。

三、JVM 垃圾回收主要发生在哪里?

理解 GC,必须先了解JVM 堆内存结构。

Java 对象主要分配在堆内存中,而堆通常按照对象生命周期特点进行分代管理。

传统分代模型主要分为:

|----------------------|-----------|
| 区域 | 作用 |
| 新生代 Young Generation | 存放新创建的对象 |
| 老年代 Old Generation | 存放长期存活的对象 |

新生代又可以分成:

  • Eden 区:大多数新对象首先分配在这里。

  • Survivor 0(S0):存放经过 GC 后存活的对象。

  • Survivor 1(S1):与 S0 交替使用。

为什么要这样设计?

因为 JVM 发现:

大多数 Java 对象都是朝生夕死的。

例如:

java 复制代码
public void test() {
    for (int i = 0; i < 10000; i++) {
        User user = new User();
    }
}

循环创建的很多对象,在使用结束后就没有引用了。

因此,把新生代和老年代分开,可以针对不同生命周期的对象采用不同的回收策略。

四、JVM 垃圾回收有哪些算法?

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

分为两个步骤:

  1. 标记:找到所有需要保留的对象。

  2. 清除:回收没有被标记的对象。

优点:

  • 实现简单。

  • 不需要移动存活对象。

缺点:

  • 会产生大量内存碎片。

  • 分配大对象时可能找不到连续的内存空间。

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

核心思想:

将内存划分为两块,每次只使用其中一块。

当发生 GC 时:

  1. 找出存活对象。

  2. 将存活对象复制到另一块内存。

  3. 清空原来的内存区域。

优点:

  • 不会产生内存碎片。

  • 回收过程相对简单。

缺点:

  • 需要额外的内存空间。

  • 存活对象较多时,复制成本较高。

因此,这种算法很适合新生代,因为新生代对象通常存活率比较低。

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

核心思想:

  1. 标记所有存活对象。

  2. 将存活对象向内存一端移动。

  3. 清理边界之外的内存。

优点:

  • 不会产生内存碎片。

  • 不需要额外保留一整块复制空间。

缺点:

  • 需要移动对象。

  • 整理过程有一定性能开销。

这种算法常用于老年代的垃圾回收实现中。

三种算法对比

算法 优点 缺点
标记-清除 简单 内存碎片
标记-复制 无碎片,效率较高 需要额外空间
标记-整理 无碎片 移动对象开销

五、JVM 垃圾回收器有哪些?

面讲的是 GC 算法,现在讲真正执行垃圾回收工作的组件:垃圾回收器。

常见的有:

垃圾回收器 特点
Serial 单线程,简单
Parallel Scavenge 注重吞吐量
ParNew 新生代并行回收器
CMS 注重低停顿,采用并发标记清除
G1 面向服务端应用,兼顾吞吐量和停顿时间
ZGC 面向低延迟,支持并发回收

其中有三个尤其值得学习:

1. CMS

CMS 的主要特点是尽可能减少垃圾回收时应用线程的停顿时间。

主要阶段:

  • 初始标记

  • 并发标记

  • 重新标记

  • 并发清除

2. G1

G1 将堆划分为多个大小相同的 Region,不再简单地按照连续空间划分新生代和老年代。

它可以优先回收垃圾最多、回收收益较高的 Region。

3. ZGC

ZGC 是一款低延迟垃圾回收器,主要通过并发处理和着色指针等技术减少停顿时间。

六、用一个实际案例理解 GC

假设我们运行下面的代码:

java 复制代码
public class Main {
    public static void main(String[] args) {

        User user1 = new User();
        User user2 = new User();
        User user3 = new User();

        user1 = null;
        user2 = null;

        System.gc();
    }
}

假设三个对象都成功分配在堆中。

执行过程:

注意:System.gc() 只是向 JVM 提出执行 GC 的建议,并不保证立即执行垃圾回收。

相关推荐
揣一尾游鱼yu2 小时前
Spring上(IOC)笔记
java·笔记·spring·ioc
荣码2 小时前
长上下文与大文档处理:200页PDF怎么喂给AI,我踩了4个坑
java·python
极限挑战哥3 小时前
共享电单车联营平台分账架构实践:二清合规与自动分账零人工方案
java·大数据·人工智能·微信小程序·架构·微信公众平台
皮皮林5513 小时前
比 VMware 更快,更轻的虚拟机,横空出世!
java
Thomas.Sir3 小时前
第13课:MyBatis-Plus整合SpringBoot4.x,分页、条件查询、逻辑删除实战
java·spring boot
meiying88384 小时前
多快递公司快递单号批量查询接口怎么对接?Python 聚合查询实现
java·大数据·python
古法安卓4 小时前
Android-GPU压测工具
android·java·android studio
SL_staff5 小时前
10人团队如何用轻量私有化文档平台实现知识复用提效?技术视角拆解B/S架构下的协作闭环
java·开源·全栈
释厄6235 小时前
同性线套娃律——圆面积必然=1+2+...+n 的求和·圆心不与 π 相关
java·人工智能·算法·1024程序员节