java垃圾标记算法和垃圾回收算法总结

标记算法用于区分存活对象和死亡对象(垃圾标记阶段),垃圾回收算法用于执行垃圾的回收(清除阶段)。

1.两种主流的垃圾标记算法

1.1 引用计数法

       对于一个对象A,只要有任何一个对象引用了A,则A的用用计数器就加1,当引用失效时,引用计数器就减一。只要计数器的值为0,说明该对象A不再被使用,即引用失效。该算法的优势在于,不用等到内存不够用时才进行垃圾回收,完全可以在赋值操作的同时检查计数器是否为0,如果是的话就可以立即回收。

      但是引用计数器有一个严重的问题,即无法处理循环引用的情况。比如对象A和对象B,它们互相持有对方的引用,此时,对象A和对象B的应用计数器都不为0,但是系统中却不存在第三个对象引用A或B,也就是说,A和B是应该被回收的垃圾对象,但是由于垃圾对象的互相引用,从而使垃圾回收器无法识别,引起内存泄漏。

      引用计数器也有两个不足之处,一是它需要单独的字段来存储计数器,增加了内存空间的开销;其次,每次赋值操作都需要更新计数器,增加了时间的开销。优势在于,便于识别垃圾对象,只要计数器为0,就可以作为垃圾回收掉。但是由于它不能解决循环引用的缺陷,在java的垃圾回收器中没有使用这类算法。

1.2 根搜索算法

      HotSpot和大部分JVM都是使用根搜索算法作为垃圾标记算法的实现。根搜索算法是以根对象集合为起始点,按照从上至下的方式搜索根对象集合所连接的目标对象是否可达(使用根搜索算法后,内存中的存活独享都会被根对象集合直接或间接连接着),如果目标对象不可达,就意味着该对象已经死亡,就可以在对象头部标记为垃圾对象。在根搜索算法中,只有能够被根对象集合连接或者间接连接的对象才是存活对象。在HotSpot中,根对象集合中包含了5个元素,Java栈内的对象引用、本地方法栈内的对象引用、运行时常量池中的对象引用、方法区中类静态属性的对象引用、一个与类对应的唯一数据类型的Class对象。换句话说,这5个元素为方法局部变量、常量池中对象应用、静态属性的引用、类的字节码文件。

      在根搜索算法中,不可达对象不一定会被回收,他们只是暂时被标记为垃圾对象。如果要它真正死亡,至少需要经历两次标记过程。如果对象在根搜索算法后发现没有与GC Roots(根集合)相连接的引用链,那么它就会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过了(finalize方法只可能被调用一次),虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将会导致F-Queue队列中其他的对象一直处于等待状态,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中逃脱死亡,只要重新与引用链中的任何一个对象建立关联即可,那么在第二次标记时它将会被移出“即将回收”的集合。如果对象这个时候还没有逃脱死亡,那它就会被标记为真正需要回收的对象。

2. 主流的垃圾回收算法

2.1 标记-清除算法(Mark-Sweep)

       标记清除算法将垃圾回收分成两个阶段进行。首先是标记阶段,收集器从应用程序的根对象开始进行遍历,对所有可以访问到的对象打上一个标记,一般在对象头部(header)中,将它标记为可达对象。然后进入清除阶段,收集器对堆内存从头到尾进行线性的遍历,如果发现某个对象没有被标记为可达对象(通过读取对象头来判断),这个对象就被认为是垃圾对象,就将其回收。

      标记清除算法存在一个比较大的弊端,就是会产生大量的空间碎片,因为回收之后就结束了,并没有进行空间的压缩、优化,所以很容易产生这种情况:举个例子,一段连续的内存,前半段是已经被使用的,中间是未被使用的,后半段又是被使用的,当进行对象内存分配的使用,正好发现中间未被使用的那一段放不下我这个对象,需要去额外查找、申请其他的内存,这一点在进行大对象内存分配的时候尤其明显,工作效率远远低于连续的空间。

2.2 复制算法(Copying)

      复制算法的出现其实就是为了解决标记清除算法的缺陷。它首先将内存空间分为两块,每次只会使用其中的一块,在垃圾回收时,将赈灾使用的那一个块内存中还存活的对象复制到未被使用的内存块中,然后清除正在使用的那一块内存中的所有对象,之后交换两个内存块的角色,最后完成垃圾回收。

      复制算法需要复制存活的对象,如果存活的数量比较大,也会出现比较严重的工作效率问题。其实真正使用的时候,存活的对象数量是比较小的,因此,他的效率还是很高的,而且回收过程中,对象都是统一被复制到另一块新的内存空间中去的,因此可以确保回收喉 的内存空间是连续的,不会产生碎片。

      该算法的缺点就是只能使用一半的可用内存,它要确保存在一块内存空间是未被使用的(全新的),分配都在另一半内存空间进行。

2.3 标记-压缩算法(Mark-Compact)

      标记压缩算法也是在标记清除算法上进行改良而出现的。它还结合了复制算法执行高效的优点,去除了复制算法只能使用一半的缺点。首先和标记清除算法一样,它需要进行标记处内存中的垃圾对象,然后将所有的存活对象都复制到一个规整、连续的内存空间中,然后执行GC回收无用的对象。最后的情况就是,已经被使用的内存和未被使用的内存各自占据一边,彼此之间维系着一个记录下一次分配起始点的标记指针。

      复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。所以复制算法常见于年轻代的回收,但是在老年代,大部分对象都是存活对象,复制的成本就比较高了。而且标记清除算法又会产生比较多的内存空间碎片,所以老年代比较适合用标记压缩算法,它和标记清除算法的区别就在于在清除阶段,它并不是简单的去清理未被标记的对象,而是将所有的存活对象复制到另一端去(这里借鉴了复制算法的),最后再清理空间。这种方式既避免了产生内存空间碎片,又不需要将内存一分为二,因此这种算法是比较理想的。

2.4 增量算法(Incremental Collecting)

      在垃圾回收过程中,应用程序处于STW的状态(stop the world),在这种状态下,应用程序的所有线程都会挂起,暂停工作,等待垃圾回收完成,因此,如果垃圾回收过程时间比较长,相应的,应用程序也会被挂起很久,严重影响用户体验、系统稳定性。所以最初的想法,是希望一个进程执行垃圾收集工作,另一个进程执行应用程序,避免STW的产生。但实际情况是,垃圾收集器在第一阶段中辛辛苦苦标记出来的结果可能被被另一个进程中的内存操作改得面目全非,导致第二阶段的工作无法展开。

      这个算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成较长的停顿,它让垃圾收集线程和应用程序线程交替执行,并且每次收集线程只收集一小片区域的内存空间,然后切换到应用程序线程。依次反复,直到垃圾收集完成。这种算法虽然能减少STW的时间,但是由于频繁地切换,总体回收的成本比较高,使系统吞吐量下降。

2.5 分代收集算法(Generational Collecting)

      分代收集算法将内存空间根据对象的特点分成几块,根据每块的特点,使用不同的回收算法来提高效率。

      比如常见的HotSpot虚拟机,它将需要分配的新对象放入一个称为年轻代的内存区域,这个区域的特点是,存活对象较少、生命周期短,因此,在年轻代使用效率较高的复制算法。当一个对象经过几次回收后依然存活,它就会进入称为老年代的内存区域,这个区域中的对象都是经过几次GC存活下来的,因此这里的对象生命周期较长、而且存活率较高,这里就不能再采用复制算法了(因为需要复制大量的对象),需要使用标记压缩算法。

      所以说,分代收集算法是在对对象生命周期进行分析后得出的垃圾回收算法,把堆内存区域根据对象的生命周期特点划分为年轻代、老年代、持久代(JAVA8移除),对不同生命周期的对象使用不同的算法。从J2SE1.2开始,JVM垃圾回收期都是采用此算法。

2.6 标记-清除算法、复制算法、标记压缩算法比较

 标记清除算法复制算法标记-压缩算法
速度中等最快最慢
空间开销少(但是会不断累积)通常需要存活对象的2倍内存大小少(不会堆积)
移动对象

 

参考文献:

[深入理解JVM&G1 GC].周明耀.2017.6

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值