前言:  

  最近看了google的工程师写的一个非常简单的垃圾收集器,大概200多行C代码,感叹大牛总能够把复杂的东西通过很简单的语言和代码表达出来。为了增加自己的理解,决定把大牛的想法和代码分析一遍,与大家分享,顺便结合wikipedia,复习下GC的基本概念。

  相信大家在写程序的过程中都遇到内存管理的问题,诸如malloc/delete、new/free等,C/C++需要程序员主动进行内存的释放,即垃圾内存的回收,而像Java就提供了GC机制来自动进行垃圾回收。

一、垃圾与垃圾回收

  为什么需要进行垃圾回收呢?

  垃圾回收就是要让程序员感觉有“无限”的内存供他一直allocate,事实上计算机不会有无限的内存,这就需要将一些垃圾内存进行自动回收,已使得在“任一时刻”都有内存可用。

  这种自动回收机制除了解放了程序员外,对程序本身也有很多好处:

  1、野指针问题。这在C/C++中很常见,某块内存区域已经被释放掉或被重新分配,而其引用(指针)变量依然在被使用,往往带来很多难以预料的错误。

  2、内存泄漏。如果内存都有程序员来管理,若某块内存使用完了没有及时释放,很容易造成内存的泄漏。

  3、还有一些其他的诸如重复释放问题,即内存区域已被释放或另作他用,程序员又手动再次free。

  总之,GC机制解决了很多内存管理上的问题,很大程度上避免了人为产生的Bug。

  当然,GC机制也在一些场合也有一些问题:

  1、一个最大的问题就是,GC机制本身占用了系统资源,从而造成系统性能的下降。

  2、GC机制中,什么时候进行垃圾回收是不确定的,也就是说某一时刻会造成系统性能下降,这在一些诸如实时系统中是无法容忍的。

  什么是垃圾?

  在计算机中,垃圾内存是指之前被分配过,但不再使用的内存。这里又有一个问题,如何知道某个内存区域不再被使用。如果程序中不再有这块内存的引用,显然就可以说明这块内存不再会被使用到了。为了更好的说明不再使用的内存,我们先定义什么是使用中的内存:

  1、如果某个正在使用的变量引用了这块内存对象,说明这个对象正在使用;

  2、如果某个正在使用的内存对象引用了这块内存对象,说明这个对象也是正在使用的。

  [1、Any object that’s being referenced by a variable that’s still in scope is in use.

  2、Any object that’s referenced by another object that’s in use is in use.]

例如:

  1. class A{
  2. B b;
  3. ...
  4. }
  5. Class B{
  6. ...
  7. }
  8.  
  9. A a = new A();

  只要a正在使用中,那么所指向的那块空间就正在使用中,又由于A中有b对象,那么b所指向的空间也正在使用中(这其实是一条递归的定义方式,或者说是传递闭包)。换句话说,任何一个对象,只要能通过程序中的某个变量访问到(reachable),那么这个对象所占用的内存资源就是正在使用的,假设现在有一个对象之间的引用图,如果某块内存区域在这个图上是不可达的,那么这块内存区域就是不再使用的。

  总结来说,一个对象如果能被访问到,即是正在使用的(in-use),一般是下面两种情况:

  对象可达性定义:

  1、对象在调用栈中被引用(局部变量,参数等),或者作为全局变量引用。

  2、被1中的对象引用。

  总结下,GC的过程就可以归结为,a、找到不再使用的对象;b、回收其占用的内存资源。

  目前有很多方法来实现GC的这个过程。其中最简单的一种是Naïve mark-and-sweep。顾名思义,这种方法总共分为两步,对不再使用内存的标记和扫除。具体来说就是每个对象占用的内存都有一个标记。这个标记仅在GC执行时才会用到。在GC运行后,首先按照上述对对象可达性定义,将这个对象占用的内存的标记设为in-use状态(Mark),然后扫描内存区域,那些拥有标记位,但是没有被置为in-use状态的,说明就是需要被回收的(Sweep),整个过程结束后,再将所有的标记位重置,等待下一次回收过程。

三、自己动手实现一个简单的Garbage Collector

1、类型

  为了简化,我们这里只讨论两种类型,一种是不含嵌套的,类似于int、char这样的类型,一种是含有嵌套的,类似于class这样的类型。对于第二种类型,作者采用的是Pair,即<A,B>,其中A、B也可以是Pair类型(当然也可以是第一种普通的类型)。对象的类型表示如下:

  1. typedef enum {
  2. OBJ_INT,
  3. OBJ_PAIR
  4. } ObjectType;

  为了让不同类型的对象信息便于用一个统一的数据结构维护,我们又定义了Object类型,这个类型包含了具体的类型,和其所对应的值。

  1. typedef struct sObject {
  2. ObjectType type;
  3.  
  4. union {
  5. /* OBJ_INT */
  6. int value;
  7.  
  8. /* OBJ_PAIR */
  9. struct {
  10. struct sObject* head;
  11. struct sObject* tail;
  12. };
  13. };
  14. } Object;

  这里有一个trick(个人认为),就是采用了union这样的结构,实现的内存的overlapping。另外,值得注意的是,这里展示了Pair这个数据结构的定义,如前所述,这是一个递归的定义。

2、虚拟机

  前面已经提到,垃圾回收机制的实现,得益于其维护了一些中间信息,最最基本的中间信息便是a、程序总共分配了哪些内存构成的集合A;b、正在用的是哪些内存构成的集合B。有了这些信息,gc的过程就变得很简单,即sweep掉A-B的内存

  我们把这样的一个数据结构程序虚拟机,对于正在使用的对象集合,采用栈来维护(同样采用栈式结构的还有JVM),对于程序总共分配的内存集合,采用链表来维护。由于采用了链表,原来的Object结构体需要加上next指针,更新后的Object结构体如下:

  1. typedef struct sObject {
  2. ObjectType type;
  3.  
  4. /* The next object in the linked list of heap allocated objects. */
  5. struct sObject* next;
  6.  
  7. union {
  8. /* OBJ_INT */
  9. int value;
  10.  
  11. /* OBJ_PAIR */
  12. struct {
  13. struct sObject* head;
  14. struct sObject* tail;
  15. };
  16. };
  17. } Object;

  因此,虚拟机定义如下:

  1. #define STACK_MAX 256
  2.  
  3. typedef struct {
  4. Object* stack[STACK_MAX];
  5. int stackSize;
  6.  
  7. /* The first object in the linked list of all objects on the heap. */
  8. Object* firstObject;
  9. } VM;

  创建并初始化一个虚拟机的过程如下:

  1. VM* newVM() {
  2. VM* vm = malloc(sizeof(VM));
  3. vm->stackSize = ;
  4. vm->firstObject = NULL;
  5. return vm;
  6. }

  创建一个Object的过程如下:

  1. Object* newObject(VM* vm, ObjectType type) {
  2.  
  3. Object* object = malloc(sizeof(Object));
  4. object->type = type;
  5. object->next = vm->firstObject;
  6. vm->firstObject = object;
  7.  
  8. return object;
  9. }

  注意,这里创建一个Object仅是将其放到程序分配的内存集合,即链表中,还没有作为正在使用的对象加到栈中。

  对于正在使用中的对象的维护,对应的push和pop函数如下:

  1. void push(VM* vm, Object* value) {
  2. assert(vm->stackSize < STACK_MAX, "Stack overflow!");
  3. vm->stack[vm->stackSize++] = value;
  4. }
  5.  
  6. Object* pop(VM* vm) {
  7. assert(vm->stackSize > , "Stack underflow!");
  8. return vm->stack[--vm->stackSize];
  9. }

  对于不同的类型,根据上面实现的push函数,可以写出对应类型的push,如pushInt,具体如下:

  1. void pushInt(VM* vm, int intValue) {
  2. Object* object = newObject(vm, OBJ_INT);
  3. object->value = intValue;
  4.  
  5. push(vm, object);
  6. }
  7.  
  8. Object* pushPair(VM* vm) {
  9. Object* object = newObject(vm, OBJ_PAIR);
  10. object->tail = pop(vm);
  11. object->head = pop(vm);
  12.  
  13. push(vm, object);
  14. return object;
  15. }

  这里的pushPair是先单独push两个object,然后再取出来,构建一个新的Pair类型的object,重新push。(个人没明白为什么要这么做,感觉没必要)。

  到这里,我们已经拥有了程序运行时内存分配的信息,接下来就要开始进行mark-and-sweep了。

3、Mark

  很显然,我们需要一个Flag标志来区分哪些是in-use,哪些不是。因此,需要在原来的Object结构体中加上这样的标志位,更新后的Object结构体如下:

  1. typedef struct sObject {
  2. ObjectType type;
  3. unsigned char marked;
  4.  
  5. /* The next object in the linked list of heap allocated objects. */
  6. struct sObject* next;
  7.  
  8. union {
  9. /* OBJ_INT */
  10. int value;
  11.  
  12. /* OBJ_PAIR */
  13. struct {
  14. struct sObject* head;
  15. struct sObject* tail;
  16. };
  17. };
  18. } Object;

  同时还需要更新的是,在newObject函数中对mark位进行初始化:

  1. Object* newObject(VM* vm, ObjectType type) {
  2.  
  3. Object* object = malloc(sizeof(Object));
  4. object->type = type;
  5. object->next = vm->firstObject;
  6. vm->firstObject = object;
  7. object->marked = ;
  8.  
  9. return object;
  10. }

  有了这个标志位,我们就可以写mark函数了,mark过程有一个值得注意的就是,对于嵌套的数据结构,其内部子Object也需要进行mark,根据这样的原则,我们很容易写出如下代码:

  1. void mark(Object* object) {
  2. object->marked = ;
  3.  
  4. if (object->type == OBJ_PAIR) {
  5. mark(object->head);
  6. mark(object->tail);
  7. }
  8. }

  事实上这里有一个很明显的错误,即如果出现循环嵌套情况(A中有B,B中有A),mark过程就会一直执行下去,因此,需要一个边界条件来结束递归。边界条件便是,如果已经标记过了,就返回,更新后的代码如下:

  1. void mark(Object* object) {
  2. /* If already marked, we're done. Check this first
  3. to avoid recursing on cycles in the object graph. */
  4. if (object->marked) return;
  5.  
  6. object->marked = ;
  7.  
  8. if (object->type == OBJ_PAIR) {
  9. mark(object->head);
  10. mark(object->tail);
  11. }
  12. }

  对于栈中维护的object,可以用一个markAll来全部标记:

  1. void markAll(VM* vm)
  2. {
  3. for (int i = ; i < vm->stackSize; i++) {
  4. mark(vm->stack[i]);
  5. }
  6. }

4、Sweep

  Sweep过程很简单,遍历程序总共分配的对象链表,如果没有标记,就free掉这块内存,因为标记了的都是在栈中出现过的,如果标记过,就将其mark位复位,以备下次gc过程重新检查其是不是还在栈中,即是不是还需要标记。

  1. void sweep(VM* vm)
  2. {
  3. Object** object = &vm->firstObject;
  4. while (*object) {
  5. if (!(*object)->marked) {
  6. /* This object wasn't reached, so remove it from the list
  7. and free it. */
  8. Object* unreached = *object;
  9.  
  10. *object = unreached->next;
  11. free(unreached);
  12. } else {
  13. /* This object was reached, so unmark it (for the next GC)
  14. and move on to the next. */
  15. (*object)->marked = ;
  16. object = &(*object)->next;
  17. }
  18. }
  19. }

  执行完sweep过程后,我们就将所有unreasonable的内存全部回收!

  因此,一个完整的gc函数如下:

  1. void gc(VM* vm) {
  2. markAll(vm);
  3. sweep(vm);
  4. }

  你以为GC机制就这样完成了?其实还差那么一点,一个很重要的问题摆在我们面前,什么时候去调用这个gc函数?在GC的定义中,给出的是当low on memory的时候去调用,那low on memory又是一个什么概念?显然这又与具体的硬件配置有关。

5、gc的调用

  为了简化问题,我们采用一种非常naive的方法来触发gc函数(既然是可以让我们自己动手实现的,当然是越简单越好喽~),简单来说,就是设置一个对象数的上限,当超过这个上限时,就触发gc函数,这需要有虚拟机来维护。因此,我们在虚拟机里加入两个变量,一个是numObjects,表示当前分配的对象总数,另一个是maxObjects,表示上限。更新后的虚拟机如下:

  1. typedef struct {
  2. Object* stack[STACK_MAX];
  3. int stackSize;
  4.  
  5. /* The first object in the linked list of all objects on the heap. */
  6. Object* firstObject;
  7.  
  8. /* The total number of currently allocated objects. */
  9. int numObjects;
  10.  
  11. /* The number of objects required to trigger a GC. */
  12. int maxObjects;
  13. } VM;

  那么在初始化VM时,就需要同时对这两个值也进行初始化:

  1. VM* newVM() {
  2. VM* vm = malloc(sizeof(VM));
  3. vm->stackSize = ;
  4. vm->firstObject = NULL;
  5. vm->numObjects = ;
  6. vm->maxObjects = INITIAL_GC_THRESHOLD;
  7. return vm;
  8. }

  在创建一个对象时,就会判断下是否超过上限,如果超过,就执行gc函数,同时更新numObjects。更新后的newObject函数如下。

  1. Object* newObject(VM* vm, ObjectType type) {
  2. if (vm->numObjects == vm->maxObjects) gc(vm);
  3.  
  4. Object* object = malloc(sizeof(Object));
  5. object->type = type;
  6. object->next = vm->firstObject;
  7. vm->firstObject = object;
  8. object->marked = ;
  9.  
  10. vm->numObjects++;
  11.  
  12. return object;
  13. }

  对于先前的sweep函数,在释放掉一块unreasonable内存后,也要更新numObjects。更新后的sweep函数如下:

  1. void sweep(VM* vm)
  2. {
  3. Object** object = &vm->firstObject;
  4. while (*object) {
  5. if (!(*object)->marked) {
  6. /* This object wasn't reached, so remove it from the list and free it. */
  7. Object* unreached = *object;
  8.  
  9. *object = unreached->next;
  10. free(unreached);
  11.  
  12. vm->numObjects--;
  13. } else {
  14. /* This object was reached, so unmark it (for the next GC) and move on to
  15. the next. */
  16. (*object)->marked = ;
  17. object = &(*object)->next;
  18. }
  19. }
  20. }

  那么,现在又有一个问题,上限值应该设为多少合适呢?不同的程序会有不同的内存要求,设置太大了,对于对内存消耗比较低的程序,可能就根本不会触发gc,设置过小了,对于内存消耗比较大程序就会频繁触发gc,导致性能下降。因此,上限值需要动态更新,这里更新的原则是,每次执行gc后,链表中没有被free掉的对象数的两倍。更新后的gc程序如下:

  1. void gc(VM* vm) {
  2. int numObjects = vm->numObjects;
  3.  
  4. markAll(vm);
  5. sweep(vm);
  6.  
  7. vm->maxObjects = vm->numObjects * ;
  8.  
  9. printf("Collected %d objects, %d remaining.\n", numObjects - vm->numObjects,
  10. vm->numObjects);
  11. }

  至此,一个简单的GC程序便实现了!(想想还真有点小激动呢~)

后记:

  本文相当于一篇读后感吧,包括各种博客、代码等,一是为了梳理思路,了解相关知识(在这之前我还真没关注过GC的过程);二是分享给那些跟我一样对GC不太明白甚至心生畏惧的同学,一个简单的GC就是这么容易地实现了,200多行哦;三则是为了抛砖引玉,我水平有限,尤其对这些有点偏底层偏细节的东西理解不深,如有错误,还望指出!如果您觉得对您有帮助,不要忘了推荐哦~

参考资料:

1、大牛的博客:http://journal.stuffwithstuff.com/2013/12/08/babys-first-garbage-collector/

2、大牛的github:https://github.com/zhujiangang/mark-sweep/blob/master/main.c

3、维基百科:http://en.wikipedia.org/wiki/Garbage_collection_(computer_science)

[GC]一个简单的Garbage Collector的实现的更多相关文章

  1. 一个简单的Garbage Collector的实现

    一个简单的Garbage Collector的实现 前言: 最近看了google的工程师写的一个非常简单的垃圾收集器,大概200多行C代码,感叹大牛总能够把复杂的东西通过很简单的语言和代码表达出来.为 ...

  2. 提交并发量的方法:Java GC tuning :Garbage collector

    三色算法,高效率垃圾回收,jvm调优 Garbage collector:垃圾回收器 What garbage? 没有任何引用指向它的对象 JVM GC回收算法: 引用计数法(ReferenceCou ...

  3. k8s garbage collector分析(2)-处理逻辑分析

    garbage collector介绍 Kubernetes garbage collector即垃圾收集器,存在于kube-controller-manger中,它负责回收kubernetes中的资 ...

  4. Getting Started with the G1 Garbage Collector(译)

    原文链接:Getting Started with the G1 Garbage Collector 概述 目的 这篇教程包含了G1垃圾收集器使用和它如何与HotSpot JVM配合使用的基本知识.你 ...

  5. k8s garbage collector分析(1)-启动分析

    k8s garbage collector分析(1)-启动分析 garbage collector介绍 Kubernetes garbage collector即垃圾收集器,存在于kube-contr ...

  6. 使用MongoDB和JSP实现一个简单的购物车系统

    目录 1 问题描述  2 解决方案  2.1  实现功能  2.2  最终运行效果图  2.3  系统功能框架示意图  2.4  有关MongoDB简介及系统环境配置  2.5  核心功能代码讲解  ...

  7. 从一个简单的Java单例示例谈谈并发

    一个简单的单例示例 单例模式可能是大家经常接触和使用的一个设计模式,你可能会这么写 public class UnsafeLazyInitiallization { private static Un ...

  8. 从一个简单的Java单例示例谈谈并发 JMM JUC

    原文: http://www.open-open.com/lib/view/open1462871898428.html 一个简单的单例示例 单例模式可能是大家经常接触和使用的一个设计模式,你可能会这 ...

  9. AGC027 B - Garbage Collector 枚举/贪心

    目录 题目链接 题解 代码 题目链接 AGC027 B - Garbage Collector 题解 对于一组选取组的最优方案为,走到一点,然后顺着路径往回取点 设选取点坐标升序为{a,b,c,d} ...

随机推荐

  1. 明晰三种常见存储技术:DAS、SAN和NAS

    随着企业网络应用的时间和应用的数据量的加大,企业已经感觉到存储容量和性能落后与网络的应用发展需求,特别是流媒体企业,在这种应用条件下满足用户的存储需求的技术应用诞生,DAS.NAS和SAN三种存储技术 ...

  2. RE:转:一些不常用的html代码

    1. oncontextmenu="window.event.returnvalue=false" 将彻底屏蔽鼠标右键<table border oncontextmenu= ...

  3. fiddler插件开发step by step 1

    Fiddler 是优秀的抓包工具,有着众多的优秀插件.Fiddler 软件是由C#语言开发的,运行在.net Framework 框架之上,所以我们也可以使用vs来开发自己的Fiddler插件,下面就 ...

  4. poj 2265 Bee Maja

    题目的意思很容易理解.就是找两个不同坐标的对应关系.下面的思路转自POJ的论坛 首先,记由1到2的方向记为2,1到3的方向记为3……1到7的方向记为7,他们分别是:(0,1),(-1,1),(-1,0 ...

  5. SPRING IN ACTION 第4版笔记-第九章Securing web applications-001-SpringSecurity简介(DelegatingFilterProxy、AbstractSecurityWebApplicationInitializer、WebSecurityConfigurerAdapter、@EnableWebSecurity、@EnableWebMvcS)

    一.SpringSecurity的模块 At the least, you’ll want to include the Core and Configuration modules in your ...

  6. Altium designer中级篇-名称决定多边形连接样式

    在工作中积累了诸多小技巧,可以让工作变的更简单,就比如这个多边形铺铜,与大部分规则的不同之处在于,通过更改多边形的名称,就能达到控制多边形规则的效果.这样多边形铺铜变的及其灵活,下面将对这个经验做一个 ...

  7. arcgis 10.2 安装教程

    arcgis 10.2 安装教程(含下载地址)_百度经验 http://jingyan.baidu.com/article/fc07f98911b66912ffe5199b.html arcgis 1 ...

  8. LoadImage 和 BitBlt

    #include <windows.h> #define WINDOWCLASS TEXT("Test") #define WNDTITLE TEXT("Te ...

  9. 启用了不安全的HTTP方法

    安全风险:       可能会在Web 服务器上上载.修改或删除Web 页面.脚本和文件. 可能原因:       Web 服务器或应用程序服务器是以不安全的方式配置的. 修订建议:       如果 ...

  10. Java SE7新特性之try-with-resources语句

       try-with-resources语句是一个声明一个或多个资源的 try 语句.一个资源作为一个对象,必须在程序结束之后随之关闭. try-with-resources语句确保在语句的最后每个 ...