标记阶段
引用计数算法(JAVA并未采用)
对于一个对象A,只要有任何一个对象引用A,则A的引用计数器加1,当引用失效时,计数器减一,当计数器为0时,表示该对象可回收。
图一
graph TD
B --> A
C --> A
D --> B
E --> C
图一解
| 对象A | 对象B | 对象C | 对象D | 对象E |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 | 0 |
| 2 | 1 | 1 | 0 | 0 |
GC回收过程
当GC回收对象D时,发现该对象没有被引用所以它引用计数器为0可以被回收,随着对象D被回收,对象B的引用计数器减1,所以B的引用计数器为0,所以对象B也可以被回收,这时对象A的引用计数器为1,是无法被回收的,只有当GC再次回收对象E时,对象C的引用计数减1变为0并且被回收时,对象A的引用计数器再次减1变为0才可以被回收。
图二
graph LR
A --> B
B --> C
C --> A
图二解
| 对象A | 对象B | 对象C |
|---|---|---|
| 1 | 1 | 1 |
GC回收过程
当GC想回收对象A时,发现对象A引用对象B,对象B引用对象C,对象C引用对象A,引用计数器全部为1,所以无法进行回收,这种方式会导致内存泄漏。
代码示例
public class RefCounterTest {
// 创建一个10mb的字节数组
private byte[] bytes = new byte[1024 * 1024 * 10];
private Object obj;
public static void main(String[] args) {
RefCounterTest refCounterTest1 = new RefCounterTest();
RefCounterTest refCounterTest2 = new RefCounterTest();
refCounterTest1.obj = refCounterTest2.obj;
refCounterTest2.obj = refCounterTest1.obj;
refCounterTest1 = null;
refCounterTest2 = null;
// 通过日志可以得出结论:
// Java中没有采用引用计数算法,否则由于在对象相互引用,垃圾回收是不会进行的
System.gc();
}
}
可达性分析算法
概念
- 可达性分析算法是以根对象集合(
GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达 - 内存中的存活对象都会被根对象集合直接或间接连接,搜索的路径称为引用链(
Reference Chain) - 只有能够被根对象集合直接或者间接连接的对象才是存活对象 GC Roots
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
总结一句话
堆周围的一些区域可以被称之为GC Roots
图一
graph TD
object1 --> GCRoots
object2 --> object1
object3 --> object1
object4 --> object1
object6 --> object5
object7 --> object5
图一解
object1、object2、object3、object4全部被GC Roots所引用着所以GC不会对它们进行回收,而object5、object6、object7不在被GC Roots所引用,所以在GC时会对这些对象进行回收。
代码示例
/**
* 通过JProfiler工具查看GCRoots
*/
public class JProFilerTest {
public static void main(String[] args) throws InterruptedException {
JProFilerTest jProFilerTest = new JProFilerTest();
jProFilerTest.gcRoots();
}
public void gcRoots() throws InterruptedException {
System.out.println("JProFilerTest.gcRoots start");
ArrayList<String> data = new ArrayList<>();
Date date = new Date();
for (int i = 0; i < 100; i++) {
data.add("i + " + i);
Thread.sleep(1_000);
}
System.out.println("JProFilerTest.gcRoots end");
}
}
可达性分析算法-对象复活
public class GCReliveTest {
private static GCReliveTest obj;// 类变量属于GC ROOTS
public static void main(String[] args) throws InterruptedException {
GCReliveTest gcReliveTest = new GCReliveTest();
gcReliveTest.test();
}
@Override
protected void finalize() {
System.out.println("GCReliveTest.finalize");
// 对象复活
obj = this;
}
public void test() throws InterruptedException {
obj = new GCReliveTest();
// 第一次
obj = null;
System.gc();
// 由于 Finalizer 线程优先级较低,所以让 main 线程暂停 2000s,让 jvm 虚拟机有时间调用finalize()进行对象复活
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is null");
} else {
System.out.println("obj is not null");
}
// 第二次
obj = null;
System.gc();
// Finalizer 机制只能复活一次,所以在第二次将 obj 对象置为 null 时,已经无法再复活了.所以让 main 线程暂停就没有意义
// Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is null");
} else {
System.out.println("obj is not null");
}
// Thread.sleep(1_000_000);
}
}
对象复活机制的要求
- 对象只可以复活一次
- 对象复活的引用必须是
GT Roots
清除阶段
标记清除算法(Mark-Sweep)
当堆中的有效空间被耗尽时,就会停止整个程序(STW),然后进行两项工作,第一是标记,第二是清除。
- 标记:
Collector从引用根节点开始遍历,标记所有被引用的对象。一般是再对象的Header中记录为可达对象。 - 清除:
Collector对堆内存从头到尾进行线性遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
图一
- 绿色:存活对象
- 紫色:垃圾对象
- 白色:空闲列表
- ->:引用关系 通过标记清除算法可以看出这种算法是比较简单的,但它的劣势是开销较大,因为需要额外的维护一份空间列表,这个是因为内存不规整造成的,这个我们在对象的实例化过程中有提到过关于对象创建的步骤中第二步为对象分配内存
复制算法(Copy)
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中存活对象赋值到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。主要应用于
新生代,存活对象少,垃圾对象多。
复制算法的内存开销较大,因为它需要额外在内存中开辟一块空间,但是它的效率较高,因为它是内存规整的,不需要额外维护一份空闲列表。
标记整理算法(Mark-Compact)
“标记 - 整理”算法的标记过程与“标记 - 清除”算法相同,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
以上三种算法对比
| Mark-Sweep | Mark-Compact | Copy | |
|---|---|---|---|
| 速度 | 中等 | 最慢 | 最快 |
| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要存活对象的2倍大小(不堆积碎片) |
| 移动对象 | 否 | 是 | 是 |
结论:
目前这些算法都属于分代收集算法,在下一篇将介绍分区收集算法`G1`