操作系统内存垃圾回收技术

(整期优先)网络出版时间:2021-08-23
/ 2

操作系统内存垃圾回收技术

邵慧华

中移(杭州)信息技术有限公司 (310063)

摘要:操作系统内存垃圾回收是完成高效计算的重要组成部分。本文就操作系统的内存管理(垃圾回收)和虚拟机JVM垃圾回收需要考虑的实际因素和算法机制给出分析。

关键词: 操作系统;内存垃圾回收;算法机制讨论


一 内存组成原理和内存管理(垃圾回收)算法

操作系统将RAM分为内核空间和用户空间,内核空间上面都属于用户空间,用户空间是用户是可以操作的,内核空间是不允许用户接触的。内存层是CPU要使用的内核存储区,一般程序员无法直接控制。在程序文件区最底部存储程序代码和静态数据,在内存文件区和共享动态库区存储内存映射文件,在栈区存储方法参数,返回值地址,本地变量,引用地址。

在堆区存储显示分配的内存,比如C中的malloc()和java中new创建的对象[1]

内存管理主要是说堆内存管理,因为栈的内存总是自动管理的。目标是找到不再使用的对象,释放他们的空间以供再利用。内存管理分为手动管理和自动管理:其中手动管理比如:C语言用free();自动管理比如Rust使用region-based方式,而JAVA用garbage collection方式[2]。引用计数是最简单的自动内存管理方式,广泛使用在一些脚本语言,如Python,ruby等,也在一些小规模的系统编程中使用,每个对象都会分配一个额外的空间作为引用计数,记录有多少其它对象指向它,如果这个数为0 则说明没有指向他的reference则他就会清理。这种内存管理机制的优点是简单,内存在小范围内回收,容易理解内存回收和消耗;缺点是循环数据相互引用,从而不能移除造成内存泄漏,需要额外内存开销来存储引用、以及需要处理器时间来更新引用。


二 常见的几种内存分配机制原理

1 区域回收内存管理(Rust):通过ownership系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,ownership(所有权)系统的任何功能都不会减慢程序。

每个程序都使用一种或多种内存缓冲区、屏幕空间和网络资源连接、数据库资源等。事实上,在面向对象的环境中,每种类型都标识一些资源可供您的程序使用。要使用这些资源中的任何一个,都需要分配内存来表示类型。这个访问资源所需的步骤如下:

1.为表示资源的类型分配内存。

2.初始化内存以设置资源的初始状态并使资源可用。

3.通过访问类型的实例成员来使用资源(根据需要重复)。

4.拆掉资源状态进行清理。

5.释放内存。


2 region-based内存回收管理:当进程初始化时,运行时会保留一个连续的地址空间区域,该区域最初没有分配存储。此地址空间区域是托管堆。堆还维护一个指针,我称之为NextObjPtr。这指针指示堆中下一个对象的分配位置。最初,NextObjPtr被设置为保留地址空间区域。应用程序使用new操作符创建对象。此运算符首先确保新对象所需的字节放入保留区域(如有必要,提交存储)。如果对象匹配,那么NextObjPtr指向堆中的对象调用对象的构造函数,新操作符返回对象的地址。

在C运行时堆中,为对象分配内存需要浏览数据结构的链表。一旦找到足够大的块,就必须拆分该块,并在必须修改链表节点以保持所有内容完好无损。对于托管堆,分配对象只意味着添加指针的值。相比之下,这是非常快的。实际上,从托管堆分配对象的速度几乎与

从线程堆栈分配内存的速度一样快。到目前为止,托管堆的实现速度和简单性似乎远远优于C运行时堆。当然,托管堆获得了这些优势,因为它提出了一个非常大的假设:地址空间和存储是无限的。这种假设实际上是很荒谬的,因此托管堆必须采用一种机制允许堆进行此假设。这种机制称为垃圾收集器。让我们看看它是怎么工作的。

当应用程序调用new操作符来创建对象时,区域中可能没有足够的地址空间来创建对象

分配给对象。堆通过将新对象的大小添加到NextObjPtr来检测这一点。如果NextObjPtr超出地址空间区域,则堆已满,必须执行收集。

每次触发GC(n) Pause young(Concurrent Start) ,之后都会跟一个GC(n+1) Concurrent Cycle。Pause young(Concurrent Start) 和 Pause young(Normal) 一样,都是执行新生代的回收,从 Pause 可以看出来是 stop the world 的。Concurrent Cycle 是并发标记,是并发的,如果还没结束但出现了堆中剩余空间不足以分配,那么会执行立即中断并发标记,然后执行full gc,等 full gc 执行完了才能继续,不过一般这种情况并发继续时也都显示 abort 然后该次 gc 就结束了。G1的并发标记循环分五个阶段:

第一阶段:初始标记(Young Collection with Initial Mark),收集所有GC根(对象的起源指针,根引用),STW(Stop the world),在年轻代完成

第二阶段:根区间扫描,标记所有幸存者区间的对象引用。

第三阶段:并发标记(Concurrent Marking),标记存活对象。

第四阶段:重新标记(Remark),是最后一个标记阶段,STW(stop the world),很短,完成所有标记工作。

第五阶段:清除(Clean),回收没有存活对象的Region并加入可用Region队列

后续阶段:并发标记结束后,G1 也就知道了哪些区块是最适合被回收的,那些完全空闲的区块会在这这个阶段被回收。

如果这个阶段释放了足够的内存出来,其实也就可以认为结束了一次 GC,下一次还是会先回收年轻代。否则进入混合回收,会利用这次并发标记的结果,选择一些活跃度最低的老年代区块进行回收。(相当于 mixed 的并发标记阶段)


3 全堆栈整理回收

G1 的并发标记阶段(其余三个阶段都是 Stop the world的),G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。G1收集器上记忆集的应用其实要复杂很多,每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,同时每个 region 也都有一份Card Table,通过卡表来进行内存的划分(一般为512Bytes)。所以每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。即 RSet是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。

有一个专用于调用Finalize方法的特殊运行时线程。当freachable队列为空(通常是这样)时,该线程休眠。但是当条目出现时,这个线程会唤醒,从队列中删除每个条目,并调用每个对象的Finalize方法。因此,如果对象在freachable队列上,那么该对象是可访问的,而不是垃圾。简而言之,当对象不可访问时,垃圾收集器会将该对象视为垃圾。然后,当垃圾收集器将对象的条目从终结队列移动到可freachable队列时,该对象不再被视为垃圾,其内存也不会被回收。此时,垃圾收集器已完成对垃圾的标识。一些标识为垃圾的对象被重新分类为非垃圾。垃圾收集器压缩可回收内存和特殊运行时线程清空freachable队列,执行每个对象的Finalize方法。


参考文献

[1] 刘垚;陈明宇;阮元;消息式内存原型系统的设计与实现[A];第十九届计算机工程与工艺年会暨第五届微处理器技术论坛论文集[C];2015年

[2] 张广飞;侯锐;张科;常轶松;张柳航;李芳;;内存系统模型与性能分析[A];第十七届计算机工程与工艺年会暨第三届微处理器技术论坛论文集(下册)[C];2013年


邵慧华,男,汉族,浙江杭州富阳,1979.7,硕士,高级嵌入式研发工程师,中移(杭州)信息技术有限公司(310063),研究方向:操作系统