浅谈JAVA垃圾回收机制及其回收时机

        众所周知,Java与C++相比有一个明显的不同就是:Java拥有自动垃圾回收机制,而C++的垃圾回收则完全由程序员自己手动完成,这不仅对程序员本身素质有一定的要求,还提高了内存泄漏的风险。
        以下内容参考《深入理解Java虚拟机》一书,经过自己理解整理而来,欲知完整全面的内容,请参看原著。

如何判断一个对象已经“死亡”

        首先,我们需要知道JVM如何判断一个对象已经“死亡”了,答案是:可达性分析算法。
        此算法的基本思想是:通过一系列的被称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,如果某个对象到GC Roots不与任何引用链相连,则表示此对象不可用。
        在Java中,GC Roots对象包括以下几种:

  • 局部变量表中引用的对象(方法中的参数,方法体中的局部变量);
  • 方法区中类静态属性引用的对象(static);
  • 方法区中常量引用的对象(final static);
  • 本地方法栈中JNI引用的对象(native方法)。

        从上面的范围我们可以看出来,基本上除了成员变量引用的对象外,其余引用对象的方式均可作为GC Roots。

垃圾回收简要过程

        这里必须点出一个很重要的误区:不可达的对象并不会马上就会被直接回收,而是至少要经过两次标记的过程。
        第一次被标记过的对象,会检查该对象是否重写了finalize()方法。如果重写了该方法,则将其放入一个F-Query队列中,否则,直接将对象加入“即将回收”集合。在第二次标记之前,F-Query队列中的所有对象会逐个执行finalize()方法,但是不保证该队列中所有对象的finalize()方法都能被执行,这是因为JVM创建一个低优先级的线程去运行此队列中的方法,很可能在没有遍历完之前,就已经被剥夺了运行的权利。那么运行finalize()方法的意义何在呢?这是对象避免自己被清理的最后手段:如果在执行finalize()方法的过程中,使得此对象重新与GC Roots引用链相连,则会在第二次标记过程中将此对象从F-Query队列中清除,避免在这次回收中被清除,恢复成了一个“正常”的对象。但显然这种好事不能无限的发生,对于曾经执行过一次finalize()的对象来说,之后如果再被标记,则不会再执行finalize()方法,只能等待被清除的命运。
        之后,GC将对F-Queue中的对象进行第二次小规模的标记,将队列中重新与GC Roots引用链恢复连接的对象清除出“即将回收”集合。所有此集合中的内容将被回收。

方法区的回收

        此处之所以要把回收方法区专门挑出来单独说,是因为方法区中要回收的内容比较特殊:废弃的常量和无用的类。废弃常量的回收比较简单,跟堆中对象的回收类似,但是判断一个类是否无用,则需要同时满足一下3个条件才可:

  • 该类所有的实例都已经被回收,也就是说Java堆中不存在该类的任何实例;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。

常用垃圾回收算法

        为了垃圾回收方便,Java堆区可以大致分为“新生代”和“老年代”。同时,一般新生代还分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
        新生代的回收算法一般使用“复制算法”:每次新的对象分配在Eden+一块Survivor的连续空间上,当这片空间无法满足分配要求,会将空间中还存活着的对象复制到另一块Survivor空间中,然后把之前空间中的内存全部清理掉。如果发现此时Survivor空间不够用,则会触发“分配担保”机制,使这些对象直接放入“老年代”的空间中。
        老年代常用的回收算法是“标记-整理”算法:首先标记处所有需要回收的对象,标记过程如前所述;之后,让所有存货的对象都向老年代内存空间的一端移动,然后直接清理掉存货对象端边界以外的所有内存(老年代范围内);

两种GC方式

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Full GC):指发生在老年代的GC,出现一次Full GC尝尝伴随至少一次Minor GC(只有Minor GC仍然满足不了空间分配才会触发Full GC)。

对象的分配

        大多数情况下,新分配的对象会在新生代Eden区分配,但Eden区没有足够空间进行分配时,会进行一次Minor GC。
        “大对象”会直接分配至老年代,所谓“大对象”指需要大量连续内存空间的Java对象。由-XX:PretenureSizeThreshold参数设置门限大小。
        对于长期在Survivor中存活的对象显然也不会让它们一直在那里安稳地呆着,每个对象均有一个对象年龄计数器,此对象没经历一次GC过程并且仍然存活,则此计数器加一,当计数器值超过某个阈值,则将此对象转移至老年代。可通过-XX:MaxTenuringThreshold设置阈值。

空间分配担保

        在每次发生Minor GC之前(注意这个时间点),JVM均会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间(注意,此处所说的是当前新生代中的所有对象总空间,不管其中是否有需要回收对象,都算在内,且不管是否有对象需要放入老年代,都需要在Minor GC前进行这次判断!)。如果这个条件成立,则可确保此次Minor GC是安全的;如果不成立,则进行空间分配担保(此机制可以禁用):检查老年代中最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试一次Minor GC,否则,先进行一次Full GC来清理老年代。
        需要注意的是:空间担保机制本质是使得原本应该放入Survivor区的对象,由于该区最大连续空间容量不足,从而使得这部分本该复制进该区的对象直接放入老年区,若老年区也放不下,则会进行一次Full GC使得老年区试着腾出空间来接纳这些对象。清理过后,新的对象还是直接分配在Eden区(如果不是“大对象”的话)。

你可能感兴趣的:(Java学习)