概要:
why:为什么回收,见what
what:垃圾回收哪些内存(不可达对象的确定)
when:何时执行GC(安全点、安全区域)
how:如何回收(原理——垃圾回收算法、实现——垃圾收集器)
1、垃圾回收哪些内存
JVM运行时数据区中,线程私有的程序计数器、虚拟机栈、本地方法栈随线程的创建和退出而自动产生和销毁,不需要垃圾回收;而对于方法区和堆区,由于是随虚拟机的启动和退出而创建和销毁,在这期间被各线程共享,若不回收垃圾以腾出空间则最终会耗尽这两部分空间。因此,JVM垃圾回收的是共享区域的内存,主要是方法区和Java堆内存的回收。
1.1、方法区
方法区的垃圾收集主要是回收废弃常量和无用的类(卸载类)。回收废弃常量与下面将介绍的回收Java堆中的对象很相似,而判定“无用的类”需同时满足三个条件:
1、该类所有实例已被回收,即Java堆中无该类的任何实例。(下层)
2、该类对应的java.lang.Class对象没有在任何地方被引用,无在任何地方通过反射访问该类的方法。(下层)
3、加载该类的ClassLoader已被回收。(上层)
java 8提供了-xx:MetaspaceSize来设置触发元空间垃圾回收的阈值。
1.2、堆
Java堆里面存放着几乎所有的对象实例,垃圾收集器对堆进行回收前,首先要做的就是确定哪些对象可以作为垃圾回收。
JDK1.2后,Java对引用概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。被强引用的对象(我们通常所用的对象就是强引用的)一定不会被回收,被其他引用的对象却可能被回收。
总的来说,回收的堆对象有两类:
1、有被引用的对象:即被软引用、弱引用、虚引用所引用的对象可能被当垃圾回收。
1、软引用:软引用对象在系统将要发生内存耗尽(OOM)前会被回收
2、弱引用:弱引用对象只能躲过一次垃圾回收,躲过一次后就被回收
3、虚引用:虚引用对象肯定会被回收,一个对象是否有虚引用完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,对象被设置成虚引用关联的唯一作用是在对象被垃圾收集时收到一个系统通知。
2、无被引用的对象:需要检查哪些对象已死(没被引用),主要有两种算法检测(3.1节详述具体实现):
1、引用计数法:对象有个计数器记录被引用的个数,为0者可被回收。采用此法的有微软的COM技术、使用ActionScript3的FlashPlayer、Python等。此法存在对象间循环引用的问题导致对象不能被回收。
2、可达性分析法:主流Java虚拟机采用此法。选取一系列GC Roots对象作为起点分析引用链确定不可达对象。采用此法的有Java、C#、Lisp等。对于不可达的对象,如果其覆盖了finalize()方法并且没被执行过,则在JVM对之回收前其有一次逃过被回收的机会。如下:JVM将该对象放入F-Queue队列,并由虚拟机自动建立的、低优先级的Finalizer线程稍后去执行它。若finalize()里此对象又被成员变量(类变量或实例变量)引用,则此对象复活,不会被回收;否则被回收。
2、垃圾回收算法
垃圾回收算法(概述)的效率和适用场景:
复制算法:时间效率较高、空间利用率低、不会产生空间碎片。主要操作是复制“活对象”,故适合于每次回收时大量对象死去只有少量存活的情况(年轻代)
标记-清除算法:时间效率低、空间利用率高、会产生空间碎片。主要操作是清除“死对象”,故适合于回收时大量对象仍存活只有少数死去的情况(老年代)
标记-整理算法。时间效率低、空间利用率高、不会产生空间碎片。适用情况同上
分代收集算法:就是上面几种用在堆的不同的分代区域中。年轻代:复制算法;老年代:标记-清除算法、标记-整理算法。
下面详述。
2.1、复制算法
将内存分为两等块,每次使用其中一块。当这一块内存用完后,就将还存活的对象复制到另外一个块上,然后再把已使用过的内存空间一次清理掉。示例:
说明:适合于每次回收时大量对象死去只有少量存活的情况,如年轻代中。1、无内存碎片问题,实现简单,时间效率高。2、空间利用率低,可用内存缩小为原来的一半。
2.2、标记-清除算法
首先标记出所有需要回收的对象,使用可达性分析算法判断一个对象是否为可回收,在标记完成后统一回收所有被标记的对象。示例:
说明:1、效率问题,标记和清除两个阶段的效率都不高。2、空间问题,标记清除后会产生大量不连续的内存碎片,以后需要给大对象分配内存时,会提前触发一次垃圾回收动作。
2.3、标记-整理算法
标记过程与标记 - 清除算法一样,但之后让所有存活的对象移向一端,然后直接清理掉边界以外的内存。示例:
说明:适合对象存活率高的情况,如老年代中。无需考虑内存碎片问题。
2.4、分代收集算法
根据对象存活周期将堆分为新生代和老年代,然后根据各年代特点选择适当的回收算法。
新生代基本上对象都是朝生暮死的,生存时间很短暂,因此可采用复制算法,只需要复制少量的对象就可以完成垃圾收集。
老年代中的对象存活率高,也没有额外的空间进行分配担保,因此必须使用标记 - 整理或者标记 - 清除算法进行回收,只需要清除少量对象即可完成垃圾收集。
3、具体实现(以HotSpot为例)
3.1、如何确定不可达对象
3.1.1、引用计数法
(采用此法的有微软的COM计数、使用ActionScript3的FlashPlayer、Python等)
思想:
堆中的每一个对象有一个引用计数,当一个对象被创建并把指向该对象的引用赋值给一个变量时,引用计数置为1,当再把这个引用赋值给其他变量时,引用计数加1,当一个对象的引用超过了生命周