垃圾回收相关算法
- 在堆里存放着几乎所有的Java对象实例,在
GC执行垃圾回收
之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
- 那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
- 判断对象存活一般有两种方式:
引用计
数算法
和可达性分析算法。
1. 标记阶段—引用计数算法
- 引用计数算法(Reference Counting)比较简单,对每个对象保存一个
整型的引用计数器属性。用于记录对象被引用的情况。
- 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。
只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
- 优点:
实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
- 缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,
导致在Java的垃圾回收器中没有使用这类算法。
1.1 循环引用
当p的指针断开的时候,内部的引用形成一个循环,这就是循环引用,从而造成内存泄漏
;
1.2 循环引用举例
我们使用一个案例来测试Java中是否采用的是引用计数算法
/**
* 引用计数算法测试
* -XX:+PrintGCDetails
*/
public class RefCountGC {
// 这个成员属性的唯一作用就是占用一点内存
private byte[] bigSize = new byte[5*1024*1024];
// 引用
Object reference = null;
public static void main(String[] args) {
RefCountGC obj1 = new RefCountGC();
RefCountGC obj2 = new RefCountGC();
obj1.reference = obj2;
obj2.reference = obj1;
obj1 = null;
obj2 = null;
// 显示的执行垃圾收集行为,判断obj1 和 obj2是否被回收?
System.gc();
}
}
运行结果:
[GC (System.gc()) [PSYoungGen: 14148K->712K(75776K)] 14148K->720K(249344K), 0.0023576 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 712K->0K(75776K)] [ParOldGen: 8K->639K(173568K)] 720K->639K(249344K), [Metaspace: 3437K->3437K(1056768K)], 0.0060505 secs] [Times: user=0.03 sys=0.02, real=0.01 secs]
Heap
PSYoungGen total 75776K, used 650K [0x000000076b780000, 0x0000000770c00000, 0x00000007c0000000)
eden space 65024K, 1% used [0x000000076b780000,0x000000076b822a68,0x000000076f700000)
from space 10752K, 0% used [0x000000076f700000,0x000000076f700000,0x0000000770180000)
to space 10752K, 0% used [0x0000000770180000,0x0000000770180000,0x0000000770c00000)
ParOldGen total 173568K, used 639K [0x00000006c2600000, 0x00000006ccf80000, 0x000000076b780000)
object space 173568K, 0% used [0x00000006c2600000,0x00000006c269fce8,0x00000006ccf80000)
Metaspace used 3444K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
我们能够看到,
上述进行了GC收集的行为
,将上述的新生代中的两个对象都进行回收了
PSYoungGen: 15490K->808K(76288K)] 15490K->816K(251392K)
如果使用引用计数算法,那么这两个对象将会无法回收。而现在两个对象被回收了,说明Java使用的不是引用计数算法来进行标记的。
1.3 小结
引用计数算法,是很多语言的资源回收选择,例如因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制。
具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,
以提高吞吐量的尝试。
Java并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。
Python如何解决循环引用?
- 手动解除:很好理解,就是在合适的时机,解除引用关系。
使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环引用。
2. 标记阶段—可达性分析算法
可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集。
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
相较于引用计数算法,
这里的可达性分析就是Java、C#选择的
。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)。
所谓"GC Roots”根集合就是一组必须活跃的引用。
基本思路:
- 可达性分析算法是以==根对象集合(GCRoots)==为起始点,
按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)。
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。