《JVM第8课》垃圾回收算法
为什么要进行垃圾回收?
垃圾是指 JVM 中没有任何引用指向它的对象,如果不及时清理这些垃圾对象,那么它就会一直占用内存,如果垃圾对象越来越多,就会出现OOM了。
要判断对象是否是垃圾对象有两种方式,一、引用计数法。二、可达性分析法。
而要清除垃圾对象有三种常用方式,一、标记-清除算法。二、复制算法。三、标记-整理算法。
1.标记算法
要清除垃圾首先要找到垃圾,JVM是通过什么算法找到堆中的垃圾对象的呢?
1.1 引用计数法
每个对象都保存一个引用计数器属性,用于记录对象被引用的次数。如果计数器为 0 则表示是垃圾对象。
优点:原理简单,计数器为 0 则表示是垃圾对象。
缺点:
- 需要额外的空间和时间来维护引用计数。
- (严重) 无法处理循环引用的问题。
循环引用就是A对象里有个B对象的属性,B对象里有个A对象的属性,这样的话A和B的引用计数器都会等于1,永远不会被回收。所以引用计数法很少使用!!!
1.2 可达性分析法
可达性分析法会以GC Roots作为起始点,然后一层一层的去寻找引用的对象,被找到的对象就是存活对象,无法到达的对象就是垃圾对象。
GC Roots是一组引用,基本常用的包括:
- 线程的虚拟机栈的栈帧中的方法参数、局部变量所对应的对象引用
- 线程的本地方法栈的栈帧中的方法参数、局部变量所对应的对象引用
- 方法区中保存的类信息中静态属性、常量属性所对应的对象引用
2.回收算法
2.1 标记-清除算法(Mark-Sweep)
既然我们已经能够找到哪些是垃圾对象,那么我们该怎么去回收这些垃圾对象呢?这里又涉及到一些算法,首先比较简单的就是标记-清除算法。
标记-清除算法是一种非常基础的垃圾回收算法,针对某块内存空间,比如新生代、老年代,如果可用内存不足后,就会STW(暂停用户线程的执行),然后执行算法进行垃圾回收:
- 标记阶段:从 GC Roots 开始遍历,找到可达对象,并在对象头中进行记录。
- 清除阶段:堆内存空间进行线性遍历,如果发现对象头中没有记录是可达对象,则回收它。
缺点:
- 效率不高,和后面讲的算法来比效率确实不高。
- 内存碎片,对象被清除后会留下一块一块的小内存,由于这些小内存不是连续的,所以很可能不够存储新来的大对象。
优点:
- 思路简单。
2.2 复制算法(Coping)
复制算法的思想就是空间换时间。它将内存空间分为两块,每次只使用一块,在进行垃圾回收时,会首先STW,然后遍历将可达对象复制到另外没有被使用的内存块中,然后再将当前内存块标记为可重用,后续再按相同的流程进行垃圾回收,两块内存交换着来。这也是为什么新生代中会有 S1 和 S2 两块内存区域的原因。
复制算法只遍历一次,在遍历的同时就把可达对象复制到另一块内存中,而标记-清除算法需要遍历两次,标记和清除各一次。
优点:
- 只遍历一次,效率高。
- 不会出现内存碎片。
缺点:
- 需要更多的内存,始终有一半的空闲内存。
- 对象复制后,对象的内存地址发生了变化,需要额外的时间修改栈帧中记录的引用地址。
- 如果可达对象比较多,垃圾对象比较少,那么复制算法的效率就会比较低,所以新生代特别适合复制算法,因为新生代的垃圾对象比较多(但也不绝对)。
2.3 标记-整理算法(Mark-Compact)
第一阶段和标记-清除算法一致,也是标记可达对象,也会STW。
第二阶段将所有存活对象移动到内存的一端。
最后清理边界外的所有空间
优点:
- 不会出现内存碎片。
- 也不需要利用额外的内存空间。
缺点:
- 效率要低于标记清除算法、复制算法。
- 也需要修改栈帧中的引用地址。
3.三种垃圾回收算法的对比
标记-清除 | 标记-整理 | 复制 | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(有碎片) | 少(无碎片) | 最多 |
移动对象 | 否 | 是 | 是 |