Java虚拟机--如何确定需要回收的垃圾对象
1. 何为对象的引用?
Java中的垃圾回收一般是在Java堆中进行,因为堆中几乎存放了Java中所有的对象实例。在java中,对引用的概念简述如下(引用强度依次减弱) :
- 强引用: 这类引用是Java程序中最普遍的,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用: 用来描述一些非必须的对象,在系统内存不够使用时,这类对象会被垃圾收集器回收,JDK提供了SoftReference类来实现软引用。
- 弱引用: 用来描述一些非必须的对象,只要发生GC,无论但是内存是否够用,这类对象就会被垃圾收集器回收,JDK提供了WeakReference类来实现弱引用。
- 虚引用: 与其他几种引用不同,它不影响对象的生命周期,如果这个对象是虚运用,则就跟没有引用一样,在任何时刻都可能会回收,JDK提供了PhantomReference类来实现虚引用。
示例代码:
import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; /** * @author xxx * @date 2020-07-03 10:11 **/ public class ReferenceDemo { public static void main(String[] args) { // 强引用 Object object = new Object(); Object[] objects = new Object[10]; // 软引用 SoftReference<String> stringSoftReference = new SoftReference<>("sss"); System.out.println(stringSoftReference.get()); System.gc(); // 手动gc,这是内存充足,对象没有被回收 System.out.println(stringSoftReference.get()); // 弱引用 WeakReference<String> stringWeakReference = new WeakReference<>("www"); System.out.println(stringWeakReference.get()); System.gc(); // 手动gc,返回null,对象已经被回收 System.out.println(stringWeakReference.get()); // 虚引用 // 虚引用主要用来跟踪对象被垃圾回收器回收的活动。 // 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。 // 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。 ReferenceQueue<String> stringReferenceQueue = new ReferenceQueue<>(); PhantomReference<String> stringPhantomReference = new PhantomReference<>("ppp", stringReferenceQueue); System.out.println(stringPhantomReference.get()); } }
2. 如何确定需要回收的垃圾对象?
2.1. 引用计数器
每个对象都有一个引用计数器 , 新增一个引用的时候就+1,引用释放的时候就-1,当计数器为0的时候,就表示可以回收。引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个
不错的选择,不过Java语言并没有选择这种算法来进行垃圾回收,主要原因是它很难解决对象之间的相互循环引用问题。
public class LoopReferenceDemo { public static void main(String[] args) { TestA a = new TestA(); //1 TestB b = new TestB(); //2 a.b = b; //3 b.a = a; //4 a = null; //5 b = null; //6 } } class TestA { public TestB b; } class TestB { public TestA a; }
第一行 : TestA的引用计数器加1,TestA的引用数量为1
第二行 : TestB的引用计数器加1,TestB的引用数量为1
第三行 : TestB的引用计数器加1,TestB的引用数量为2
第四行 : TestA的引用计数器加1,TestA的引用数量为2
内存分布如下图:
第五行 : 将a变量设置为null,不再指向堆中的引用,所以TestA的引用计数器减1,TestA的引用数量为1
第六行 : 将b变量设置为null,不再指向堆中的引用,所以TestB的引用计数器减1,TestB的引用数量为1
内存分布如下图:
结论 : 虽然上面程序将局部变量a和b设置为null了,但是在堆中,TestA和TestB还是互相持有对方的引用,引用计数器依然不等于0,这个就称为循环引用,所以说"引用计数器"
会存在这个问题,导致这类对象无法被清理掉。
2.2. 可达性分析
目前主流的虚拟机都采用"可达性分析(GC Roots Tracing)"算法来标记哪些对象是可以被回收的。
该算法是从 GC Roots 开始向下搜索,搜索走过的路径称之为引用链。当一个对象到 GC Roots 没有任何引用链相连时,就代表这个对象是不可用的,称为"不可达对象"。
GC Roots包括:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量(表达式内的变量是临时变量, `for(int i=0){}` i 就是临时变量.)等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
实际上,要真正宣告一个对象死亡,至少要经历两次标记过程 :
如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆
盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置
在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为
一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方
法中让该对象重引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
https://my.oschina.net/wangkang80/blog/1559071