JVM内存 中程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭。这几个区域不用考虑回收问题,因为方法结束后 或者 线程结束后,内存就跟着回收了。而Java堆和方法区不一样,一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要的内存也可能不一样,我们只有在程序运行的时候才知道会创建那些对象,这部分内存的分配和回收都是动态的,垃圾收集主要所关注的就是Java堆和方法区。
1. 可达性分析算法
可达性分析算法,这个算法的思想,通过一系列的称为 GC Roots 的起始点,当一个对象到 GC Roots 没有任何的引用链相连,则证明这个对象是不可用的,这个对象是可回收的,如下图中的object5、object6,object7是可回收的(该图选自:深入理解Java虚拟机:JVM高级特性与最佳实践 / 周志明老师著)。
GCRootS 对象包括如下几种。
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI (即一般说的Native方法)引用的对象。
我们知道,每个方法执行的时候,JVM 都会创建一个相应的栈帧(栈帧中包括操作数栈、局部变量表、运行时常量池的引用),栈帧中包含这在方法内部使用的所有对象的引用(当然还有其他的基本类型数据),当方法执行完后,该栈帧会从虚拟机栈中弹出,这样一来,临时创建的对象的引用(虚拟机栈中)也就不存在了,或者说没有任何 GCRootS 指向这些临时对象(堆中),这些对象在下一次GC时便会被回收掉。
静态属性是该类型(class)的属性,不单独属于任何实例,因此该属性自然会作为GCRootS 。只要这个class存在,该引用指向的对象也会一直存在。Class 也是会被回收的,请看第四小节。
2 引用
引用的定义:如果 reference 类型的数据(栈帧中的本地变量表)中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。一个对象如果只有两种状态:引用和未被引用,如下情况无法去实现,以类对象,内存空间还足够的时候,可以保存这类对象,内存空间不够的话,则抛弃(回收)这些对象。
Java的引用类型分为:强引用,软引用,弱引用,虚引用。
强引用:Object object = new Object(); 这类引用,只要强引用存在,就不会回收被引用的对象(堆中)。
软引用:用来描述一些还有用,但并非必须的对象,在内存将要溢出的时候,会对于软应用的对象进行回收,如果这次回收之后内存还是不够,才会报出异常。使用 SoftReference 类来创建软引用。
弱引用:弱引用都只能活到下次垃圾回收之前,无论内存是否足够,都会进行回收。使用 WeakReference 类来创建弱引用。
虚引用:最弱的一种引用关系,一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来获取实例,为一个对象设置虚引用就是这个对象被垃圾收集器回收时受到一个系统通知。使用 PhantomReference 来创建虚引用。
3. 生存还是死亡
可达性分析方法中不可达的对象也不是“非死不可”的,这时候它们处于“缓刑”阶段,要真正宣告一个对象死亡还需要两次标记过程。
第一次标记:如果对象进行可达性分析后,发现没有与GC Roots 相连接的引用链,那么他会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法,当对象没有覆盖 finalize 方法 或者 finalize 方法已经被虚拟机调用过,虚拟机将这两种情况都是为“没必要执行”。
如果有必要执行,则将他们添加到队列F-Queue的队列之中,并在稍后由一个虚拟机自动建立、低优先级的 Finalizer 线程去执行它,这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等他运行结束,这样做的原因,如果一个对象在 finalize() 方法中执行缓慢,或者发生了死循环,导致其他对象一直等待, finalize() 方法是逃脱被回收的最后一次机会。
第二次标记:GC会再次对 F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中拯救自己,只要重新与引用链上的任何一个对象建立关联即可,例如:把自己赋值给某个类变量或者对象的成员变量,那在第二次标记时他将被移除“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
4. 回收方法区
很多人认为方法区(或者HotSpot虚拟机的永久代)是没有垃圾收集的,Java 虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的“性价比”较低;在堆中,尤其是在新生代,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
回收废弃常量与回收 Java 堆中的对象非常相似。例如:有一个常量字符串 “abc”,如果没有String 对象使用它,就回收这个常量字符串。
判断类是否为废弃类,需要满足三个条件。
- 该类的所有实例都被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
在大量使用反射、动态代理、CGLib 等框架中、动态生成 JSP 以及 OSGI 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备卸载的功能,以保证永久代不会溢出。
参考文献
- 深入理解Java虚拟机:JVM高级特性与最佳实践 / 周志明著. —— 2 版 . —— 北京:机械工业出版社,2013.6