---
title: "Java的垃圾回收算法"
source: https://www.boris1993.com/java-gc-algorithms.html
date: 2019-03-28
updated: 2026-10-10
tags: [Java, 垃圾回收, GC]
categories: [学知识]
---

# Java的垃圾回收算法

在[JVM运行时的数据区域](https://www.boris1993.com/projects/java/fundamentals/java-jvm-runtime-data-areas.html)中了解到了JVM的内存模型，那么既然使用了内存，就要考虑如何回收内存。与C语言不同，Java不需要开发人员人工回收内存，而是交给Java的垃圾回收机制来完成。

## 哪些内存需要回收

在Java中，GC的对象是堆和方法区。栈中的栈帧随着方法的调用和退出，会自行完成压栈和出栈操作，每个栈帧所需的内存空间也是在类结构确定下来时就已知的，所以不怎么需要考虑内存的回收问题。但是堆和方法区则不一样，这部分的空间是动态分配和回收的，同时也只有在运行时才可得知要生成哪些对象以及需要多少空间。

判断对象是否可以被回收通常有两种算法：`引用计数法`和`可达性分析法`。

### 引用计数法

引用计数法会给每个对象添加一个引用计数器，每当有一个地方引用它时，计数器就会加一；反之，每当一个引用失效时，计数器就会减一。任何时候，如果引用计数为0，则说明这个对象可以被回收。

![Reference counter](https://blog-static.boris1993.com/java-gc-algorithms/reference-counter.png)

但是，这个算法有一个问题，那就是无法处理循环引用，即这样：

![Circular reference](https://blog-static.boris1993.com/java-gc-algorithms/circular-reference.png)

此时，`对象1`，`对象2`，`对象3`都是不可达状态，理论上这三个对象都应该被回收，但是因为它们三个形成循环引用，引用计数器不为零，导致GC不会回收它们的空间。所以实际上，JVM并没有采用这种判断方法。

### 可达性分析法(根搜索算法)

可达性分析法的原理是，从根对象(GC Root)开始向下搜索，搜索走过的路径称为“引用链”，对象与引用链可以形成一个图，当任一个对象没有到根对象的引用链，即在这个图中该对象是不可达的，那么就判定这个对象是可以被回收的。

Java语言使用如下几种GC Root对象：

> 1.  虚拟机栈(栈帧中的本地变量表)中引用的对象
> 2.  方法区中静态属性引用的对象
> 3.  方法区中常量引用的对象
> 4.  本地方法栈中JNI引用的对象

还是用上面这个循环引用作为示例：

![Circular reference](https://blog-static.boris1993.com/java-gc-algorithms/circular-reference.png)

此时虽然`对象1`、`对象2`、`对象3`互相之间存在引用，但是从根对象开始无法找到到达它们的路径，即它们三个都是`不可达`的，也就是可以被回收的。

## 如何进行回收

在根搜索算法的基础上，现代虚拟机中实现了三种算法：`标记-清除算法`，`复制算法`，`标记-整理算法`。

### 标记-清除算法

标记-清除算法把垃圾回收过程分成标记和清除两个阶段。在标记阶段，通过根节点标记所有可达的对象，也就是说，未被标记的对象都是不可达的对象。然后在清除阶段回收所有未被标记的对象。详细来说的话，就是当堆中的有效内存空间被耗尽时，就会停止整个程序(stop the world)，然后逐步开始标记和清除工作。

标记的过程，实际上是遍历所有的GC Roots，并标记所有可达的对象。而清除的过程，则是遍历堆中所有的对象，并清除没有被标记的对象。

在回收过程中一定要停止程序运行的原因，是为了避免在标记完成而尚未开始清除时，有新的可达的对象被创建出来。一旦出现这种情况，因为新创建的对象没有被标记，所以在清除阶段这个对象又会被清除。如果停止了程序的运行，那么在清除过程中，对象的状态不会发生变化，也就不会发生前面说的这种问题。

这个算法尽管可以有效的回收内存，但是也有两个比较大的缺点：

1.  遍历所有对象的效率比较低，导致程序停止运行的时间比较长
2.  这种方法清理出的内存空间是不连续的，会造成空闲空间碎片化，并会影响数组分配空间。同时为了得知哪些空间是可用的，JVM还需要额外维护内存闲置空间的信息。

### 复制算法

复制算法的思想，是将原有的内存空间分成两部分，每次只使用其中一部分。在垃圾回收时，会从正在使用的部分中，将标记的对象复制到另一块内存中，然后清除正在使用的内存块，并交换两块内存的角色，来完成空间的回收。

该算法比标记-清除算法的效率高，但是该算法不适合活动对象较多的场合，比如老年代空间。此外，该算法会造成一定程度的内存空间浪费，因为总是有一片内存空间是被闲置的。为了节省空间，考虑到新生代空间中的对象存活时间大多不会很长，所以虚拟机可以选择不将内存对半分，而是将内存分割成一块比较大的Eden空间和两块比较小的Survivor空间(`From Survivor`和`To Survivor`)，每次同时使用Eden和其中一个Survivor。比如HotSpot虚拟机默认为Eden分配80%的空间，为两个Survivor各分配10%的空间。

Eden区，如其名字“伊甸园”一般，对象在被创建时，首先会放在这个区域；Survivor区，也如其名字“幸存者区”一样，存放的是每次垃圾回收后被保留下来的对象。

在每次垃圾回收时，Eden区中不能被回收的对象，和From Survivor区中不能被回收的对象，都将被复制到To Survivor区中，然后回收Eden区和From Survivor区的空间，并且幸存下来的对象的age属性会加一，最后From Survivor和To Survivor两者的角色对调。如果发生Survivor空间不足以存放所有活动对象时，则会使用老年代来进行分配担保，大的对象会跳过Survivor区直接进入老年代。

### 标记-整理算法

因为复制算法在活动对象较多时，会发生很多的复制操作，导致算法效率比较低，而老年代的特点就是活动的对象比较多。“标记-整理”算法就是为了应对这一情况而诞生的。

标记-整理算法把垃圾回收过程分成标记和整理两个阶段。标记阶段的做法与“标记-清除”算法一样，遍历所有的GC Roots并标记出活动的对象；而在整理阶段，所有的活动对象都会向内存空间的一端移动(比如全部从内存空间的其实位置开始排列)，然后将边界以外的内存直接清理。

该算法的另一个优点是，因为该算法不会分割内存空间，而且每次回收后对象占用的空间肯定小于回收前所占用的空间，所以不再需要额外的空间进行分配担保。

### 分代收集算法

分代收集算法实际上就是根据不同内存空间的特性，一般是将堆分为新生代和老年代，并根据其各自的特点，在新生代使用复制算法回收，在老年代使用标记-整理算法回收。
