由于垃圾收集算法的实现涉及大量的程序细节,而且每个平台的虚拟机操作内存的方法又各不相同,因此博客中不过多的讨论算法的实现,只是介绍几种算法的思想以及发展。

  相关阅读:

  1、深入理解java虚拟机之java内存区域

  2、深入理解java虚拟机之对象真的死了吗

  1、标记-清除算法

  标记清除算法分为“标记”和“清除”两个阶段,首先先标记出那些对象需要被回收,在标记完成后会对这些被标记了的对象进行回收;如下图:

Java垃圾收集算法-LMLPHP

  这种算法的优点在于不需要对对象进行移动操作,仅对不存活的对象进行操作,所以在对象存活率较高的情况下效率非常高,但是从上图模拟的结果来看对象被回收后,可用的内存并不是连续的,而是断断续续,造成大量的内存碎片。 存储对象时要求内存空间时连续的,所以虚拟机在给新的内存较大的对象分配空间时,有可能找不到足够大的连续的空闲的空间来存放,从而引发一次垃圾回收动作,实际上里面是有大量的空闲空间的,只是不连续而已。

  2、复制算法

  复制算法是将内存分为两块大小一样的区域,每次是使用其中的一块。当这块内存块用完了,就将这块内存中还存活的对象复制到另一块内存中,然后清空这块内存。这种算法在对象存活率较低的场景下效率很高,比如说新生代,只对整块内存区域的一半进行垃圾回收,在垃圾回收的过程也不会出现内存碎片的情况,不需要移动对象,只需要移动指针即可,实现简单,所以运行效率很高。运行效率是在建立在浪费空间的基础上的,这是典型的已空间换时间的方法,因为每次只能是使用北村的一半。算法示意图如下:

  Java垃圾收集算法-LMLPHP

  现在商用的jvm中都采用了这种算法来回收新生代,因为新生代的对象基本上都是朝生夕死的,存活下来的对象约占10%左右,所以需要复制的对象比较少,采用这种算法效率比较高。hotspot版本的虚拟机将堆(heap)内存分为了新生代和老年代,其中新生代又分为内存较大的Eden区和两个较小的survivor区。当进行内存回收时,将eden区和survivor区的还存活的对象一次性地复制到另一个survivor空间上,最后将eden区和刚才使用过的survivor空间清理掉。hotspot虚拟机默认eden和survivor空间的大小比例为8:1,也就是每次新生代中可用内存空间为整个新生代空间的90%(80%+10%),只会浪费掉10%的空间。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当survivor空间不够用时,需要依赖于其他内存(这里指的是老年代)进行分配的担保。

  3、标记-整理算法

  复制算法在对象存活率较高的情况下就要进行较多的对象复制操作,效率将会变低。更关键的是,如果你不需要浪费50%的空间,就需要有额外的空间进行分配担保,用以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种办法。

  根据老年代的特点,有人提出了标记-整理的算法,标记过程仍然与标记-清楚算法一样,但后续步骤不是直接将可回收对象清理掉,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,算法示意图如下:

Java垃圾收集算法-LMLPHP

Java垃圾收集算法-LMLPHP

  4、分代收集算法

  分代收集算法将heap区域划分为新生代和老年代,新生代的空间比老年代的空间要小。新生代又分为了Eden和两个survivor空间,它们的比例为8:1:1。对象被创建时,内存的分配是在新生代的Eden区发生的,大对象直接在老年代分配内存,IBM的研究表明,Eden区98%的对象都是很快消亡的。

  为了提高gc效率,分代收集算法中新生代和老年代的gc是分开的,新生代发生的gc动作叫做minor gc 或 young gc,老年代发生的叫做major gc 或 full gc。

  minor gc 的触发条件:当创建新对象时Eden区剩余空间小于对象的内存大小时发生minor gc;

  major gc 触发条件:

  1、显式调用System.gc()方法;

  2、老年代空间不足;

  3、方法区空间不足;

  4、从新生代进入老年代的空间大于老年代空闲空间;

  Eden区对象的特点是生命周期短,存活率低,因此Eden区使用了复制算法来回收对象,上面也提到复制算法的特点是在存活率较低的情况下效率会高很多,因为需要复制的对象少。与一般的复制算法不同的是,一般的复制算法每次只能使用一半的空间,另一半则浪费掉了,Eden区的回收算法也叫做"停止-复制"算法,当Eden区空间已满时,触发Minor GC,清理掉无用的对象,然后将存活的对象复制到survivor1区(此时survivor0有存活对象,survivor1为空的),清理完成后survivor0为空白空间,survivor1有存活对象,然后将survivor0和survivor1空间的角色对象,下次触发Minor gc时重复上述过程。如果survivor1区剩余空间小于复制对象所需空间时,将对象分配到老年代中。每发生一次Minor gc时,存活下来的对象的年龄则会加1,达到一定的年龄后(默认为15)该对象就会进入到老年代中。

  老年代的对象基本是经过多次Minor gc后存活下来的,因此他们都是比较稳定的,存活率高,如果还是用复制算法显然是行不通的。所以老年代使用“标记-整理”算法来回收对象的,从而提高老年代回收效率。

  总的来说,分代收集算法并不是一种具体的算法,而是根据每个年龄代的特点,多种算法结合使用来提高垃圾回收效率。

  参考资料:《深入理解Java虚拟机》

05-08 03:46