理想的垃圾回收的目的是回收程序不再使用的对象所占用的空间,任何自动内存管理系统都面临三个任务:

  • 为对象分配空间;

  • 确定存活对象;

  • 回收死亡对象所占用的空间;

这些任务并非相互独立,特别是回收空间的方法影响着分配新空间的方法。真正的存活性问题是一个不可确定的问题,因此我们使用指针可达性爱近似对象的存活性:只有当堆中存在一条从根出发的指针链最终到达某个对象时,才能认定该对象存货,更进一步,如果不存在这样一条指针链,则认为对象死亡,其空间可以回收。尽管存活对象集合中可能包含一些永远不会再被赋值器访问的对象,但是死亡对象集合中的对象却必定是死亡的。

标记——清扫回收算法是在指针可达性递归定义指导下最直接的回收方案。它的回收过程分为两个阶段:

1)第一阶段为追踪阶段,即回收器对象从根集合(寄存器、线程栈、全局变量)开始遍历对象图,并标记所遇到的每个对象;

2)第二阶段为清扫阶段,即回收器检查堆中的每一个对象,并将所有未标记的对象当作垃圾进行回收。

标记——清扫算法是一种间接回收算法,它并非直接检测垃圾本身,而是先确定所有存活对象,然后反过来判定其他对象都是垃圾。需要注意的是,该算法的每次调用都需要重新计算存活对象集合,但并非所有的垃圾回收算法都需要如此。引用计数垃圾回收算法是直接回收策略,其可以通过对象本身来判断其存活性,因此不需要额外的追踪过程。

1.标记——清扫算法

从垃圾回收器角度来看,赋值器线程所执行的只有三种操作:创建、读、写。每一个回收算法必须堆这三种操作进行合理的重定义。标记——清扫算法与赋值器之间的接口十分简单:如果线程无法分配对象,则唤起回收器,然后再次尝试分配。

回收器和赋值器之间的调用关系:如果线程无法再分配对象了,则唤起回收器,回收完后再次尝试分配,若分配失败,则抛出OOM异常。
回收器如何标记垃圾:回收器可以通过在对象头部某个位或者字节的方式对其进行标记,也可以标记在另外的一张表中。
标记阶段:工作列表为空的时候表示标记完成,此时表示回收器已经对每个可达对象完成的访问和标记,没有打上标记的则表明是垃圾。
清扫阶段:回收器会在堆中进行线性扫描,即从堆底开始释放未标记的对象,然后将这些对象返还给分配器,用于后面的内存分配需求。同时还会清空存活对象的标记用于下次回收时使用。

标记——清扫算法:分配阶段

New():ref <- allocate()if ref = null  /*堆中无可用空间*/collect()ref <- allocate()if ref = null  /*堆中仍无可用空间*/error "Out of memory"return refatomic collect(): /*原子操作*/markFromRoots()sweep(HeapStart,HeapEnd)

回收器在遍历对象图之前必须先构造标记过程中需要用到的起始工作列表(work list,即标记阶段中的markFromRoots),即对每个根对象进行标记并将其加入工作列表。回收器可以通过设置对象头部某个位(或者字节)的方式对其进行标记,该位(或字节)也可位于一张额外的表中。不包含指针的对象不会有任何后代,因此无须将其加入工作列表,但仍需将其标记。为了减少工作列表的大小,markFromRoots方法将在线程根加入到工作列表之后立刻调用mark方法,而如果将mark方法从循环中提出则可加快线程根扫描,例如并发回收器可能只需要挂起线程并扫描其栈,而接下来的对象图遍历则可以与赋值器并发进行。

标记——清扫算法:标记阶段

markFromRoots():initialise(worklist)
for each fld in Rootsref <- *fldif ref != null && not isMarked(ref)setMarked(ref)add(worklist,ref)mark()initialise(worklist):worklist <- emptymark():while not isEmpty(worklist)ref <- remove(worklist)  /*ref已经标记过*/for each fld in Pointers(ref)child <- *fldif child != null ** not isMarked(child)setMarked(child)add(worklist,child)

标记存活对象的过程非常直观:先从工作泪飙中获取一个对象引用,然后对其所引用的其他对象进行标记,直到工作列表变空为止。需要注意的是,在这一版本的mark方法中,工作列表中的每个对象都拥有自身的标记位。如果某一指针域的值为空,或者其指向的对象已经标记过,则无须对其进行处理,否则回收器需要对其目标对象进行标记并将其添加到工作列表中。
标记阶段完成的标志是工作列表变空,而不是将所有已标记对象都添加到工作列表。此时回收器已经完成每个可达对象的访问与标记,任何没有打上标记位的对象都是垃圾。
在清扫阶段,回收器会将所有未标记的对象返还给分配器。在这一过程中,回收器通常会在堆中进行线性扫描,即从堆底开始释放未标记的对象,同时清空存活对象的标记位以便下次回收过程复用。另外,如果标记过程使用两个标记位,且连续两次标记过程使用的标记位不同,则可以省去清空标记位的开销。
标记——清扫:清扫阶段

sweep(start,end):scan <- startwhile scan < endif isMarked(scan)unsetMarked(scan)else free(scan)scan <- nextObject(scan)

标记——清扫回收器要求堆布局满足一定的条件:

  • 标记——清扫回收器不会移动对象,因此内存管理器必须能够控制堆内存碎片,这是因为过多的内存碎片可能会导致分配器无法满足新分配请求,从而增加垃圾回收频率,在更坏情况下,新对象的分配可能根本无法完成;
  • 第二,清扫器必须能够遍历堆中每一个对象,即对于给定对象,不管其后是否存在一些用于对齐的填充字节,sweep方法都必须能够找到下一个对象,因此用nextObject方法要完成堆的遍历,仅获取对象的大小信息是远远不够的。

1.1三色抽象

三色抽象可以简洁的描述回收过程中对象状态的变化。
在三色抽象中,回收器将对象图划分为黑色对象(确定存活)和白色对象(可能死亡)、灰色对象(已被回收器遍历但尚未完成处理或者需要再次进行处理)对象。

2.改进标记——清扫算法

由于标记——清扫算法需要堆布局满足上述条件,且标记——清扫算法不满足程序在运行时表现出的时间局部性和空间局部性:

  • 时间局部性,即一旦程序访问了某个内存地址,则很可能在不久之后再次访问该地址,因此值得将它的值缓存;
  • 空间局部性:一旦程序访问了某个内存地址,则很可能在不久之后访问该地址附近的数据。
    标记——清扫回收器的时间局部性并不明显,尽管程序中可能存在一些被引用次数非常多的对象,但大部分对象都不是共享的(即只被一个指针所引用),因而回收器在标记阶段通常只会读写对象头部各一次,即:回收器读取对象头部的标记位,如果该对象尚未得到标记则设置其标记位,除此之外回收器在标记阶段一般不会再次访问该对象。对象头部通常会包含一个指向该对象类型信息的指针(对象类型信息可能也是一个对象),回收器据此来获取对象中的指针域。这类型信息可能包含指针域的描述信息,也可能包含将对象自身标记并将其子节点添加到标记栈的代码。程序通常只使用有限的几种类型,并且个别类型的使用频率要远远大于其他类型,因此堆类型信息进行缓存的价值较高。对于将标记位放置在对象头部的策略,由于堆中对象在标记阶段往往只会被访问一次,所以硬件预取无法发挥功效。

2.1位图标记

面提到回收器可以通过在对象头部某个位或者字节的方式对垃圾进行标记,也可以用一个独立的位图来进行标记:位图中的每个位对应着每个可能分配对象的地址。位图可以只有一个,也可以有多个,比如每个内存块维护各自的位图。
位图标记通常仅适用于单线程环境,因为多线程同时修改位图存在较大的写冲突。相对于位图,实践中更常用的是字节图(byte-map),虽然占用的空间是前者的8倍,却可以解决写冲突问题。在实际中,如果将标记位保存在对象头部,会引起额外的复杂性。因为对象头一般存放赋值器共享的数据,如锁和哈希值。当标记线程和赋值器线程并发执行时可能会产生冲突。所以为了确保安全,一般标记位会占用对象头部额外的一个字,以便与赋值器共享的数据进行区分。
位图标记的优点:
- 标记位更加密集
- 过程只需要读取存活对象的指针域而不会修改任何对象
- 清扫器不会对存活对象进行任何读写操作,它只会在释放垃圾对象的过程中覆盖其某些域,例如将它连接到空闲链表上所以位图标记不仅可以减少内存中需要修改的字节数,而且减少了对高速缓存行的写入,进而减少需要写回内存的数据量

  • 减少回收过程的内存换页次数。许多证据表明,对象往往成簇出现,成簇死亡,而许多分配器往往也会将这些对象分配在相邻空间。好处:1.在位图/字节图中,每个位或者每个字节全部都被设置/清空标记位的情况经常出现,因此回收器可以批量读取/清空一批对象的标记位;2.通过位图可以更简单的判定某一个内存块中的所有对象是否都是垃圾,进而可能一次性回收整个内存块。
    混合标记策略:将每一个数据块与字节图中的一个字相关联,同时依然保留对象头部的标记位。当且仅当内存块中至少存在一个存活对象时,该内存块所对应的标记字节才会被设置。所以清扫器可以根据字节图快速的判断某一个内存块是否完全为空,进而整体回收。

2.2 懒惰清扫
懒惰清扫:标记过程的时间复杂度:O(L),L是堆中存活对象的数量。清扫过程中的时间复杂度是O(H),H是堆空间大小。H>L,所以我们会误认为清扫阶段的开销是整个标记-清扫开销的主要部分。但实际上,标记追踪阶段内存的访问模式是不可预测的,而清扫过程的可预测性则高的多,同时清扫一个对象的开销也比 追踪的开销小得多。
如何降低甚至消除清扫阶段的停顿时间?
基于2个特征:
1.一个对象一旦成为垃圾,它将一直是垃圾,不可能再次被赋值器访问;
2.赋值器不会访问对象的标记位。在回收过程中,回收器在将堆中所有存活对象标记完成之后,只是简单的将完全为空的内存块返还给分配器,同时将其他内存块添加到其所对应空间大小分级的回收队列中。**对于任何内存分配的请求,分配器首先从合适的空间大小分级中分配一个空闲槽,如果调用失败则调用清扫器执行懒惰清扫,即从该空间大小分级的回收队列中取出一个或者多个内存块进行清扫,直到满足分配要求。**也可能会出现没有内存块可供清扫或者是被清扫的内存块不包含任何空闲槽的情况。此时分配器就要尝试从更低级别的块分配器中获取新内存。但若无法获取新内存块,则必须执行垃圾回收

3.标记-清扫回收器需要考虑的问题:

  • 赋值器的开销:最简单的标记-清扫回收器不会给赋值器带来任何额外的读写开销,相比之下,引用计数器会带来显著的开销。分代回收器,并发回收器,增量回收器都要求赋值器在修改指针时通知回收器,但对于程序的整体执行时间而言,这部分的开销通常较小。
  • 使用懒惰清扫策略的回收器通常有较高的吞吐量,主要开销在于追踪阶段。对于已发现的垃圾对象,只需要设置一个标记位。相比之下,复制算法和标记-整理算法则还需要移动对象。
  • 空间利用率:如果将标记位放在对象头部,则不会产生额外的开销。如果使用位图,则额外空间取决于对象的字节要求,如要求32字节对齐,则总开销不会超过堆内存的1/32或1/64(一个32字节的对象用一个字节来标记,所以总占用空间是所有对象大小的1/32,也就是堆内存的1/32)。而复制式算法的利用率则较低。但非整理式回收器(如标记-清理)通常都会存在内存碎片,这对空间利用率也会造成一定的影响。

和其他追踪式回收算法一样,标记-清扫算法需要在回收所有死亡对象空间之前先确定所有的存活对象。这意味着追踪式回收器需要保留一定的空间来执行这项操作。若堆中存活对象较多,且分配速率较快,则会引起频繁的垃圾回收操作。对于中大型的堆,需要保留20%~50%的空间。

  • 移动还是不移动。非移动算法的优点在于,不移动对象的特征使得标记-清扫算法可以用于那些编译器和回收器不合作的场景。回收器无法获取到赋值器根集合和对象域的详细信息,所以不能随便移动对象。同时,出于安全考虑,在不合作的系统中保守式的回收器不能修改用户数据,包括对象头,所以用位图进行标记要比在对象头进行标记更好。但非移动算法的主要问题在于随着回收进行会产生内存碎片,所以会比整理式算法的回收频率更高。也需要保留20%~50%的空间,避免性能颠簸。因为会产生内存碎片,所以许多标记-清扫算法还会定期使用标记-整理算法进行内存整理。

    总结起来,标记-清扫分为标记和清扫2个过程,回收过程不会移动对象,因此容易产生内存碎片。针对2个过程,分别有优化的方法。标记过程的优化可以采用位图(字节图)进行标记的方法。清扫过程的优化可以采用懒惰清扫的策略。

部分参考:

https://blog.csdn.net/FoolishAndStupid/article/details/72571480

垃圾回收算法——标记—清扫回收算法相关推荐

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

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

  2. 【Android 内存优化】垃圾回收算法 ( 分代收集算法 | Serial 收集器 | ParNew 收集器 | Parallel Scavenge 收集器 | CMS 并发标记清除收集器 )

    文章目录 一. 分代收集算法 二. 垃圾回收器 / 收集器 ( GC ) 三. 串行收集器 ( Serial ) 四. ParNew 收集器 五. Parallel Scavenge 收集器 六. C ...

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

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

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

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

  5. Java教程分享:JVM垃圾回收机制之对象回收算法

    前言 在前面的文章中,介绍了JVM内存模型分为:堆区.虚拟机栈.方法区.本地方法区和程序计数器,其中堆区是JVM中最大的一块内存区域,在Java中的所有对象实例都保存在此区域,它能被所有线程共享. 在 ...

  6. 必知必会JVM垃圾回收——对象搜索算法与回收算法

    垃圾回收(GC)是JVM的一大杀器,它使程序员可以更高效地专注于程序的开发设计,而不用过多地考虑对象的创建销毁等操作.但是这并不是说程序员不需要了解GC.GC只是Java编程中一项自动化工具,任何一个 ...

  7. Java回收垃圾的基本过程与常用算法

    目录 一.基本概述 二.垃圾分类 基本背景 举例说明各种引用类型的作用 强引用(Strong Reference) 软引用(Soft Reference) 弱引用(Weak Reference) 虚引 ...

  8. JVM(三)GC垃圾回收以及四种GC算法

    JVM(三) 学习视频链接,以示尊重:https://www.bilibili.com/video/BV1iJ411d7jS?p=4 图片来源:https://blog.csdn.net/weixin ...

  9. 67.Java垃圾收集机制\对象引用\垃圾对象的判定\垃圾收集算法\标记—清除算法\标记—整理算法\分代收集\垃圾收集器\性能调优

    67.Java垃圾收集机制 67.1.对象引用 67.2.垃圾对象的判定 67.3.垃圾收集算法 67.3.1.标记-清除算法 67.3.2.标记-整理算法 67.3.3.分代收集 67.4.垃圾收集 ...

最新文章

  1. 学用 ASP.Net 之 字符串 (2): string.Format
  2. android:windowSoftInputMode属性
  3. Spring-配置bean的方法(工厂方法和Factorybean)【转】
  4. 别用这种方式聊天,你都不知道自己是怎么聊死的
  5. AJAX省市县三级联动的实现
  6. HTML+CSS+JS实现 ❤️3D万花筒图片相册展示特效❤️
  7. 自然语言处理 —— 2.3 词嵌入的特性
  8. 16 张图教你如何从 0 到 1 构建一个稳定、高性能的 Redis 集群!
  9. centos 安装gcc
  10. 【编程软件】keli自定义跳转函数及返回跳转原位置按键(附赠MDK525版本下载地址)
  11. 一、网络知识 1.计算机网络原理
  12. ZYNQ使用W25Q256问题笔记
  13. pdca实施的流程图_PDCA实战案例详解:PDCA的 4个阶段 8个步骤及应用详解
  14. 【CPRI】(2)组网方式及接口指标
  15. 老赵谈IL(2):CLR内部有太多太多IL看不到的东西,包括您平时必须了解的那些...
  16. 用CSS制造出光泽一闪而过的图片效果
  17. OPPO VIVO等多平台官方远程真机测试平台
  18. sap 新增科目表_SAP系统中四大科目表的总结
  19. 沙奎尔·奥尼尔——盘点那些“巨人”的事①
  20. 最大公约数(Java)

热门文章

  1. C语言 逗号运算(,)及其表达式
  2. 解决“Cradle project sync failed. Basic functionality(e.g.editing.debugging)”will not work properly
  3. adapter调用fragment中的方法
  4. There are no TAP-Windows adapters on this system. You should be able to create a TAP-Windows adapte
  5. 微信惊现任意代码执行漏洞 360手机卫士提供自检方案
  6. 代码随想录 Day04
  7. Kubernetes+SpirngCloud+SkyWalking实现链路追踪
  8. 【tyvj】P2065 「Poetize10」封印一击(贪心+线段树/差分)
  9. Xamarin 跨平台应用开发(5)——本地存储
  10. ijkplayer源码---seekTo