垃圾收集器与内存分配策略
内容摘抄自《深入理解Java虚拟机 第三版》
3.1 简介
略
3.2 对象已死?
在堆中存放着Java几乎所有的对象的实例,垃圾收集器在对堆进行回收之前首先判断那些对象还活着,那些对象已经死去(死去意旨不被任何途径使用的对象)
3.2.1 引用计数法
很多书籍上判断对象是否存活的算法是这样的:在对象中添加一个引用计数器,每当有一个地方引用它,计数器的值就加1;当引用失效时,计数器就减1;任何计数器为零的对象就是不可能再被使用的。
客观的来说,引用计数法(Reference Counting)虽然占用额外的额外的内存空间进行计数,但它原理简单,效率比较高,大多数情况下是不错的选择。但是在Java主流虚拟机中都没有选用引用计数法来管理内存,主要原因是这个看似简单的算法有很多例外情况要考虑,必须配合大量额外操作才能正确工作,比如单纯的引用计数法就很难解决对象之间的循环引用。
3.2.2 可达性分析
当前主流的商用系统(Java,C#等)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链,或者从图论来说的话就是从GC Roots到这个对象不可达时,则证明对象是不可能被使用的。
下图3-1所示,对象object5,object6,object7虽然互有关联,但是他们到GC Roots是不可达的,因此他们为可回收对象。

在Java技术体系中,固定可做GC Roots的对象包括一下几种:
- 在虚拟机栈(栈帧中的局部变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数,局部变量,临时变量等
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
- 在方法区中常量引用对的对象,譬如字符串常量池(String Table)里的引用
- 在本地方法JNI(几通常所说的Native方法)引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(NullPointException,OutOfMemoryError)等,还有系统的类加载器
- 所有被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等
除了这些固定的GC Roots集合外,根据用户所选的垃圾收集器及当前回收的内存区域不同,还可以有其他对象临时加入,共同构成完整的GC Roots集合。
目前最新的几款垃圾收集器无一例外都具有局部回收的特征。为了避免GC Roots包含过多对象而过度膨胀,他们在实现上也做了各种优化处理。
2021-09-10
3.2.3 再谈引用
无论通过引用计数法来判断对象的引用数量,还是通过可达性分析算法来判断对象是否引用链可达,都离不开引用。在JDK1.2之前,Java里的引用很传统的定义:如果reference类型的数据存储的数值代表另外一块内存的起始地址,就称该reference数据是代表着某块内存,某个对象的引用。这种定义没什么不对,但是过于狭隘。一块内存只有“被引用”和“未被引用”两种状态,对于描述一些“食之无味,弃之可惜”的对象就显得无能为力了。譬如我们希望能描述一类对象:当内存足够时,能保留在内存,如果内存空间在进行垃圾收集后还比较紧张时,就可以抛弃这些对象。
在JDK1.2之后,Java对引用的概念进行了扩张,将引用分为强引用(Strongly Reference),软引用(Soft Reference),弱引用(Weak Reference)和虚引用(Phantom Reference),这四种引用依次逐渐减弱
- 强引用是最传统的引用的定义,是指在程序代码中普遍存在的引用赋值,即类似“Object a = new Object()”这种引用关系。无论任何情况,只要强引用还存在,垃圾收集器不会会收掉被引用的对象
- 软引用是描述一些对象还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之内进行二次会收,如果这次会收还没有足够内存,系统抛出内存溢出异常。在JDK1.2之后提供SoftReference类实现软引用
- 弱引用也用来描述那些非必须对象,但是它的强度要比软引用更弱一些,被软引用关联的对象只能生存到下次垃圾收集发生为止,当垃圾收集器开始工作后,无论当前内存是否足够,都会回收掉之内弱引用关联着的对象。在JDK1.2之后提供WeakReference类实现弱引用
- 虚引用也称为幽灵引用或者欢迎引用,它是最弱的一种引用关系。一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过一个虚引用来获得一个对象实例。为一个对象设置虚引用的唯一目的只是为了被收集器收集时收到一个系统通知。在JDK1.2之后,提供PhantomReference类实现虚引用
3.2.4 生存还是死亡?
即时对象在可达性分析中被列为不可达的对象,也不是“非死不可”的,这时候他们处于“缓刑”阶段,要真正的宣告一个对象死亡,至少要经历两次标记过程:
-
第一次标记
如果对象进行可达性分析后发现没有与GC Roots向关联的引用链,进行第一次标记,随后进行以此筛选,筛选的条件是此对象是否有必要执行finalize()方法。加入对象没有覆盖finalize()方法,或者finalise()方法已经被虚拟机调用过,那么虚拟机将这两种情况视为“没有必要执行”
-
第二次标记
如果这个对象判断为确有必要执行finalize()方法,那么对象将被放置在名为F-Queue的队列中,并在稍后虚拟机自定建立的,低调度优先级的Finalizer线程中执行他们的finalize()方法。这里说的执行是指虚拟机会触发这个方法开始运行,但不承诺一定会等他们运行结束。finalize()方法是对象逃离死亡的最后一次尝试,稍后收集器将对F-Queue中的对象进行二次小规模标记,如果对象在finalize()中拯救了自己——只要重新与引用链上的对象重新建立连接即可,那么在第二次标记时它将被移除“即将回收”的集合;如果这个时候还没有逃离,那基本上它就真的要被回收了。下列代码为测试例子
/** * @Author gcwel * @Description * 1. 对象拯救成功 * 2. 这种自救机会只有一次,因为一个对象的finalize()方法最多被虚拟机调用一次 * @Date 2021/9/14 */ public class FinalizeGCTest { public static FinalizeGCTest SAVE_HOCK = null; public void isAlive(){ System.out.println("仍然存活"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize 被执行了"); FinalizeGCTest.SAVE_HOCK = this; } public static void main(String[] args) throws InterruptedException { SAVE_HOCK = new FinalizeGCTest(); System.out.println("--------第一次拯救--------"); //对象自一次成功拯救自己 SAVE_HOCK = null; System.gc(); //因为Finalizer方法优先级很低,暂停0.5s 等待其 Thread.sleep(500); if(SAVE_HOCK != null){ SAVE_HOCK.isAlive(); }else { System.out.println("对象已死亡"); } System.out.println("--------第二次拯救--------"); //下列代码与上述相同,但是拯救失败了 SAVE_HOCK = null; System.gc(); //因为Finalizer方法优先级很低,暂停0.5s 等待其 Thread.sleep(500); if(SAVE_HOCK != null){ SAVE_HOCK.isAlive(); }else { System.out.println("对象已死亡"); } } }执行结果
--------第一次拯救-------- finalize 被执行了 仍然存活 --------第二次拯救-------- 对象已死亡从上述代码中可以看出,SAVA_HOCK对象的finalize()确实被垃圾回收器触发过,并且在被收集前成功逃脱了。
另外一个需要注意的地方就是,代码中两段代码完全一样,但是执行结果一次成功,一次失败。这是因为finalize()方法都只会被系统自动调用一次,如果系统下次面临回收,它的finalize()方法不会被再次执行,因此第二段代码的自救失败
2021-09-13
3.2.5 回收方法区
有人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为,《Java虚拟机规范》提到可以不要求虚拟机在方法区实现垃圾收集,事实上也确实有未实现或者未完全实现方法区类型卸载的收集器存在。相比堆的垃圾回收,方法区回收有苛刻的判定条件,其区域的垃圾收集效率十分低效
方法区的回收主要有两方面内容:废弃的常量和不再使用的类型
回收废弃常量与Java堆中的对象十分相似。例如一个字符串“Java”曾经进入过常量池,但是当前系统没有任何一个字符串的值是“Java”,换句话说,已经没有任何字符串对象引用常量池的“Java”常量,且虚拟机其他地方也没有引用这个字面量。如果这时发生内存回收,且垃圾收集器判断确有必要的话,“Java”常量就会被清理出常量池。常量池的其他类(接口),方法,字段的符号引用也与此类似
判断一个常量是否被废弃还是相对简单的,而要判断一个类型是否属于“不再使用的类”的条件就比较苛刻了。需要同时满足下面三个条件
- 该类的所有实例都已被回收,也就是Java堆中不存在该类及其派生子类的实例
- 加载该类的类加载器已被回收,这个条件除非是被精心设计的可替换类加载器的场景,如OSGi,JSP否则很难实现
- 该类对应的Java.lang.Class对象没有任何地方被引用,无法通过反射访问该类方法
Java虚拟机被允许满足上述三个条件的无用类进行回收,这里说的是被允许,而并不是和对象一样,没有引用就必然被回收。
关于是否对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbosee:class以及-XX:+TraceClassLoading,-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbosee:class以及-XX:+TraceClassLoading可以在Product版的虚拟机使用,-XX:+TraceClassUnLoading参数要FastDebug版的虚拟机支持
在大量使用反射,动态代理,CGLib等字节码框架,动态生成JSP和OSGi这类频繁自定义类加载器的场景中,都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大压力
~~2021-09-16~
浙公网安备 33010602011771号