JAVA垃圾回收机制(GC)--标记算法
Posted 数据分析技术整合
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA垃圾回收机制(GC)--标记算法相关的知识,希望对你有一定的参考价值。
1、 概述
Java的堆内存里面几乎存放着所有的对象实例,垃圾收集器在对堆进行回收之前,需要判断哪些对象是存活的,哪些对象已经死去,因此需要规则去判断哪些是可以回收的,哪些是不能回收的。本文涉及两个算法,引用计数器算法和可达性分析算法。
2、引用计数器算法
2.1、概念
引用计数器算法比较简单,通过在对象头中分配一个空间来保存该对象被引用的次数,如果该对象被引用,则其引用次数加一;如果删除该对象的引用,则其引用次数减一,当该对象的引用次数为零时,则该对象会被回收。
String p = new String("abc");
如上,当创建String对象p时,abc对象的引用计数值加1为1.
而当我们去除abc字符串对象的引用时,则abc字符串对象的引用计数减1为0.
p = null;
2.2、实现原理
看如下伪代码:
New(): //分配内存
ref <- allocate()
if ref == null
error "Out of memory"
rc(ref) <- 0 //将ref的引用计数(reference counting)设置为0
return ref
atomic Write(dest, ref) //更新对象的引用
addReference(ref)
deleteReference(dest)
dest <- ref
addReference(ref):
if ref != null
rc(ref) <- rc(ref)+1
deleteReference(ref):
if ref != null
rc(ref) <- rc(ref) -1
if rc(ref) == 0 //如果当前ref的引用计数为0,则表明其将要被回收
for each fld in Pointers(ref)
deleteReference(*fld)
free(ref) //释放ref指向的内存空间
当对象的引用发生变化时,比如说将对象重新赋值给新的变量等,对象的引用计数如何变化。假设我们有两个变量p和q,它们分别指向不同的对象,当我们将他们指向同一个对象时,下面的图展示了p和q变量指向的两个对象的引用计数的变化。
String p = new String("abc")
String q = new String("def")
p = q当我们执行代码p=q时,实际上相当于调用了伪代码中的Write(p,q), 即对p原先指向的对象要进行deleteReference()操作 - 引用计数减一,因为p变量不再指向该对象了,而对q原先指向的对象要进行addReference()操作 - 引用计数加一。
当某个对象的引用计数减为0时,collector需要递归遍历它所指向的所有域,将它所有域所指向的对象的引用计数都减一,然后才能回收当前对象。在递归过程中,引用计数为0的对象也都将被回收,比如说下图中的phone和address指向的对象。
2.3、优缺点
优点
实时:无需等到内存不够时进行回收,运行时根据数据对象的计数器是否为0,为0就可以直接回收;
应用无需挂起:垃圾回收时,应用无需挂起。如果申请内存时,内存不足则会立刻报OOM异常;
区域性:更新对象计数时,只会影响到该对象,不会去扫描全部对象。
缺点
每次对象被引用时,都需要去更新计数器,有时间开销,尤其是在循环时;
无法解决循环依赖的问题
public class Test {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.setB(b);
b.setA(a);
}
}
class A {
private B b;
public B getB() {
return b;
}
public void setB(B b) {
this.b = b;
}
}
class B {
private A a;
public A getA() {
return a;
}
public void setA(A a) {
this.a = a;
}
}
2.4、应用
Python虚拟机、redis、OkHttp、netty等都有使用到这种算法
3、根搜索算法
3.1、概念
以gc roots对象开始,到指定对象是否可达。若gc roots不可达(当一个对象到gc roots没有任何引用链相连),则判定此对象为可回收对象。
根搜索算法判断对象的存活与对象的引用有关,之前有记录过Java的四种引用类型(强引用、软引用、弱引用、虚引用),在实际回收标记中,根搜索算法判断的对象不可达并非直接判定对象”死亡“,这时候的对象处于”缓刑“阶段,真正进行回收至少需要经历至少两次标记阶段,
如果一个对象通过根搜索之后发现没有与GC Roots相连的引用连接,此时会进行第一次标记并且进行筛选,当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,则都不会再执行。此时对象没有自救的机会。
如果对象被判断为有必要执行finalize()方法,此时对象会被放在名为F-Queued的队列中,并在稍后由虚拟机自动创建的低优先级的Finalize线程去执行,这里所谓的执行指的是虚拟机会触发这个方法,但并保证会等待进行结束。
finalize()方法是对象逃脱”死亡“的最后一次机会,且只有一次。稍后GC将会对F-Queued中的对象进行第二次小规模的标记,如果对象想要在finalize()中成功拯救自己,只要重新引用链接上任何一个对象建立链接关系即可,譬如把自己(this关键字)赋值给某个变量或者对象的成员变量,则此对象再第二次标记时将会被移出”即将回收“的集合。
如下代码:
public class FinalizeEscapeGC {
private static FinalizeEscapeGC SAVE_HOOK = null;
private void isAlive() {
System.out.println("Yes,I am still alive!");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize() method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String args[]) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
// 对象第一次成功拯救自己
SAVE_HOOK = null;
// 调用该方法建议系统执行垃圾清理,但也并不一定执行
System.gc();
// 因为Finalize线程优先级较低,暂停0.5秒以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("No, I am dead!");
}
// 下面这段代码与上面完全相同,但这次却自救失败了
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("No, I am dead!");
}
}
}
3.2、GC Roots
3.2.1、概念
可达性算法的原理是以一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点。这样通过 GC Root 串成的一条线就叫引用链),直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为垃圾对象,会被 GC 回收。
如图示,如果用可达性算法即可解决上述循环引用的问题,因为从GC Root 出发没有到达 a,b,所以 a,b 可回收。
3.2.2、两栈两方法
在Java中,哪些可以作为GC Root呢,归结为两栈两方法
两栈
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
两方法
方法区中类静态属性引用的对象
方法区中常量引用的对象
3.2.2.1、虚拟机栈中引用的对象
如下代码所示,a 是栈帧中的本地变量,当 a = null 时,由于此时 a 充当了 GC Root 的作用,a 与原来指向的实例 new Test() 断开了连接,所以对象会被回收。
public class Test {
public static void main(String[] args) {
Test a = new Test();
a = null;
}
}
3.2.2.2、方法区中类静态属性引用的对象
所谓本地方法就是一个 java 调用非 java 代码的接口,该方法并非 Java 实现的,可能由 C 或 Python等其他语言实现的, Java 通过 JNI 来调用本地方法, 而本地方法是以库文件的形式存放的(在 WINDOWS 平台上是 DLL 文件形式,在 UNIX 机器上是 SO 文件形式)。通过调用本地的库文件的内部方法,使 JAVA 可以实现和本地机器的紧密联系,调用系统级的各接口方法,还是不明白?见文末参考,对本地方法定义与使用有详细介绍。
当调用 Java 方法时,虚拟机会创建一个栈桢并压入 Java 栈,而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不会在 Java 栈祯中压入新的祯,虚拟机只是简单地动态连接并直接调用指定的本地方法。
JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {
...
// 缓存String的class
jclass jc = (*env)->FindClass(env, STRING_PATH);
}
如上代码所示,当 java 调用以上本地方法时,jc 会被本地方法栈压入栈中, jc 就是我们说的本地方法栈中 JNI 的对象引用,因此只会在此本地方法执行完成后才会被释放。
3.2.2.3、方法区中类静态属性引用的对象
如下代码所示,当栈帧中的本地变量 a = null 时,由于 a 原来指向的对象与 GC Root (变量 a) 断开了连接,所以 a 原来指向的对象会被回收,而由于我们给 s 赋值了变量的引用,s 在此时是类静态属性引用,充当了 GC Root 的作用,它指向的对象依然存活!
public class Test {
public static Test s;
public static void main(String[] args) {
Test a = new Test();
a.s = new Test();
a = null;
}
}
3.2.2.4、方方法区中常量引用的对象
如下代码所示,常量 s 指向的对象并不会因为 a 指向的对象被回收而回收
public class Test {
public static final Test s = new Test();
public static void main(String[] args) {
Test a = new Test();
a = null;
}
}
以上是关于JAVA垃圾回收机制(GC)--标记算法的主要内容,如果未能解决你的问题,请参考以下文章