前言:被判定为垃圾的对象或者内存区域会被垃圾收集器回收。那么什么样的对象或者内存区域会被判定为垃圾呢?下面就要说起经常作为垃圾判定依据的可达性分析算法与引用计数法了。这两种算法,都是经常被用作垃圾判定的算法,下面说下这两种算法。
一.引用计数法
为对象添加一个引用计数器,当有一个对象引用了该对象时,引用计数器就加一,当引用失效时引用计数器就减一,当引用计数器的值为零时,就说明该对象变成了垃圾。
二.可达性分析算法
从一系列被称为“GC Roots”的节点集根据引用关系向下搜索,搜索过程所走过的路径被称为引用链(Reference Chain),如果某个对象到GC Roots之间没有任何引用链,或者用图论的方式来说GC Roots到这个对象不可达时,则证明该对象是不可达的,但此时并不能判定该对象是垃圾,只能说该对象不可达。判定一个对象为垃圾,要经过以下步骤:
如上图①:第一步使用可达性分析算法判断GC Roots到对象之间是否还有引用链,有则存活,没有引用链则对对象进行第一次标记。
如上图②:第二步判断被标记的对象是否有重写finalize()方法,若该方法已经重写过且被执行过或者未重写该方法则对象判断为死亡,否则将对象放入F-Queue队列中,稍后JVM会启动一个低调度优先级的Finalize线程去执行F-Queue队列中的各个对象的finalize()方法。
如上图③:第三步在执行各个对象的finalize()方法时,是各个对象完成自我救赎,不被判定为垃圾的最后机会,如果在finalize()方法中当前对象成功与GC Roots建立了引用链。则会判定为存活,否则会被进行第二次标记,被两次标记的对象就是被判定为死亡的对象,在下次垃圾回收时,会被垃圾收集器回收掉。
三.看到这里肯定大部分都是这个疑问?GC Roots是什么?凭什么用他判定对象是否可达?
1.GC Roots是什么?
GC Roots就是一组可以作为访问根节点的一组对象集,常见的GC Roots有以下这些
①在虚拟机栈(局部变量表)中引用的对象。
②方法区中静态属性引用的对象(JDK8之后静态变量在堆中)。
③方法区中常量引用的对象(这里指运行时常量池中的常量)。
④本地方法中JNI(Native方法)引用的变量。
⑤JVM内部的引用,如基本数据类型对应的Class对象等。
2.凭什么用他们判定对象是否可达?
分析前四种可以作为GC Roots的对象可以发现,他们都有一个特征,这些对象的引用都存在堆内存以外,那为什么不是只存在堆中的对象作为GC Roots呢?因为只存在堆中的对象,比如实例变量,他的最终调用者肯定还是前四种对象(这里可以仔细思考下之前写过的代码)。那这四种常见的可以作为GC Roots的对象,其实就是最常见的引用调用的最外层结构。所以他们作为根节点,向下搜索引用关系是合理的。
四. 我们使用的虚拟机使用哪种算法判定垃圾?
我们最常用的虚拟机HotSpot一直使用的都是可达性分析算法,那为什么HotSpot不使用引用计数法呢?分析下这两种算法的优缺点。
引用计数法:
优点是判定效率高,缺点是占用一定内存,最重要的缺点就是无法解决相互引用的问题,当对象相互引用时,引用计数器的值最少也是1.如下方代码所示
objA.instance = objB; objB.instance = objA; objA = null; objB = null;
当我们使用objA=null;objB=null;以后分别各有1个引用指向objA,objB。采用引用计数法这两个引用是无法消除的,因为 objA.instance失效需要先让objA失效,objA失效需先让objB.instance失效(因为objB.instance指向objA),objB.instance想要失效需要objB失效,objB失效需要先让objA.instance失效(因为objA.instance指向objB),所以这样就陷入了死循环,导致objA、objB的引用计数器一直都是1,便无法被虚拟机回收,这也就是引用计数法的最大弊端。 如下图:
1.可达性分析算法:
优点是不必为每个对象都分配一小块内存用以存储引用计数,能轻松解决相互引用等其他场景。缺点也是相对于引用计数来说的,判定效率不如引用计数法。
五.说引用必须说下java中的强引用、软引用、弱引用、虚引用(从左到右引用关系由强到弱)。
强、软、弱、虚出现的背景:如果栈中的reference中存储的是代表另一块内存或对象的起始地址,我们称reference代表某个内存或某个对象的引用,这种对对象的定义并没有什么不对,只是随着JDK版本的不断更新,这种定义已经有些狭义了,比如对于那些比较鸡肋的对象只使用这种方式定义就显得不足,因此引入了强引用、软引用、弱引用、虚引用等用来描述不同场景下的对象引用。
1.强引用:强引用指的是最传统的引用定义,指的是我们在写java代码时最常用的一种引用赋值,如Object obj = new Object();这也是强、软、弱、虚定义之前定义引用的方式。
何时回收强引用?
强引用的引用本身就是可以作为GC Root的存在,所以只要引用关系存在,那么强引用就一直不会被回收,所以在不使用时置null即可。
2.软引用:软引用用来描述那些还有用,但是非必须的对象,使用SoftReference来修饰对象,使用get方法可以获得被弱软引用引用的对象,定义时如下:
Object obj = new Object(); String str = "abc"; SoftReference<String> sf = new SoftReference<String>(str); String strRefe = sf.get(); System.out.println(strRefe); 输出结果: abc Process finished with exit code 0
何时回收软引用?
jvm会在内存吃紧时,会尝试去清理软引用,但是不一定会能回收的了软引用的对象,jvm只是在即将OOM时会去尝试清理堆中的软引用对象,如果清除不了还是会报OOM的。如果有人看到过这种说法“jvm会在内存溢出前,清空软引用引用的对象”,请知晓,这是错误的,下面验证下这种错误的说法(因为很多人这么说),代码如下:
import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.List; public class TestHeapOom { static class OOMObject{ String name; public OOMObject(String name){ this.name = name; } } // 虚拟机配置:-Xmx10M -Xms10M public static void main(String[] args){ int n =0; List<OOMObject> list = new ArrayList<OOMObject>(); SoftReference<List<OOMObject>> sr = new SoftReference<List<OOMObject>>(list); System.out.println(sr.get().size()); while (true){ list.add(new OOMObject("a"+n)); System.out.println(sr.get().size()); } } }
上方程序是用软引用关联一个list,然后建立一个循环不断的往list中加入对象,这种情况10M的堆内存很快会用光,正常情况下内存溢出,会报OOM:Java heap space,这里输出如下展示:
106280 106281 106282 106283 106284 106285 Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded at java.lang.AbstractStringBuilder.<init>(AbstractStringBuilder.java:68) at java.lang.StringBuilder.<init>(StringBuilder.java:89) at sgcc.supplier.pojo.model.queues.TestHeapOom.main(TestHeapOom.java:23) Process finished with exit code 1
很显然这里并没有抛出OOM:Java heap space,而是抛出的OOM:GC overhead limit exceeded(不是必现),为什么抛出的是这个呢?首先解释下这个错误的意思,该错误表示cpu98%的时间都在做内存回收,但是回收到的内存依然很小,不足以支撑系统使用,系统即将崩溃。就会报这种问题,介绍软引用时已经说明了,软引用会在内存溢出之前被尝试回收,因为在上面的例子中内存即将溢出,jvm便在一直尝试回收软引用list,但是list仍是可达状态,回收不了,但系统又一直尝试软引用,便出现了这个错误:OOM:GC overhead limit exceeded。所以说“jvm会在内存溢出前,清空软引用引用的对象”这种说法是错误的,回不回收软引用对象最终依据还是可达性分析算法,jvm只是会在内存紧张时尝试回收软引用的对象。,换一种说法“jvm会在内存溢出前,清空只被软引用引用的对象”这种说法就是正确的。验证代码如下:
import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.List; public class TestHeapOom { static class OOMObject{ String name; public OOMObject(String name){ this.name = name; } } // 虚拟机配置:-Xmx10M -Xms10M -XX:+UseSerialGC public static void main(String[] args){ int n =0; //List<OOMObject> list = new ArrayList<OOMObject>(); SoftReference<List<OOMObject>> sr = new SoftReference<List<OOMObject>>(new ArrayList<OOMObject>()); while (true){ sr.get().add(new OOMObject("a"+n)); System.out.println(sr.get().size()); } } }
从上方的程序中,可以轻易的看出新创建出的ArrayList对象只被软引用关联了,除了软引用没有其他的引用关联,运行结束产生如下日志,报出的是空指针,所以在垃圾回收时,这个只被软引用关联的对象是被清空了的。另外从另一个角度也可以解释为什么只被软引用关联的对象会被清空:判定对象是否是垃圾的依据是可达性分析算法,可达性分析算法中的要求GC Roots到对象之间有引用链才说明对象可达,而软引用是不能作为GC Roots的。
132782 132783 132784 132785 132786 132787 132788 132789 132790 132791 132792 132793 Exception in thread "main" java.lang.NullPointerException at sgcc.supplier.pojo.model.queues.TestHeapOom.main(TestHeapOom.java:22) Process finished with exit code 1
3.弱引用:弱引用被用来描述那些非必须的对象,java中使用WeakReference来描述弱引用,使用get方法可以获得被弱引用引用的对象。弱引用的使用方法如下:
String str = "abc"; WeakReference<String> wf = new WeakReference<String>(str); String strRew = wf.get(); System.out.println(strRew);
何时回收弱引用?
弱引用对象在下次垃圾回收时系统会尝试进行回收,如果对象不可达就会被回收掉,对象如果是可达状态依然不会回收,验证代码如下:
import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; public class TestHeapOomWeak { static class OOMObject{ String name; public OOMObject(String name){ this.name = name; } } // 虚拟机配置:-Xmx10M -Xms10M public static void main(String[] args){ int n =0; List<OOMObject> list = new ArrayList<OOMObject>(); WeakReference<List<OOMObject>> wr = new WeakReference<List<OOMObject>>(list); System.out.println(wr.get().size()); while (true){ list.add(new OOMObject("a"+n)); System.out.println(wr.get().size()); } } }
输出结果如下,可见系统一直在进行垃圾回收,即将崩溃,但是引用却没有失效,所以弱引用并不在垃圾回收时一定被回收(这里这么解释是因为很多人说会在下一次垃圾回收时回收掉)回不回收弱引用对象最终依据还是可达性分析算法,jvm只是会在下一次GC时尝试回收弱引用。
106273 106274 106275 106276 106277 106278 106279 106280 106281 Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded at java.lang.Integer.toString(Integer.java:401) at java.lang.String.valueOf(String.java:3099) at java.io.PrintStream.print(PrintStream.java:597) at java.io.PrintStream.println(PrintStream.java:736) at sgcc.supplier.pojo.model.queues.TestHeapOomWeak.main(TestHeapOomWeak.java:25) Process finished with exit code 1
但是如果某个对象只被弱引用引用了,那下一次垃圾回收就会把该对象回收掉。验证代码如下:
import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; public class TestHeapOomWeak { static class OOMObject{ String name; public OOMObject(String name){ this.name = name; } } public static void main(String[] args){ int n =0; //List<OOMObject> list = new ArrayList<OOMObject>(); WeakReference<List<OOMObject>> wr = new WeakReference<List<OOMObject>>(new ArrayList<OOMObject>()); System.out.println(wr.get().size()); while (true){ wr.get().add(new OOMObject("a"+n)); System.out.println(wr.get().size()); } } }
如下方所示,输出变成了空指针,而不是OOM,因为在GC后只被弱引用关联的对象被回收了,后面通过弱引用get的对象就成了null,便报了空指针。
28532 28533 28534 28535 Exception in thread "main" java.lang.NullPointerException at sgcc.supplier.pojo.model.queues.TestHeapOomWeak.main(TestHeapOomWeak.java:24) Process finished with exit code 1
4.虚引用:又被称为灵幻引用,使用PhantomReference定义虚引用,使用get方法得到的永远是null,同时虚引用定义时必须传入一个ReferenceQueue 队列,对象被清理时,该对象的引用会被放入该队列中,被虚引用关联的对象无法通过get方法获得该对象的实例(软引用,弱引用都可以)所以只被虚引用关联的对象其实立马就会被回收掉,虚引用的声明方式如下
String str = "abc"; ReferenceQueue rq = new ReferenceQueue(); PhantomReference<String> pf = new PhantomReference<String>(str,rq); System.out.println(pf.get());