导语
  在GC 中最重要的算法就是GC标记-清除算法(Mark-Sweep GC)。在很多的场景下都还是在使用这个算法来进行垃圾回收操作。就如如同它的名字一样先标记,然后清除。下面就来看看标记清除算法到底如何实现。

文章目录

  • 什么是GC标记-清除算法
    • 标记阶段
    • 清除阶段
    • 分配
    • 合并
  • 算法优点
    • 实现相对简单
    • 与保守的GC算法高度兼容
  • 缺点
    • 碎片化
    • 分配速度
    • 与写时复制技术不能兼容
  • 总结

什么是GC标记-清除算法

  GC标记清除是由两个阶段构成的,一个是标记阶段,就是把所有活跃对象都进行相应的标记,另一个是清除阶段,是把所有那些没有进行标记的对象,也就是所说的非活跃对象进行回收。通过这两个阶段的操作,就可以让不能被利用的内存空间重新得到利用。但所带来的的问题就是内存碎片化,为了解决这个问题就出现了标记复制算法,这个算法在其他博客中会进行分享。

  根据标记清除算法的描述,它的两个阶段,分别由下面两个步骤组成。标记清除的伪代码如下。

mark_then_sweep(){frist_mark_phase();//先标记then_sweep_phase(); //后清除
}

标记阶段

  上面代码中使用frist_mark_phase()函数来表示标记阶段,在标记阶段主要的工作内容就是对活动对象进行标记。对于活动对象,在之前的博客中有分享。在Java中对象之间是存在一条引用链路,要想知道对象是否是活跃对象,或者说是否被使用,只需要知道它这个引用链路是否断裂即可。它的伪代码如下

frist_mark_phase(){for(r:roots){mark(r);}
}

  如图下图,roots 其实就是存储了整个引用链路上的所有的能被引用的对象,也就是以这些对象为根节点所有可以被引用的对象。对于那些不能被引用的对象,则不会出现在引用链路上。这里使用了数组加链表的形式来表示一个树形的数据结构。

  mark()函数,其实这里简单的理解一下,找到一个根节点,从这个根节点开始递归去找到所有的其他链路节点就可以实现链路对象的标记。它是一个深度搜索的策略,找到一个对象之后要继续去查找这个对象下的所有的对象。

mark(obj){if(obj.mark==FALSE){obj.mark=TURE;for(child:children(obj)){mark(child);}}
}

  当所有的标记阶段完成之后肯定是会有一部分的对象,不能被其他对象所引用,这个时候,就认为这些对象是非活跃对象,而整个的清理阶段就是对这些非活跃对象进行操作。

  对于标记活跃对象来说,有两种方式可以采用。前面提到了这里使用的是数组加链表的形式来表示一个数型数据结构。那么对于树的搜索,有两种方式,一种是深度优先搜索,一种是广度优先搜索。从上面的内容来看,标记清除阶段其实是一个深度优先搜索的过程,从一个根节点开始,一直往下查找,直到找到最后一个节点才算完成。如下图所示。

  对于深度优先还是广度优先来说,为什么标记阶段要使用深度优先算法,首先深度优先算法更容易一次找到一条引用链路上所有活跃对象。拿上图来说,假如此时它的第一次搜索结果到这里就结束了,那么找到的所有的活跃节点就是1,2,3,4 。继续进行第二次搜索,那么第二次搜索开始的时候就从节点3 开始,一直往下找,接下来是节点2,然后节点8……。所以从这个角度上来看,在一定的时间复杂度内,深度优先更容易实现对于活跃节点的标记。为什么要使用深度优先搜索,从上图可以看到,几乎所有的可能被回收的对象都处于整个的树形结构的最底层,也就是说最底层的叶子节点是最容易被回收的。就像前面提到的那样在短时间能可以更高效的实现活跃节点的标记。从这个树形结构来看,其实,最上层的对象存活的时间越久。越不容易被回收,如果使用广度优先搜索,会做很多无用的操作。不能很高效的找到所有底层活跃节点。

清除阶段

  如上图中所示,看上去所有对象在内存中的存储的引用链路其实是一个树型结构。当然它虽然是树形结构,但是实际存储的时候却不是以这种结构存储,它是采用上面提到的数组加链表的形式。那么怎么对这个树形结构中所有没有被标记的对象进行清理呢?这个就是下面需要讨论的问题。
  在之前的分享中,我们知道,在Java中,一个对象的对象结构如下图所示。头和域组成,其中头中包括的对象大小、对象类型等信息,而域里面包括的内容就比较多了。

  那么清除的时候到底是如何操作呢,下面先来看看伪代码,也就是将文字描述转换成伪代码

then_sweep_phase(){//1、首先要找到交换分区开始的位置sweeping = $heap_start//2、循环看是否到达了交换分区的结束位置while(sweeping<$heap_end){//3、如果没有到达结束的位置,则继续进行,如果到达则结束if(sweeping.mark == TRUE)sweeping.mark == FALSEelse// 4、将被清除的位置指向一个空链表,将这个空链表替换现在的位置sweeping.next = $free_list$free_list = sweeping// 5、交换分区长度增加sweeping += sweeping.size;}
}

  可以将堆内存看作是一个固定长度的数组,而在数组中就存储着各个对象。

  上面的操作就是通过sweeping 这样一个变量,对这个数组进行了遍历。在之前的标记阶段已经对所有的对象进行活跃与非活跃的标记。这次的遍历就是将所有的非活跃对象进行清理,其中有一个地方就是size,这个里的size就是对象的实际大小,从什么位置到什么位置开始,所有的内容都清空。

  这里还有另外的一个操作就是对已经进行标记的对象要把标记位清理,用来进行下一次的回收标记操作。这里还出现了一个新的域叫做next域,这个域只有在生成空链表以及从空链表中取出分块的时候才会用到。

  如下图所示,它既是被清理完成之后的堆内存状态。会看到,现在所有的空闲内存都是由一个空闲链表进行串联起来的。在清除阶段,程序会遍历整个的堆内存,进行垃圾回收,在之前提到JVM垃圾回收的时候有一个叫做Step-The-World的阶段,就是停止所有的应用进程,进行垃圾回收操作,也就是说,其实遍历堆内存的时间与堆内存的大小也是有关系的。如果堆内存太大的话也会消耗一定的时间。

分配

  在上面可以看到所有的被回收的内存都是通过空链表来连接起来的,那么怎么对这些回收后的内存进行再次利用呢?也就是说当程序运行,新的对象被创建,是怎么进行合适的内存大小进行分配对象呢?因为我们知道,在第一次运行的时候,堆内存其实可以看做是一个空白的空间,但是经历过GC之后,怎么利用这些分散的空间。方法如下

new_obj(size){// 筛选,分配chunk = pickup_chunk(size,$free_list)if(chunk!=null){return chunk;}else{allocation_fail();}
}

  在C语言中有一个函数malloc(),它是用来分配对应大小的内存空间。void * malloc (size_t size); 返回值是这段空间的地址。在上面代码中 pickup_chunk(size,$free_list) 函数用于遍历free_list ,寻找一块size大小的空间,返回一个大于等于size的空间即可。这样就可以将对象放入到对应的空间中。如果没有找到合适的空间就会返回null。这个时候就会有后面allocation_fail();分配空间失败的策略。也就说对于空间分配一个对象进来之后必须给出一个符合逻辑的结果。

  对于分配来讲有如下的三个分配策略

  • First-fit 在分配对象的时候,一个遇到大于等于size的分块就会立即返回该分块
  • Best-fit 在返回之前先遍历空链表,返回大于等于Size的分块
  • Worst-fit 找出空闲链表中最大的分块,将其分隔为申请的大小和分隔后的剩余的大小,目的是将分隔后剩余的分块最大化,但是容易生成大量小分块。

合并

  根据不同的分配策略会产生大量的小分块,但是如果这些产生的分块是一块连续的空间,那么就可以将这些小分块连接形成一个大分块。这种“连续连接分块”的操作就叫做合并,在很多的垃圾收集器中,将合并在清除的阶段顺便进行了。也就是说在清除阶段的伪代码其实可以写成如下的一种形式

sweep_phase(){sweeping = $heap_start;while(sweeping<$heap_end){if(sweeping.mark == TRUE){sweeping.mark = FALSE}else{if(sweeping == $free_list + $free_list.size){$free_list.size += sweeping.size;}else{sweeping.next = $free_list$free_list = sweeping}sweeping += sweeping.size;}}
}

  在上面代码中,增加了用于检查这次发现的内存块是否和上次发现的内存块属于连续的,如果发现是连续的块,则将它们连接成一个大块,如果不是则进行分配。

算法优点

实现相对简单

  在所有的关于垃圾回收的算法中,标记清除算法实现起来是最为简单的,通过上面的伪代码也可以看出来。它对于内存的管理方式是一个暴力的管理方式,为什么是暴力呢?就是在标记的时候需要遍历,在清除的时候也需要遍历。但是正是因为这种遍历,才能让标记清除算法与其他算法很好的结合到一起。

与保守的GC算法高度兼容

  在保守式的GC算法中,对象是不能被移动的,所以保守式的算法与复制算法或者标记压缩算法那相比较它是不能很好的兼容。而标记清除算法它本身是不会移动对象的,所以在保守式的GC操作中,很多的地方都是实现了标记清除算法。

缺点

碎片化

   这个概念在前面提到过,在GC标记清除出算法中会逐渐将内存碎片化,不久之后就会导致很多内存碎片分散在堆内存中,这种碎片化现象在Windows的文件系统中也会出现。

  如果发生了碎片化,那么即使堆中分块的总大小够用,也会因为一个个的分块都太小而不能执行分配操作。就会增加mutator 的负担,会把具有引用关系的对象安排在堆中相距较远的位置,就会增加访问需要的时间。因为分块在堆中分布情况取决于mutator的运行情况,所以只要使用GC标记清除算法就会或多或少的产生碎片化。

分配速度

  GC 标记清除算法 中分块不是连续的,所以每次分配都必须遍历一遍空闲链表,找到足够大的分块,而最糟糕的情况就是每一次遍历,所找到的分块都在链表的最后。

  另外,因为GC复制算法和GC标记压缩算法中,分块是作为一个连续的内存空间存在,所以没有必要遍历空闲链表,分配就能高速运行,并且堆内存允许在规定范围内分配大对象。

与写时复制技术不能兼容

  对于写时复制技术,在Linux系统的虚拟存储中用到的高速化方法,Linux的复制进程中,也就是使用fork的时候,大部分的内存空间都不会被复制,而是将内存空间进行了共享。这就是写时复制技术基础。在各个进程访问数据的时候能够访问共享内存就没有问题了

  但是会出现一个问题,当我们对共享内存进行写操作的时候,不能直接写共享内存,因为在写的同时如果有其他进程访问,就会导致数据不一致。所以在写的过程中,需要复制自己私有空间的数据,对这个私有空间进行重写,复制后只访问这个私有空间,不访问共享内存,这样才能真正被称为是写时复制技术。

  GC标记清除算法就对这一机制不兼容,即使没有重写对象,GC也会设置所有活动对象的标志位,这样就会频繁的发生不该发生的复制操作。这里简单的理解一下,当一个对象,它在内存中生活的好好的,本来不需要打扰。也就是说它不需要进行修改。但是标记算法过来之后,就会在该算法的进程中对该对象进行修改,但是实际上按照共享内存这种说法,它所改变的就是它自己内存中的东西,如果复制的内容太多的话就会直接影响到内存空间大小。

总结

  上面简单的介绍了关于标记清除算法的一些策略以及基础的内容,并且对标记清除算法的优缺点进行了分析。也看到了标记清除算法其实还是有很多值得优化的地方。这些优化的地方不但是可以从JVM的地方可以考虑,还可以通过对象组织形式来考虑。下面的分享笔记中就会提到关于标记清除算法从JVM角度和从对象组织形式角度的优化。

垃圾回收算法与实现系列-GC 标记-清除算法相关推荐

  1. 垃圾回收算法|GC标记-清除算法

    本文是<垃圾回收的算法与实现>读书笔记 什么是GC标记-清除算法(Mark Sweep GC) GC 标记-清除算法由标记阶段和清除阶段构成.在标记阶段会把所有的活动对象都做上标记,然后在 ...

  2. 【Android 内存优化】垃圾回收算法 ( 内存优化总结 | 常见的内存泄漏场景 | GC 算法 | 标记清除算法 | 复制算法 | 标记压缩算法 )

    文章目录 一. 内存优化总结 二. 常见的内存泄漏场景 三. 内存回收算法 四. 标记-清除算法 ( mark-sweep ) 五. 复制算法 六. 标记-压缩算法 一. 内存优化总结 内存泄漏原理 ...

  3. java标志清理_JVM内存管理之GC算法精解(五分钟让你彻底明白标记/清除算法)...

    相信不少猿友看到标题就认为LZ是标题党了,不过既然您已经被LZ忽悠进来了,那就好好的享受一顿算法大餐吧.不过LZ丑话说前面哦,这篇文章应该能让各位彻底理解标记/清除算法,不过倘若各位猿友不能在五分钟内 ...

  4. JVM内存管理–GC算法精解(五分钟让你彻底明白标记/清除算法)

    相信不少猿友看到标题就认为LZ是标题党了,不过既然您已经被LZ忽悠进来了,那就好好的享受一顿算法大餐吧.不过LZ丑话说前面哦,这篇文章应该能让各位彻底理解标记/清除算法,不过倘若各位猿友不能在五分钟内 ...

  5. JVM内存管理------GC算法精解(五分钟让你彻底明白标记/清除算法)

    转载自  JVM内存管理------GC算法精解(五分钟让你彻底明白标记/清除算法) 相信不少猿友看到标题就认为LZ是标题党了,不过既然您已经被LZ忽悠进来了,那就好好的享受一顿算法大餐吧.不过LZ丑 ...

  6. Java GC的标记-清除算法【总结】

    Java GC(Garbage Collector)标记-清除算法: 1.标记清除算法: 点击了解:Java的内存管理 GC标记-清除算法由标记阶段和清除阶段构成,在标记阶段会把所有的活动对象都做上标 ...

  7. 【Java 虚拟机原理】垃圾回收算法 ( 标记-清除算法 | 复制算法 | 标记-整理算法 )

    文章目录 总结 一.标记-清除算法 二.复制算法 三.标记-整理算法 总结 常用的垃圾回收算法 : 标记-清除算法 ; 复制算法 ; 标记-整理算法 ; 这些算法没有好坏优劣之分 , 都有各自的 优势 ...

  8. java整段标记_聊聊JAVA GC系列(7) - 标记整理算法

    在介绍"平平无奇"的标记清除算法时, 还留下了另一个问题, 就是内存碎片的问题. 内存碎片的问题是指, 每次回收的内存都是比较分散的, 可以加起来是一个比较大的数值, 但是由于可用 ...

  9. 2、垃圾回收算法(标记清除算法、复制算法、标记整理算法和分代收集算法),各种垃圾收集器讲解(学习笔记)

    2.垃圾回收概述 2.1.垃圾回收算法 2.1.1.垃圾回收算法-标记清除算法 2.1.2.垃圾回收算法–复制算法 2.1.3.垃圾回收算法–标记整理算法和分代收集算法 2.1.4.垃圾回收算法–Se ...

最新文章

  1. Java课堂测试——一维数组
  2. python在线课程-《Python程序设计与应用》在线课程使用说明
  3. eclipse配置Struts2、Hibernate3、Spring2.5范例
  4. mysql 刷新二进制日志_使用binlog日志恢复MySQL数据库删除数据的方法
  5. Hibernate ,Mybatis 区别,以及各自的一级,二级缓存理解
  6. vue 指令 v-text v-html v-pre
  7. 【对讲机的那点事】维修对讲机你会拆卸电路板上的集成电路块吗?
  8. 《BackTrack 5 Cookbook中文版——渗透测试实用技巧荟萃》—第3章3.4节识别在线设备...
  9. python入门(1)文档的处理
  10. 计算机取证之你必须要会用的24款工具
  11. 站长必会数据统计工具教程:百度统计 VS GA
  12. 海康威视前端实习生面试
  13. ESP32使用MLX90614红外测温传感器
  14. 计算两个经纬度点之间的距离
  15. Spring基础学习
  16. 平稳性检验和白噪声检验
  17. 大脑笔记:快速记忆之三大方法记忆圆周率前一百位
  18. 【微服务】GateWay概念与使用
  19. 获取元素到body顶部的距离,offsetTop和offsetParent,getBoundingClientRect
  20. 文件服务协议:cifs/nfs 简介

热门文章

  1. c语言有趣的软件,一个有趣的小程序
  2. java相关协议_java相关网络协议是什么
  3. 大兴新年大集 推10条文化旅游线
  4. android6.0权限管理工具EasyPermissionUtil
  5. Javascript中的form
  6. kali实战-被动信息收集
  7. Windows 查看程序占用的端口
  8. 开源云操作系统:填补技术空白 加速商业化进程
  9. Linux概述及centos版本介绍和下载地址
  10. 【leetcode】33. Search in Rotated Sorted Array