参考资料:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明
在堆里面存放着 Java 世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。
1、如何判断对象存活
1.1 引用计数法
给对象增加一个引用计数器,当对象被引用一次计数器加一、当引用失效时计数器减一
任何时候计数器为0的对象就不可能再被使用
这个方法虽然占用一定内存但是原理简单、效率也很高,大多情况都是不错的算法。但Java虚拟机并没有使用这个方法因为这个方法还要考虑很多额外的情况,配合大量处理逻辑才能解决问题。
引用计数法的缺点:
public class ReferenceCountingtGC { public Object instance = null; public static final int _1MB = 1024 * 1024; //这个成员属性的唯一意义就是占点内存,以便在GC日志中看清楚是否回收过 private byte[] bigSize = new byte[2 * _1MB]; public static void testGC(){ ReferenceCountingtGC objA = new ReferenceCountingtGC(); ReferenceCountingtGC objB = new ReferenceCountingtGC(); objA.instance = objB; objB.instance = objA; //假设在这发生GC,objA和objB能否被回收? System.gc(); } }
这两个对象objA和objB已经不可能再被访问了,但是它们因为互相引用对方,导致它们的引用计数都不为零。 所以引用计数算法就无法回收他们
1.2 可达性分析算法
这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集。
从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”。
如果某个对象到GC Roots直接没有引用链就代表这个对象不能被使用。
可作为java中Gc Roots的对象包含哪些?
虚拟机栈(栈帧中的局部变量表)中引用的对象。
方法区中的静态属性引用对象。(例如被Static修饰的字符串)
方法区中常量引用的对象。(例如字符串常量池的引用)
在本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象。
java虚拟机内部的引用。(例如数据类型对应的Class对象,一些常驻对象(OOM),系统类加载器)
所有被同步锁(synchronized 关键字)持有的对象。
反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
2、再谈引用
无论是引用计数法还是可达性分析算法,判断对象是否存活都和对象的引用分不开。
在jdk1.2以后对引用进行了扩充,将引用分为:
强引用
软引用
弱引用
虚引用
这四种引用强度依次递减。
2.1 强引用
强引用是最常见的引用类型,是程序代码之间普遍存在的引用赋值,例如这样的引用关系,在任何情况下只要强引用还在JVM就不会回收被引用的对象。
Object obj = new Object();
2.2 软引用
软引用是用来描述一些还有用但非必需的对象。在系统将要发生内存溢出异常之前,这类对象会被列为可回收的资源。在jdk1.2后提供了SoftReference
类来实现软引用。
Object obj = new Object(); SoftReference<Object> softReference = new SoftReference<>(obj);
内存不足时,GC会回收软引用指向的对象,以释放空间,避免OutOfMemoryError
。
2.3 弱引用
弱引用也用来描述非必需对象,但是其强度比软引用更弱,只能生存到下一次垃圾回收发生之前。使用WeakReference
类来实现弱引用。
Object obj = new Object(); WeakReference<Object> weakReference = new WeakReference<>(obj);
GC工作时,无论内存是否足够,都会回收被弱引用关联的对象。
2.4 虚引用
虚引用是最弱的一种引用关系。无法通过虚引用来取得一个对象实例,设置虚引用关联的唯一目的是在这个对象被收集器回收时收到一个系统通知。虚引用PhantomReference
必须和引用队列(ReferenceQueue
)联合使用。
ReferenceQueue<Object> queue = new ReferenceQueue<>(); Object object = new Object(); PhantomReference<Object> phantomReference = new PhantomReference<>(object, queue);
3、生存还是死亡
真正宣告一个对象的死亡需要至少经历两次标记的过程
1、如果对象进行可达性分析后没有与GC Roots相连的引用链,那么就会被第一次标记
接下来将会执行一次筛选,筛选条件是对象是否有必要执行
finalize()
方法。(假如对象没有覆盖finalize
方法或者finalize
方法已经被调用过。虚拟机都会将这两种情况视为没必要执行)如果这个对象被判断为有必要执行finalize方法,那么对象将会被放在一个F-Queue队列中。并在稍后由一条虚拟机自动建立的、低调度优先级的Finalize线程去执行它们的Finalize()方法。
2、finalize方法是对象逃脱死亡的最后机会,收集器将会对F-Queue中的对象进行第二次小规模的标记。
如果对象要拯救自己,那么需要重新与引用链上的任何一个对象建立关联即可。
这种自救的机会只有一次,因为一个对象的 finalize()方法最多只会被系统自动调用一次
4、回收方法区
方法区回收的性价比较低
方法区主要回收两部分内容:废弃的常量和不再使用的类型。
1、回收废弃常量的条件
已经没有任何字符串对象引用常量池的“Java”常量,并且虚拟机中也没有其他地方引用这个字面量。
2、不使用类型的回收条件
该类的所有实例都被回收了,也就是Java堆中不存在该类及其任何派生子类
加载该类的类加载器已经被回收
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。