垃圾回收相关算法
1. 垃圾回收的3个阶段
- 垃圾标记阶段---->标记哪些为垃圾
- 对象终止阶段---->确定垃圾是否清除
- 垃圾清除阶段---->开始清除垃圾
2. 标记阶段 - 对象存活判断
垃圾回收主要针对的是JVM内存结构中,线程可共享的部分:堆和方法区;
频繁回收新生代,较少回收老年代,基本不动方法区;因此GC主要是针对堆内存进行讲解;
2.1 引用计数法 (java没有采用)
循环引用举例
循环引用导致的内存泄漏情况举例:链表节点之间的相互引用。
当p的指针断开的时候,内部的引用形成一个循环,这就是循环引用,从而造成内存泄漏
内存泄漏就是垃圾无法被回收;
注意: 这个内存泄漏的例子是针对引用计数算法的,java中并没有采用这个算法,因此实际中,不会出现这种情况;举此例子要说明其场景是在引用计数算法下的相互引用的情况。
证明:java使用的不是引用计数算法
package GC;
/**
* -XX:+PrintGCDetails
* 证明:java使用的不是引用计数算法
*
* @author shkstart
* @create 2020 下午 2:38
*/
public class RefCountGC {
//这个成员属性唯一的作用就是占用一点内存
private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB
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;
//显式的执行垃圾回收行为,这里发生GC,obj1和obj2能否被回收?
// System.gc();
}
}
- JVM堆内存中,存在两个RefCountGC对象,如果按照计数器法,那么这两个对象的计数为2,因此即使把obj1 .reference和obj2.reference(栈引用)置null。 则在Java堆当中的两块内存依然保持着互相引用,无法回收。
- 但实际情况是,把obj1.reference和obj2.reference置为null之后,如果显示的去调用System.gc(),这里将会发生GC,JVM回收了堆中oj1和obj2实体。从而证明Java没有采用引用计数算法.
引用计数算法小结
-
引用计数算法,是很多语言的资源回收选择,例如因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制。
-
具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试。
-
Java并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。Python如何解决循环引用?
手动解除: 很好理解,就是在合适的时机,解除引用关系。
使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环引用。
2.2 可达性分析算法
- 别称: 根搜索算法、追踪性垃圾收集
可达性分析算法思路
可达性分析算法是以 根对象集合(GCRoots) 为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
哪些是GC Roots?
可达性分析算法的注意事项
一致性就是对象的引用关系在GC Roots标记前后不发生改变,一旦改变了就不准确了;
stop the world 的时候会导致用户线程停顿;
3. 对象终止阶段-对象的 finalization 机制
3.1 finalize() 方法机制
-
对象销毁前的回调函数:finalize()
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。 -
finalize() 允许开发人员提供对象被销毁之前的自定义处理逻辑。
-
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
Object 类中 finalize() 源码
// 等待被重写
protected void finalize() throws Throwable {}
- finanlize()方法不是手动调用的,而是由垃圾回收器调用的
3.2 finalize() 方法使用的注意事项
1. 永远不要主动调用某个对象的finalize()方法应该交给垃圾回收机制调用。
- 如果你不重写finalize(),那么这个方法压根就不会执行,因为本身就没有具体实现。
- 如果你重写了finalize(),也必须在可达性分析后此对象成被标记成了垃圾,并且得等到发生GC的时候,finalize()会进入一个优先级低的Finalizer线程中等待自动执行,不需要人为主动的去调用。
2.finalze()方法的特点
(1)执行finalize() 可能会导致对象复活。
(2)finalize()方法的执行时间是没有保障的,它完全由Gc线程决定。极端情况下,若不发生GC,则finalize()方法将没有执行机会。
(3)一个糟糕的finalize()会严重影响GC的性能。
(4)如果重写了finalize()方法,那么在垃圾对象被GC之前,总会先调用这个对象的finalize()方法。
从功能上来说,finalize()方法与C++ 中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize()方法在本质,上不同于C++ 中的析构函数。
3.3 对象是否"死亡"
一个经过可达性分析后,判定引用链断开的对象讲道理是需要被GC回收内存的,但是由于finalize()方法存在,此方法在对象被GC回收之前被调用,这就会导致此对象可能没有被回收。因此,由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态:
(1)可触及的:从GC Roots 引用链可以达到该对象。
(2)可复活的:GC Roots不可及,已经被标记为垃圾了,但是对象有可能在finalize()中复活。
(3)不可触及的:对象的finalize()被调用了,并且没有复活,那么就会进入不可触及状态。
finalize()只能被调用一次
-
一个对象在被回收前,即使被finalize()救活了,那么他就不能再次调用finalize()了;
-
不可触及的对象不可能被复活,因为finalize()只会被调用1次,在回收前就已经调用过了
以上3种状态中,只有在对象不可触及时才可以被回收。
3.4 判断对象是否可回收的两次标记过程
第一次标记:发现对象没有到GC Roots的引用链时
第二次标记:调用对象的finalize方法时
通过 JVisual VM 查看 Finalizer 线程:
3.5 代码测试对象可复活
package GC;
/**
* 测试Object类中finalize()方法,即对象的finalization机制。
*
* @author shkstart
* @create 2020 下午 2:57
*/
public class CanReliveObj {
public static CanReliveObj obj;//类变量,属于 GC Root
//此方法只能被调用一次
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类重写的finalize()方法");
obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系,复活了
}
public static void main(String[] args) {
try {
obj = new CanReliveObj();
// 对象第一次成功拯救自己
obj = null;
System.gc();//调用垃圾回收器
System.out.println("第1次 gc");
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("第2次 gc");
// 下面这段代码与上面的完全相同,但是这次自救却失败了
obj = null;
System.gc();
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
第1次 gc
调用当前类重写的finalize()方法
obj is still alive
第2次 gc
obj is dead
4.使用(MAT与JProfiler)工具分析GCRoots
5.垃圾清除阶段
-
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
-
目前在JVM中比较常见的三种垃圾收集算法是标记一清除算法( Mark一Sweep)、复制算法(Copying)、标记一压缩算法(Mark一Compact)。
5.1 标记-清除算法
注意:标记的是可达对象!非垃圾!
5.2 复制算法
5.3 标记-压缩(整理,Mark-Compact)算法
5.4 三种垃圾回收算法对比
6. 分代收集策略