zoukankan      html  css  js  c++  java
  • JVM垃圾回收之垃圾收集算法

      之前一篇文章(jvm-垃圾回收之垃圾标记算法)中介绍了标记阶段的算法,这篇文章将介绍清除阶段的算法。常见的大概有三种算法:标记-清除、复制、标记-压缩算法,下面将一一介绍这三种算法。

      参考自: 微信公众号 "菜鸟飞呀飞"

      1.标记-清除(Mark-Sweep)算法

      标记-清除算法是最早出现也是最基础的垃圾回收算法,它分为两个步骤:标记阶段和清除阶段。其中标记阶段的作用是标记出哪些对象是存活对象,如何判断对象是否存活,在上一篇文章垃圾回收之标记算法中已经介绍了。清除阶段就是从堆内存的起始位置开始遍历每一个对象,将未存活的对象所在的内存回收。

      注意:这里的内存回收并不是指直接置空,而是通过维护一个内存空闲列表,将死亡对象所在的内存加入到空闲列表中,内存上的数据并没有清除。当下次新的对象来申请内存空间时,就从这个空闲列表中找出一块内存区域,然后将新的对象数据写入到这块内存中,假如这块内存中原先存放过垃圾对象,那么垃圾对象的数据此时就被覆盖了。这也是为什么在 windows 电脑下删除磁盘数据或者格式化磁盘后,数据还能恢复的原因。前提条件是磁盘数据被删除或者格式化后,还没有被重新写入新的数据,否则将无法恢复。

      标记-清除算法可以用如下示意图表示。

       标记-清除算法基本上没啥优点,唯一的优点可能就是实现思路比较简单,是人们很容易想到的一种算法。

      但是它的缺点却不少,首先是效率不高。在标记阶段需要通过所有的根节点(GC Roots)遍历所有对象,判断对象是否存活,然后在清除阶段也需要从头到尾遍历整个堆空间,这相当于遍历了两遍堆空间,因此效率不高。

      其次标记-清除算法会产生内存碎片,当回收完垃圾对象后,整个堆空间中存在很多小的可用的内存区域,这些小区域不是连续的,因此被称之为内存碎片。当为一个较大的对象分配内存空间时,可能会出现堆内存虽然充足,但是却没有一块完整的空闲内存来存放这个对象,这个时候就会再次触发 GC 操作,这对应用程序是十分不友好的。

      2.复制算法

      为了解决标记-清除算法导致的内存碎片的问题,复制算法出现了。

      复制算法的实现思路是:将一块内存区域分为两个部分,每次使用时只使用其中一部分区域,另一部分区域空着,当发生垃圾回收时,就将存活的对象复制到空着的那部分区域,原先的那块内存区域一次性的全部清空。复制算法可以用如下示意图表示。

       复制算法的优点就是效率高,它将标记和清除这两个过程合二为一了,在标记过程中如果发现对象是存活对象,就直接将对象复制到空闲区域了,因此它的效率会高于标记-清除算法。另外复制算法在回收完垃圾后不会产生内存碎片。

      当然,复制算法的缺点也很明显,就是浪费了一半的内存空间。另外,因为复制对象到新的内存区域了,也就是对象的地址变了,因此复制完后,还需要修改对象的引用地址,这个过程中,也需要暂停用户线程,因此会产生 STW(Stop The World)

      如果垃圾回收的目标区域中,对象大部分都是存活对象,甚至在极端情况下,所有对象都是存活对象,那么采用复制算法就需要复制所有对象了,这效率肯定是很低了。因此复制算法适合用于每次垃圾回收时,大部分对象都是垃圾对象的区域。例如新生代区域,大部分对象都是”朝生夕灭“的对象,每次对新生代区域进行垃圾回收时,大部分都是可回收的。

      事实上,针对新生代区域的垃圾回收器如:Serial、ParNew、Parallel Scavenge,它们都是采用复制算法来进行垃圾回收的。

      在 HotSpot 虚拟机中,新生代又被细分为 Eden 区、Survivor0 区、Survivor1 区(后面简称 S0 和 S1),默认情况,「Eden:S0:S1=8:1:1」,在使用过程中 S0 和 S1 区始终会有一块区域是空闲的,占新生代的 10%,而复制算法要求一半的空闲区域,那么为什么针对新生代的垃圾回收算法还能使用复制算法呢?这是因为新生代中大部分都是垃圾对象(通常占 98%),每次回收时,存活的对象极少,因此用 Survivor 区域完全能存放下这些存活对象。如果出现了 Survivor 存不下存活对象,别担心,还有担保空间的存在。至于什么是担保空间,后面在分享 JVM 内存结构的文章中,会详细介绍。

      3.标记-压缩(Mark-Compact)算法

      前面提到的标记-清除算法会产生内存碎片,复制算法会造成一半的内存区域浪费,且不适合回收大部分对象都是存活对象的区域,为了解决这两个算法的缺点,标记-压缩算法出现了。

      标记-压缩算法的实现思路是:先根据标记算法判断每个对象是否是存活对象,然后再将存活的对象全部压缩到内存的一端,最后将边界外的内存区域全部清空。示意图如下。

       

      标记-压缩算法和标记-清除算法比较相似,但是区别是:标记-压缩算法多了一个步骤,就是内存碎片的整理。标记-压缩算法回收垃圾后,不需要维护一个单独的空闲列表来标识可用内存,而只需要维护一个空闲地址的起始指针即可,这比维护一个空闲列表所耗费的开销小很多。当需要为新的对象分配内存地址时,只需要移动该指针即可。
      标记-压缩的优点是不会产生内存碎片,同时也消除了复制算法中浪费一半内存区域的缺点。但是标记-压缩算法的缺点也很明显,它比标记-清除算法多一个整理内存空间的步骤,因此效率更低。同时标记-压缩算法在整理内存过程中,还会涉及到移动对象的过程,因此在此期间会暂停用户线程,修改变量的引用地址,也会造成 STW 的现象。
     
      对比
      这三种垃圾回收算法,各有优缺点,没有谁是完美的。通常在实际使用过程中,都是搭配使用。
        最后,用一个表格,从三个方面来对比一下标记-清除、复制、标记-压缩算法。
     
      标记-清除 复制 标记-压缩
    效率 中等 最快 最慢
    内存开销 小(但会产生内存碎片) 浪费一半内存(无内存碎片) 小(无内存碎片)
    移动对象 不需要 需要

    需要

     
  • 相关阅读:
    第二阶段个人冲刺总结01
    软件工程学习进度表13
    软件工程学习进度表12
    个人博客(09)
    个人博客(07)
    个人博客(08)
    poj1562 DFS入门
    poj3278 BFS入门
    数组单步运算
    十天冲刺
  • 原文地址:https://www.cnblogs.com/no-celery/p/13155830.html
Copyright © 2011-2022 走看看