垃圾收集桶离居民距离 (垃圾收集算法)

  1. 判断对象是否为垃圾对象

判断对象是否已死就是找出哪些对象是已经死掉的,以后不会再用到的,就像地上有废纸、饮料瓶和百元大钞,扫地前要先判断出地上废纸和饮料瓶是垃圾,百元大钞不是垃圾。判断对象是否已死有引用计数算法和可达性分析算法。

  • 引用计数算法

算法的定义为:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器都为 0 的对象就是不可能再被使用的,即为垃圾对象。

如下图,对象2有1个引用,它的引用计数器值为1,对象1有两个地方引用,它的引用计数器值为2 。

垃圾收集转运制度,垃圾收集算法

这是实现简单,且效率非常高效的一种算法。在 redis、python 的虚拟机、FlashPlayer 等应用中,也都有采用这样的算法。但是 Java 中并没有采用这样的算法实现,主要原因是其存在相互循环引用的问题无法解决。如下图,对象1和对象2都没有被堆外的变量引用,而是被对方互相引用,这时他们虽然没有用处了,但是引用计数器的值仍然是1,无法判断他们是死对象,垃圾回收器也就无法回收。

垃圾收集转运制度,垃圾收集算法

  • 可达性分析算法

算法的定义为:通过一系列名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain), 当一个对象到 GC Roots 没有任何引用链相连,或者说不可达的时候,则证明此对象不可用。如下图:object1、object2、object3、object4和GC Roots之间有可达路径,这些对象不会被回收,但object5、object6、object7到GC Roots之间没有可达路径,这些对象就被判了死刑。

GC Roots,垃圾收集的起点,本地变量表中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象都可以作为GC Roots

垃圾收集转运制度,垃圾收集算法

上面的对象(object5、object6、object7)为垃圾对象,但并不是必死无疑,还有挽救的余地。进行可达性分析后对象和GC Roots之间没有引用链相连时,对象将会被进行一次标记,接着会判断如果对象没有覆盖Object的finalize()方法或者finalize()方法已经被虚拟机调用过,那么它们就会被行刑(清除);如果对象覆盖了finalize()方法且还没有被调用,则会执行finalize()方法中的内容,所以在finalize()方法中如果重新与GC Roots引用链上的对象关联就可以拯救自己(但此方法不建议使用).

在 Java 语言中,可以作为 GC Roots 的对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中的引用的对象。

方法区中的类静态属性引用的对象。

方法区中的常量引用的对象。

本地方法栈中 JNI(即一般说的 Native 方法)的引用的对象

  • 方法区回收

上面说的都是对堆内存中对象的判断,方法区中主要回收的是废弃的常量和无用的类。

判断常量是否废弃可以判断是否有地方引用这个常量,如果没有引用则为废弃的常量。

判断类是否废弃需要同时满足如下条件:

该类所有的实例已经被回收(堆中不存在任何该类的实例)

加载该类的ClassLoader已经被回收

该类对应的java.lang.Class对象在任何地方没有被引用(无法通过反射访问该类的方法)

  1. 常用垃圾回收算法
  • 标记-清除算法:

标记 - 清除算法(Mark-Sweep)可以说应该是最基础的收集算法了。从字面意思很好理解,算法的过程分为标记过程和清楚过程。首先标记出所有需要回收的对象,在标记完了之后,对标记对象进行统一的回收工作。哪些对象需要标记,哪些对象不需要标记,这个再上一篇文章中进行了详细的介绍,可以回顾再了解下。

这个算法的缺点也非常明显,内存中的被标记的数据不一定都是连续,因此标记清楚之后,内存中会产生大量的内存碎片,碎片的存在也会导致在后续分配较大对象时候找不到足够的连续空间,导致内存不足。还有一个问题,便是标记和清楚的效率都不高。

但之所以说这是最基础的收集算法,是因为后续是算法基本上都是由此改进得来的。

垃圾收集转运制度,垃圾收集算法

  • 复制算法

为了解决效率问题,诞生了一种叫复制(Copying)的算法。该算法将可以用的内存空间划分为两大块,每次只使用其中的一块。当这块内存使用完了之后,就将还存活的对象复制到另一块空间中去。这样就不需要考虑内存碎片的问题,只需要移动堆顶指针,按顺序分配内存即可,简单高效。同样缺点也很明显,这样做了之后很明显,我们只能使用内存中的一半内存。代价还是比较高。

垃圾收集转运制度,垃圾收集算法

  • 标记-整理算法

复制算法在对象存活较高的时候,就会执行较多的复制操作,从而降低整体的回收效率,还有存在 50% 的空间浪费。基于这种情况,有人对标记 - 清楚算法进行改进,从而衍生出标记 - 整理(Mark-Compact)算法。

这种算法的标记过程和” 标记 - 清楚 “算法一致,不同的是标记完成之后,让所有存活的对象都移动到内存的一端,然后清理掉边界外面的内存。

垃圾收集转运制度,垃圾收集算法

  • 分代收集算法

把堆内存分为新生代和老年代,新生代又分为Eden区、From Survivor和To Survivor。一般新生代中的对象基本上都是朝生夕灭的,每次只有少量对象存活,因此新生代采用复制算法,只需要复制那些少量存活的对象就可以完成垃圾收集;老年代中的对象存活率较高,就采用标记-清除和标记-整理算法来进行回收。

垃圾收集转运制度,垃圾收集算法

在这些区域的垃圾回收大概有如下几种情况:

新生代使用时minor gc, 老年代使用的full gc 同时会触发一minor gc

  • Minor GC和Full GC

在说这两种回收的区别之前,我们先来说一个概念,“Stop-The-World”。

如字面意思,每次垃圾回收的时候,都会将整个JVM暂停,回收完成后再继续。如果一边增加废弃对象,一边进行垃圾回收,完成工作似乎就变得遥遥无期了。

而一般来说,我们把新生代的回收称为Minor GC,Minor意思是次要的,新生代的回收一般回收很快,采用复制算法,造成的暂停时间很短。而Full GC一般是老年代的回收,并伴随至少一次的Minor GC,新生代和老年代都回收,而老年代采用标记-整理算法,这种GC每次都比较慢,造成的暂停时间比较长,通常是Minor GC时间的10倍以上。

所以很明显,我们需要尽量通过Minor GC来回收内存,而尽量少的触发Full GC。毕竟系统运行一会儿就要因为GC卡住一段时间,再加上其他的同步阻塞,整个系统给人的感觉就是又卡又慢。

  • 大多数情况下,新的对象都分配在Eden区,当Eden区没有空间进行分配时,将进行一次Minor GC,清理Eden区中的无用对象。清理后,Eden和From Survivor中的存活对象如果小于To Survivor的可用空间则进入To Survivor,否则直接进入老年代);Eden和From Survivor中还存活且能够进入To Survivor的对象年龄增加1岁(虚拟机为每个对象定义了一个年龄计数器,每执行一次Minor GC年龄加1),当存活对象的年龄到达一定程度(默认15岁)后进入老年代,可以通过-XX:MaxTenuringThreshold来设置年龄的值。

解释:Eden空间不够,执行MinoGC, Eden和S0中存活的对象进入S1,对象年龄计数+1 ,如果S1空间不够,直接进 入老年代 ,否则对象默认经过 15次MinorCG后才会进入老年代

  • 当进行了Minor GC后,Eden还不足以为新对象分配空间(那这个新对象肯定很大),新对象直接进入老年代。
  • 多个对象总占To Survivor空间一半以上且年龄相等,该对象直接进入老年代,比如Survivor空间是10M,有几个年龄为4的对象占用总空间已经超过5M,则年龄大于等于4的对象都直接进入老年代,不需要等到MaxTenuringThreshold指定的岁数。
  • 在进行Minor GC之前,会判断老年代最大连续可用空间是否大于新生代所有对象总空间,如果大于,说明Minor GC是安全的,否则会判断是否允许担保失败,如果允许,判断老年代最大连续可用空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则执行Minor GC,否则执行Full GC。

解释:Minor GC之前,如果预测老年代内存不够,就进行Full GC老年代,否则就Minor GC新生代

  • 当在java代码里直接调用System.gc()时,会建议JVM进行Full GC,但一般情况下都会触发Full GC,一般不建议使用,尽量让虚拟机自己管理GC的策略。
  • 永久代(方法区)中用于存放类信息,jdk1.6及之前的版本永久代中还存储常量、静态变量等,当永久代的空间不足时,也会触发Full GC,如果经过Full GC还无法满足永久代存放新数据的需求,就会抛出永久代的内存溢出异常。
  • 大对象(需要大量连续内存的对象)例如很长的数组,会直接进入老年代,如果老年代没有足够的连续大空间来存放,则会进行Full GC。