使用 一个引用计数器 记录该对象还有多少个引用指针指向该对象,此算法 简单高效 但需要在代码中进行额外的逻辑处理以 防止循环引用 导致 内存泄露 的问题。
让我们来一起看看下面的例子来理解 循环引用 和 内存泄漏 两个概念:
/** * @author Zeng * @date 2020/4/6 11:41 */ public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 占点内存,以便观察清楚GC日志中是否有进行垃圾收集 */ private byte[] bigSize = new byte[2 * _1MB]; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed!"); } public static void testGC(){ ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); //对象内部的instance引用指针相互指向对方实例 objA.instance = objB; objB.instance = objA; //除了instance指针以外没有其它引用指针指向这两个对象 objA = null; objB = null; System.gc(); //执行finalize()方法的速度有些慢,让主线程等待一下它执行 Thread.sleep(500); } public static void main(String[] args) { testGC(); } } 复制代码
在引用计数算法中对于 objA
和 objB
是无法回收的,因为它们内部含有对方实例的引用指针,但是除此之外 没有其它指针引用这两个对象 ,也无法访问到这两个对象, JVM无法回收这两个对象 ,这就导致了 内存泄漏 。
执行结果如下:
可以看到JVM里面并不是采用引用计数算法,因为在显式指定垃圾收集时JVM确实把这两个对象给回收了,这两个对象的 finalize()
方法被调用了,这个方法是当对象 第一次被回收时 被调用的。
那么JVM是如何确定对象是不是一个“垃圾”呢?
通过一系列的“ GC Roots ”根对象作为起始节点,一直往下搜索引用关系,搜索过程所走过的路径称为“ 引用链 ”。
看下面的代码可以构成一条引用关系链,而 objD
因为没有指针引用它而成为了 垃圾
,等待下一次垃圾回收来了结它。
/** * @author Zeng * @date 2020/4/7 7:44 */ public class ReferenceList { private String name; public Object instance = null; public ReferenceList(String name) { this.name = name; } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println(name + " finalize method executed!"); } public static void main(String[] args) throws InterruptedException { ReferenceList objA = new ReferenceList("objA"); ReferenceList objB = new ReferenceList("objB"); ReferenceList objC = new ReferenceList("objC"); objA.instance = objB; objB.instance = objC; objB = null; objC = null; ReferenceList objD = new ReferenceList("objD"); objD = null; System.gc(); Thread.sleep(500); } } 复制代码
上面代码得到的引用关系链如下图所示:
所以objD会被JVM回收,而objA、objB、objC可以存活下来,如下图所示可以验证这个结果:
固定作为GC Roots的对象主要有以下几种:
虚拟机栈中引用的对象,例如 局部变量 、 形式参数 、 临时变量 ······
类静态属性引用的变量,例如 类的静态引用类型成员变量
常量引用的对象,例如 String str = "alive"
, 字符串常量池中的引用 。
同步锁引用的对象,例如 synchronized(obj)
中被锁住的 obj
可作为 GC Roots
强引用(Strongly Reference):即类似于 Object objA = new Object()
这种引用关系,垃圾收集器是永远不会回收掉强引用的对象的。
软引用(Soft Reference):用于表示 还有用,但非必须的对象 ,只有在发生 内存溢出 之前,才会回收这类对象。
弱引用(Weak Reference):被弱引用关联的对象 只能存活到下一次垃圾回收发生 为止,无论内存是否可用,都会回收该类对象。
虚引用(Phantom Reference):最差劲的一种引用关系,无法通过虚引用获取对象实例,设置虚引用的目的就是 为了让它成为垃圾被回收掉 。
下面例子可以证明它们引用回收的时机
import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; /** * @author Zeng * @date 2020/4/7 8:16 * 四种引用类型实践 */ public class Reference { private String name; public Reference(String name) { this.name = name; } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println(name + " finalize method executed!"); } public static void main(String[] args) throws InterruptedException { //强引用 Reference obj = new Reference("强引用obj"); System.gc(); Thread.sleep(500); //软引用 Reference objA = new Reference("软引用objA"); SoftReference<Reference> softReferenceA = new SoftReference<>(objA); objA = null; System.gc(); Thread.sleep(500); //弱引用 Reference objB = new Reference("弱引用objB"); WeakReference<Reference> weakReferenceB = new WeakReference<>(objB); objB = null; System.gc(); Thread.sleep(500); //虚引用 Reference objC = new Reference("虚引用objC"); ReferenceQueue<Reference> phantomQueue = new ReferenceQueue<>(); PhantomReference<Reference> phantomReference = new PhantomReference<>(objC, phantomQueue); objC = null; System.gc(); Thread.sleep(500); } } 复制代码
运行结果:
可以看到弱引用和虚引用在下一轮垃圾回收时都会被当成垃圾给回收掉,而强引用和软引用没有被回收,如果构造出堆溢出的情况,软引用也会被回收。
本文主要讲解了JVM如何确定一个对象是否可以回收,以及介绍 引用计数法和可达性分析 两种方法判断对象是否可回收,最后通过Java的四种引用类型的实践验证了强引用、软引用、弱引用和虚引用的垃圾回收时机,如果有任何错误欢迎提出,乐意与大家交流学习!