神之门V8(2):GC的混乱之治(下)

在上一篇文章中,分析了v8heap中新生带的垃圾回收算法,本文就主要介绍老生带的回收算法,本人自己用C++模拟了一下GC的在老生带的回收算法,接下来会结合代码以及进行分析,由于是我自己写的代码,我肯定会有属于自己的思想在里面,所以个别的细节可能跟V8GC真实的状态有所差异,但总体的思想还是基本体现了,我们说研究这些引擎的设计主要在于感悟这些内核设计的思想,甚至是艺术,所以我的主要宗旨还是以主要设计思想为主,细节的实现有的会按照我自己的想法进行编写。

ok,首先我们想老生带采用什么算法呢,新生代采用的是Scavenge算法,那老生带是不是可以照搬过来呢,设计的时候肯定会这么想,然而老生带有个特点,很不幸,它很大。。。在32位系统中。为700M,64位是1.4G。你想一下,我们说新生代采取from to分区并进行内存搬移完成compact以及对象图的遍历的前提是这个区足够小,所以进行内存的清除以及全部的重写消耗不是特别大,但对于700M甚至两倍的如此巨大的内存,你要进行这样的copy,你愿意内存都叫苦连天啊,所以,算法是需要改进的。

又参考jvm,GC是先进行全局的扫描,标记,在清理,最后进行内存的整合,多数为将指针添加进空闲内存链表,所以是不是可以考虑v8堆也进行一次标记的过程呢,标记肯定得有对老生带所有对象的一次遍历扫描,老生带的指针区跟新生代的from区对象布局是类似的,我们也是可以把它抽象为一个有向图,因为没有办法进行内存拷贝,所以只能老老实实的构造辅助队列了,先看看大概的结构示意图,

这里写图片描述

假如我们V8heap老生带指针区对象的分布就是这个样子,那么GC做的第一件事就是进行mark,也就是需要图的广度优先遍历了,上图中,难免会出现指针回指的现象,毕竟不是树这种递归结构,有可能在添加对象到队列中这个对象已经被处理(已经在辅助队列中了,或者已经从队列中离开了,即她的邻接点已经全部添加到队列中了,已经没他什么事了),这三种状态我们不能一股脑都塞进队列中,所以三中状态嘛,我就用三种不同的染色,对于GC还没有发现的对象,就是白色对象,所以刚开始的状态在GC工作之前,所有的对象都是白对象,对于GC已经发现的对象,就染色成为灰色对象,染色之后就会把这个对象放进队列等待自身邻接点的处理,最后当轮到处理他的邻接点点了,即把它所有的邻接点指针指向的对象都放进队列中,这个对象就可以说被GC完成了一切工作,他应该被染成黑色,意味着一切都结束了,下面来看看我的代码实现这个过程:

gc_status myGC::GCMark(Handle<GCObj>* root)
{
    //根节点已在队列中
    _ASSERT(root != NULL);
    //染色
    //GC发现
    root->state = 1;
    _ASSERT(gc_ref_queue != NULL);
    //push into queue
    std::vector<Handle<GCObj>*>::iterator itor;
    for (itor = root->refs.begin(); itor != root->refs.end(); itor++)
    {
        if ((*itor)->state == 0)
        {
            //边界检测
            //越界指针对象(可能在新生代)
            if ((*itor)->ptr >= (void*)_GC_HEAP_OLD_LTH)
            gc_ref_queue->gc_enQueue(*itor);
        }
    }
    //GC已处理,根节点变为黑色
    root->state = 2;
    GC_Obj_Queue_Node* qnode = gc_ref_queue->gc_deQueue();
    //空队列,则GC标记完成
    if (gc_ref_queue->isEmpty())
    {
        gc_send_msg_v8core();
        return true;
    }
    //遍历队列,逐个节点寻找指向其他heap中的对象
    //递归
    return GCMark(qnode->next->val);
}

代码实现的基本跟我上面说的类似,首先从一个根对象进行入手,根对象可以通过scope实例进行关联,毕竟被scope实例指向的对象都是根对象,我用一个vector结构来进行所有指向其他对象指针的存储,迭代遍历push进队列,中间加入边界检测,判断指向的对象到底在哪里,其实在新生代有一种可能是比较常见的,就是有那么一个奇怪的对象,它被一个老生带指针区的一个指针指向,那这样来说,它还是有价值的,但在from区,却没有对象认同他的价值,真有点悲惨,GC怎么判断他是活对象呢,所以得有附加信息,对于像F对象这种,需要有一张信息表对所有跨区指向的指针进行注册,这样GC在搜啊秒对象时就会进行表的核查,将这种奇怪的对象进行赦免,同样进行age的增加以及内存搬移到to区(这就是写屏障),所以我这里在老指针区进行边界检测就是为了发现这种跨区的指针然后把它注册进跨区指针信息表,我这里的堆内存是手动划分了边界指针。

最后的判定条件就是队列空了,前一次往队列加入了邻接点队列还是空,那说明已经没有任何的邻接点可以处理了,很有可能全部的对象都已经变成了黑对象,当然只是可能,GC不能猜 啊,这万一每次都猜错就是严重的内存泄露了,所以还需要进行内存残留检测,即进行全局的内存扫描,上面是通过递归进行下一个队列指针的邻接点遍历的,先来看看内存扫描的代码:

///扫描heap中对象进行GC标记残留检测和死对象清除
gc_status myGC::gc_xTravel_heap(gc_heap_ptr paddr)
{
    //对象边界检测
#ifdef _DEBUG
    std::cout << "gc scaning heap...\n" << std::endl;
#endif
    bool is_prev[2] = { false, false };
    while (paddr < (void*)_GC_HEAP_OLD_HTH)
    {
        //取出当前内存的前两位判断状态
        //GC是否标记,检查GC标记残留
        //检索内存页的位图
        int idx = (char*)paddr - (char*)_GC_HEAP_OLD_LTH;
        if (idx < 0 || idx > _GC_MAP_MAX)
            throw std::invalid_argument("runtime exception:invail scan pointer\n");
        //白节点(死对象检测)
        if (_gc_heap_status_map[idx] & 0xC0 == 0x80)
        {
                //对象头指针加入空闲链表
            if (!is_prev[0])
            {
                gc_sweep_heap_obj(&paddr);
                //修改了指针
                is_prev[0] = true;
                continue;
            }
            is_prev[1] = false;
        }
        //灰对象检测
        if (_gc_heap_status_map[idx] & 0xC0 == 0x40)
        {
            if (!is_prev[1])
            {
                //灰对象继续进行map遍历
                gc_remain_obj_handler(paddr);
                is_prev[1] = true;
            }
            is_prev[0] = false;
        }
        else
        {
            is_prev[0] = false;
            is_prev[1] = false;
        }
        //扫描指针移动一个字
        char* p = (char*)paddr;
        p += 0x02;
        paddr = p;
    }
}

在这一段全扫描,我的设计时让扫描指针逐字扫描,毕竟一个对象还是不小的,按字节扫描实在对于循环太耗时了,内存中是不可能含有对象的染色情况的,所以又是按照上面的想法,我们还是得建立一个信息映射表,将内存每一个字的地址跟当前所在对象范围的染色情况进行映射,因为有三个状态,所以我用一个字节的char类型取高三位进行染色的标记,这个是用一个位图形式存储,这里采用一个大数组,每一个内存页都要有对应的map进行字的状态标识,而且可以通过地址的偏移量进行数组索引,如我的代码的判别部分,这里有个重要的环节还是边界检测,知道一个对象的指针还不够,还得知道这个对象的类型,也就是她在内存区的存在范围,就是同一种颜色分布的最大范围,不管是进行灰对象残留处理还是白对象的链表回收,都是需要进行边界检测的,边界检测我用了一个布尔数组进行存储,通过不同色区的修改来进行边界的判定,

这里写图片描述

基本的原理我的代码已经写得比较清楚了,这里就不多说废话了,这样扫描指针就知道当前这个字到底是对象的一部分还是一个新对象的开始区域,如果我在里面添加移位计数,就可以计算出当前对象的大小了,可以通知V8主模块,不过我这里考虑到很多内存的分配都可以通过空闲进行分配,所以空闲链表会有分配过的对象的指针的记录,进行一次指针的查找也是可以得到这个对象的真实身份,不过对于大型的链表,进行一次O(n)的遍历也会导致性能的低下,要么就哈希表的O(1)查找,这都是设计的几种可以考虑的方案,扫描过后,灰对象就被仍会GC进行另外一次的附加标记,即残留的灰对象再组成有向图进行mark过程, 如此循环,一轮又一轮,知道整个空间黑白分明,然后白对象(没有被指向的对象)就被清理掉,加入空闲链表,在真实的V8中,有的算法还进行对象的整理,即将所有的活对象(黑对象,或者不加处理的灰对象往区域的上边界(高地址)移动),不过这么大的一个区域进行内存按位搬移对性能的影响也是颇大的,这里我就没做内存的搬移。

问题来了,为什么会有灰对象残留呢,按我上面的图最后的结果应该是黑白配啊,但假如我的队列是有容量的呢,对象的数量是很多的,这里不像from to转换,都是在本来的内存上做文章,这可是个辅助结构,如果链式队列没有上界,很有可能它的大小跟老生带的大小差不多了,而且是个辅助结构,这样对于内存的管理就有点得不偿失了,搞不好内存溢出,所以,一般,队列还是要设定一个长度,没必要那么急一次干完嘛,为了节约内存,我只能在这么大的空间内分好几次完成整个GCmark了,队列如果超过一定的容量,灰对象就不会再加入到队列中了,这时候才有灰对象的残留。

到现在整个老生带回收GC的算法过程已经分析完毕,v8中还是以标记清除即我介绍的这个算法为主,当内存的碎片化是在太严重已经无法容纳从新生代晋升过来的指针时才会进行对象压缩,内存的大规模移位,不能一条路走到死嘛。

V8当然有自己读到的地方,比如做了一些优化,GC的线程优先级是很低的,没什么地位,所以很容易就被其他线程占用cpu的时间片,所以她的GC事件是一个全世界为她让路的时间,其他的任务停下来等待GC完成自己的工作,所以这回导致GC的压力很大,效率太低整个系统都得等它,所以,V8比较人性化,让V8有一个增量断点,标记没必要一下子完成,中间可以插入一些更急的任务,这有点像我们下载文件的断点续传,不仅这样,在真正的清理过程中GC也是不一定一下子全部把白对象添加到回收链表,而是看需求,有一个对象待分配我就把内存释放到够分配这个对象的程度位置,多一点点我都懒得干了,当然大的对象当然就得进行比较彻底的回收了,这就是所谓的惰性清理,这也使得V8更加高效的 完成内存的高频分配跟回收,怪不得性能敢号称天下第一。

关于V8的GC,这里就算全部分析完毕了,主要的代码会出现在我的github上,欢迎commit,ok,本次分享到此结束~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
`v8::FunctionCallbackInfo<v8::Value>` 是 V8 JavaScript 引擎中的一个类,用于在 C++ 中定义与 JavaScript 函数的回调交互。它提供了访问函数参数、返回值等信息的能力。 一般情况下,你可以按照以下步骤使用 `v8::FunctionCallbackInfo<v8::Value>`: 1. 创建一个静态函数,作为 JavaScript 函数的回调函数。函数的签名应该是 `void Callback(const v8::FunctionCallbackInfo<v8::Value>& info)`。 2. 在回调函数中,使用 `info` 参数来访问函数的输入和输出。可以通过以下方法获取信息: - `Length()`:获取函数的参数个数。 - `operator[]`:通过索引访问参数。可以使用 `v8::Local<v8::Value>` 类型的对象来表示参数值。 - `GetReturnValue()`:获取返回值对象,可以使用它来设置要返回给 JavaScript 的值。 下面是一个简单的示例,演示了如何使用 `v8::FunctionCallbackInfo<v8::Value>`: ```cpp #include <v8.h> void MyFunctionCallback(const v8::FunctionCallbackInfo<v8::Value>& info) { v8::Isolate* isolate = info.GetIsolate(); // 获取参数个数 int numArgs = info.Length(); // 访问参数值 if (numArgs > 0) { v8::Local<v8::Value> arg = info[0]; // 对参数值进行处理... } // 设置返回值 v8::Local<v8::Value> returnValue = v8::String::NewFromUtf8(isolate, "Hello, World!"); info.GetReturnValue().Set(returnValue); } ``` 上述示例中的 `MyFunctionCallback` 函数可以在 JavaScript 中作为回调函数使用。它接受任意数量的参数,并返回一个字符串 "Hello, World!"。 希望这个解答对你有帮助!如果你还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值