在堆⾥放着⼏乎所有的java对象实例,在GC执⾏垃圾回收之前,⾸先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会在执⾏垃圾回收,释放掉其所占⽤的内存空间,因此这个过程我们可以称为垃圾标记阶段。
那么在JVM中究竟是如何标记⼀个对象是死亡的呢?简单地说,当⼀个对象已经不再被任何的存活对象继续引⽤是,就可以判断为已经死亡。
判断对象存活⼀般有两种⽅式:引⽤计数算法和可达性分析算法。
引⽤计数算法:通过判断对象的引⽤数ᰁ来决定对象是否可以被回收。
引⽤计数算法是垃圾收集器中的早期策略。在这种⽅法中,堆中的每个对象实例都有⼀个引⽤计数。当⼀个对象被创建时,且将该对象实例分配给⼀个引⽤变ᰁ,该对象实例的引⽤计数设置为 1。当任何其它变ᰁ被赋值为这个对象的引⽤时,对象实例的引⽤计数加 1(a = b,则b引⽤的对象实例的计数器加 1),但当⼀个对象实例的某个引⽤超过了⽣命周期或者被设置为⼀个新值时,对象实例的引⽤计数减 1。特别地,当⼀个对象实例被垃圾收集时,它引⽤的任何对象实例的引⽤计数器均减 1。任何引⽤计数为0的对象实例可以被当作垃圾收集。
引⽤计数收集器可以很快的执⾏,并且交织在程序运⾏中,对需要不被⻓时间打断的实时环境⽐较有利,但其很难解决对象之间相互循环引⽤的问题。如下⾯示例所示,对象objA和objB之间的引⽤计数永远不可能为 0,那么这两个对象就永远不能被回收。
/**
* -Xms10m -Xmx10m -XX:+PrintGCDetails
* 证明java使⽤的不是引用计数器算法
*/
public class ReferenceCountGC {
public Object instance = null;
private byte[] bigObject = new byte[1024*1024];
public static void main(String[] args){
ReferenceCountGC objA = new ReferenceCountGC ();
ReferenceCountGC objB = new ReferenceCountGC ();
// 对象之间相互循环引用,对象objA和objB之间的引用计数永远不可能为 0
objB.instance = objA;
objA.instance = objB;
objA = null;
objB = null;
System.gc(); //通过注释,打开或关闭垃圾回收的执行
}
}
上述代码最后⾯两句将objA和objB赋值为null,也就是说objA和objB指向的对象已经不可能再被访问,但是由于它们互相引⽤对⽅,导致它们的引⽤计数器都不为 0,那么垃圾收集器就永远不会回收它们。
相对于引⽤计数算法,这⾥的可达性分析是java、c# 选择的。这种类型的垃圾收集通常也叫追踪性垃圾收集(Tracing Garbage Collection)。
可达性分析算法是通过判断对象的引⽤链是否可达来决定对象是否可以被回收。
可达性分析算法是从离散数学中的图论引⼊的,程序把所有的引⽤关系看作⼀张图,通过⼀系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所⾛过的路径称为引⽤链(Reference Chain)。当⼀个对象到 GC Roots 没有任何引⽤链相连(⽤图论的话来说就是从 GCRoots 到这个对象不可达)时,则证明此对象是不可⽤的,如下图所示。
在java中,可作为 GC Root 的对象包括以下⼏种:
class RearchabilityTest {
// 静态变量
private static A a = new A();
// 常量
public static final String CONTANT = "I am a string";
public static void main(String[] args) {
// 局部变量
A innerA = new A();
}
}
class A {
}
⾸先,类加载器加载RearchabilityTest类,会初始化静态变量a,将常ᰁ引⽤指向常量池中的字符串,完成RearchabilityTest类的加载; 然后main⽅法执⾏,main⽅法会⼊虚拟机⽅法栈,执⾏main⽅法会在堆中创建A的对象,并赋值给局部变量innerA。此时GC Roots状态如下:
java提供了对象终⽌(finalization)机制来允许开发⼈员提供对象销毁之前的⾃定义处理逻辑。
当垃圾回收器发现没有引⽤指向⼀个对象,即:垃圾回收次对象之前,总会先调⽤这个对象的finalize()⽅法。
finalize()⽅法允许在⼦类中被᯿写,⽤于在对象被回收时进⾏资源释放。通常在这个⽅法张中进⾏⼀些资源释放和清理的⼯作,⽐如关闭⽂件、套接字和数据库连接等。
从功能上来说,finalize()⽅法与C++中的析构函数⽐较类似,但是java采⽤的是基于垃圾回收器的⾃动内存管理机制,所以finalize()⽅法在本质上不同于C++中的析构函数。
永远不要主动调⽤某个对象的finalize()⽅法,应该交给垃圾回收机制调⽤。
理由也包含下⾯三点:
由于finalize()⽅法的存在,虚拟机中的对象⼀般处于三种可能的状态。
如果从所有的根节点都⽆法访问到某个对象,说明对象已经不再使⽤了。⼀般来说,该对象需要备回收。但事实上,也并⾮是"⾮死不可"的。这时候他们暂时处于"缓刑"阶段。⼀个⽆法触及的对象有可能在某⼀条件下"复活"⾃⼰,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:
以上3种状态中,是由于finalize()⽅法的存在,进⾏的区分。只有在对象不可触及是才可以被回收。
判定⼀个对象objA是否可回收,⾄少要经历两次标记过程:
public class CanReliveObj {
public static CanReliveObj obj;
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类的finalize方法");
obj = this;
}
public static void main(String[] args) {
try {
obj = new CanReliveObj();
obj = null;
System.gc();
System.out.println("第1次 gc");
//因为 finalizer线程优先级很低,暂停2s,以等待它
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线程优先级很低,暂停2s,以等待它
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();
}
}
}
MAT是⼀个强⼤的内存分析⼯具,可以快捷、有效地帮助我们找到内存泄露,减少内存消耗分析⼯具。
MAT是Memory Analyzer tool的缩写,是⼀种快速,功能丰富的Java堆分析⼯具,能帮助你查找内存泄漏和减少内存消耗。很多情况下,我们需要处理测试提供的hprof⽂件,分析内存相关问题,那么MAT也绝对是不⼆之选。
MAT安装有两种⽅式,⼀种是以eclipse插件⽅式安装,⼀种是独⽴安装。在MAT的官⽅⽂档中有相应的安装⽂件下载,下载地址为:https://www.eclipse.org/mat/downloads.php
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Scanner;
public class GCRootsTest {
public static void main(String[] args) {
List<Object> numList = new ArrayList<>();
Date birth = new Date();
for(int i=0;i<100;i++){
numList.add(String.valueOf(i));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("数据添加完毕,请下⼀步操作:");
new Scanner(System.in).next();
numList = null;
birth = null;
System.out.println("numList、birth已置空,请下⼀步操作:");
new Scanner(System.in).next();
System.out.println("结束");
}
}