本文
前往“校招VIP”小程序,访问更方便

【校招VIP】如何检测垃圾

csdn 08月04日

转载声明:原文链接:https://blog.csdn.net/qq_26719997/article/details/110438083  

检测垃圾

在堆⾥放着⼏乎所有的java对象实例,在GC执⾏垃圾回收之前,⾸先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会在执⾏垃圾回收,释放掉其所占⽤的内存空间,因此这个过程我们可以称为垃圾标记阶段。
那么在JVM中究竟是如何标记⼀个对象是死亡的呢?简单地说,当⼀个对象已经不再被任何的存活对象继续引⽤是,就可以判断为已经死亡。
判断对象存活⼀般有两种⽅式:引⽤计数算法和可达性分析算法。

引⽤计数算法(Reference Counting)

概述:
引⽤计数算法:通过判断对象的引⽤数ᰁ来决定对象是否可以被回收。
引⽤计数算法是垃圾收集器中的早期策略。在这种⽅法中,堆中的每个对象实例都有⼀个引⽤计数。当⼀个对象被创建时,且将该对象实例分配给⼀个引⽤变ᰁ,该对象实例的引⽤计数设置为 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的垃圾回收器中没有使⽤这类算法。

扩展知识点:
java并没有选择引⽤计数,是因为其存在⼀个基本的难题,也就是很难处理循环引⽤关系。引⽤计数算法,是很多语⾔的资源回收选择,例如python,它更是同时⽀持引⽤计数和垃圾收集机制。

Python如何解决循环引⽤?
⼿动解除:很好理解,就是在合适的时机,解除引⽤关系。
使⽤弱引⽤weakref,weakref是Ptyhon提供的标准库,旨在解决循环引⽤。

可达性分析算法(Rearchability Analysis)

概述:
相对于引⽤计数算法,这⾥的可达性分析是java、c# 选择的。这种类型的垃圾收集通常也叫追踪性垃圾收集(Tracing Garbage Collection)。
可达性分析算法是通过判断对象的引⽤链是否可达来决定对象是否可以被回收。
可达性分析算法是从离散数学中的图论引⼊的,程序把所有的引⽤关系看作⼀张图,通过⼀系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所⾛过的路径称为引⽤链(Reference Chain)。当⼀个对象到 GC Roots 没有任何引⽤链相连(⽤图论的话来说就是从 GCRoots 到这个对象不可达)时,则证明此对象是不可⽤的,如下图所示。

在java中,可作为 GC Root 的对象包括以下⼏种:
1:虚拟机栈(栈帧中的局部变:量表)中引⽤的对象;
各个线程被调⽤的⽅法中使⽤的参数、局部变量等
2:⽅法区中类静态属性引⽤的对象;
java类的引⽤类型静态变量
3:⽅法区中常ᰁ引⽤的对象;
字符串常量池(String Table)⾥的引⽤
4:本地⽅法栈中Native⽅法引⽤的对象;
5:所有被同步锁synchronized持有的对象
6:ava虚拟机内部的引⽤
基本数据类型对应的Class对象,⼀些异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器
7:反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
由于Root采⽤栈⽅式存放变量和指针,所以如果⼀个指针,它保存了堆内存⾥⾯的对象地址,但是⾃⼰⼜不存放在堆内存⾥⾯,那它就是⼀个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状态如下:

对象的finalization机制:
java提供了对象终⽌(finalization)机制来允许开发⼈员提供对象销毁之前的⾃定义处理逻辑。
当垃圾回收器发现没有引⽤指向⼀个对象,即:垃圾回收次对象之前,总会先调⽤这个对象的finalize()⽅法。
finalize()⽅法允许在⼦类中被᯿写,⽤于在对象被回收时进⾏资源释放。通常在这个⽅法张中进⾏⼀些资源释放和清理的⼯作,⽐如关闭⽂件、套接字和数据库连接等。
从功能上来说,finalize()⽅法与C++中的析构函数⽐较类似,但是java采⽤的是基于垃圾回收器的⾃动内存管理机制,所以finalize()⽅法在本质上不同于C++中的析构函数。
永远不要主动调⽤某个对象的finalize()⽅法,应该交给垃圾回收机制调⽤。
理由也包含下⾯三点:
在finalize()时可能会导致对象复活
finalize()⽅法的执⾏时间是没有保障的,它完全由GC线程决定,极端情况下,若不发⽣GC,则finalize()⽅法将没有执⾏机会
⼀个糟糕的finalize()会严᯿影响GC的性能
由于finalize()⽅法的存在,虚拟机中的对象⼀般处于三种可能的状态。
如果从所有的根节点都⽆法访问到某个对象,说明对象已经不再使⽤了。⼀般来说,该对象需要备回收。但事实上,也并⾮是"⾮死不可"的。这时候他们暂时处于"缓刑"阶段。⼀个⽆法触及的对象有可能在某⼀条件下"复活"⾃⼰,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:
可触及的:从根节点开始,可以到达这个对象。
可复活的:对象的所有引⽤都被释放,但是对象有可能在finalize()中复活。
不可触及的:对象的finalize()被调⽤,并且没有复活,那么就会进⼊不可触及的状态。不可触及的对象不可能被复活,因为finalize()只会被调⽤⼀次。
以上3种状态中,是由于finalize()⽅法的存在,进⾏的区分。只有在对象不可触及是才可以被回收。

判定⼀个对象objA是否可回收,⾄少要经历两次标记过程:
1:如果对象objA到GC Roots没有引⽤链,则进⾏第⼀次标记。
2:进⾏筛选,判断此对象是否执⾏finalize()⽅法
如果对象objA没有᯿写finalize()⽅法,或者finalize()已经被虚拟机调⽤过,则虚拟机视为"没有必要执⾏",objA被判定是不可触及的。
如果对象objA᯿写了finalize()⽅法,且还未执⾏过,那么objA会被插⼊到F-Queue队列中,有⼀个虚拟机⾃动创建的、低优先级Finalizer线程触发器finalize()⽅法执⾏。
) finalize()⽅法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进⾏第⼆次标记。如果objA在finalize()⽅法中与引⽤链上的任何⼀个对象建⽴了联系,那么在第⼆次标记是,objA会被移除"即将回收"集合。之后,对象会再次出现没有引⽤存在的情况。在这个情况下,finalize⽅法不会被再次调⽤,对象会直接变成不可触及的状态,也就是说,⼀个对象的finalize()⽅法只会被调⽤⼀次。

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查看GC Roots
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("结束");
}
}

暂无回复