zoukankan      html  css  js  c++  java
  • 自动内存管理算法 —— 标记和复制法

    最近阅读了《垃圾回收算法手册》这本经典的书籍,借此机会打算写几篇内存管理算法方面的文章,也算是自己的总结吧。

                                                                                                                                                                        —— 题记

    自动内存管理系统

        自动内存管理主要面临以下三个方面的任务:

        1.为新对象分配内存空间

        2.确定“存活”对象

        3.回收“死亡”对象所占用的内存空间

    其中任务1一般称作“自动内存分配”(Memory Allocation ,下文简称 MA),任务2、3便是常说的“垃圾回收器”(Garbage Collect 即 gc ,下文简称GC)。一般来说,为了降低自动内存管理系统设计的复杂性和稳定性,一般会采用单线程设计方式,也就是说MA和GC不能同时进行,这样一来就解释一些托管代码在有大量GC的情况下程序卡顿的情况,因为在GC线程运行时,MA线程会等待,即托管代码暂停执行,直到GC结束,如果此时GC运行时间较长,那么程序卡顿的情况就会较为明显。

    标记回收法

        基本的垃圾回收策略主要有四种:标记-清扫、标记-整理、复制回收法、引用计数,这里把前两种归纳为标记法。标记法回收策略分为两个过程:标记和回收,其中标记阶段是指:从根集合开始用DFS搜索方式标记每个存活对象(即对象可达:从根集合开始可以找到该对象),回收是指:遍历堆,把未标记的对象(即不可达对象)当做垃圾回收。无论采用何种回收策略,一般复制器的工作流程是类似的,这里在先介绍一下赋值器的伪代码:

    New():
        ref <—— allocate()    //在堆中分配,可能会因内存碎片或者内存不足导致分配失败
        if ref = null 
            collect()    //执行回收策略
            ref = allocate()
            if ref = null 
                error "out of memory"
            return ref
    /*
        所有标记回收策略均满足如下范式
        先标记,后回收
        其中atomic是原子操作关键字,
        标识该方法以原子操作的方式执行完毕
    */
    atomic collect():
        markFromRoots()
        sweep(HeapStart , HeapEnd)

    一.标记-清扫算法

        标记-清扫是一种十分简单的回收策略,其中的标记阶段是标记法中通用的策略,这里的“清扫”只是回收的最简单实现。其主要思路:

    1.从“根集合(Roots)”开始,DFS遍历所有的对象,标记其是存活对象。【从Roots出发可以访问的对象可简单看做是可达对象,近似等于存活对象】

    2.线性遍历堆,回收未标记的对象。【未标记即不可达,可看做非存活对象】

    注:“Roots”是一个堆中的有限集合,复制器可以直接访问到,通常包括:静态、全局存储、线程本地存储,简单可以看成“对象图”中的入口对象集合

    下面贴出标记-清扫最简单的伪代码:

    //标记阶段
    markFromRoots():
        initList(workList)    //在DFS过程使用一个list作为缓冲
        for each fld in Roots //遍历所有Roots对象,然后由根对象DFS到每个可达对象,实现对存活对象的标记
            ref <- *fld
            if ref ~= null 
                setMarked(ref) //设置标记,这里有很多种实现,如写入对象头部,位图,字节图等
                add(workList)
                mark()
    initList():
        workList <- empty
    
    //常规的DFS算法
    mark():
        while not isEmpty(workList)
            ref <- remove(workList)
            for each fld in Points(ref)
                child <- *fld
                if child != null && not isMarked(child)
                    setMarked(child)
                    add(workList)
    
    ============================================
    
    //回收阶段 
    sweep(start , end):
        scan <- start
        while scan < end 
            if isMarked(scan)
                unsetMarked(scan)
            else 
                free(scan)
            scan = nextObj(scan)

         就像上面的伪代码只是标记-清扫最简单的描述,实际上它存在很多性能问题的,如时间局和空间局部性、无法高效利用高速缓存、容易缺页等,所以这里只做最简单的说明。

    二.整理法

        即使算法相对完备的“标记-清扫”回收策略也无法避免“内存碎片”问题,因为该算法在“清扫”过程中仅仅简单地遍历堆,直接释放“不可达对象”,这样一来必然造成内存碎片,了解操作系统的coder肯定清楚,一旦内存碎片过多是一件十分可怕的事情,很可能造成有内存却无法使用的情况,最终导致内存崩掉……所以这时候就出现了“标记-整理”回收策略,它分为“标记“和”“ 整理”连个部分,其中标记部分和“标记-清扫”算法一致,区别在于回收策略上。

        “标记-整理”法的整理过程有好几种算法,大都倾向于将存活对象整理到堆的某一端,这里介绍一种运用较为广泛的算饭“List 2”算法,该算法主要分为三部分:

    1.计算整理后“存活对象”在堆中对应的地址

    2.更新复制器的根及被标记对象的引用

    3.真正移动对象到其对应的新地址

    //“整理”算法的主体过程
    Compact():
        computeLocations(HeapStart , HeapEnd ,HeapStart) //计算整理后“存活对象”在堆中对应的地址
        updateReferences(HeapStart ,HeapEnd) //更新引用
        relocate(HeapStart , HeapEnd) //引动对象到最终位置
    
    computeLocations(start , end , toRegion):
        scan <- start
        free <- toRegion
        while scan < end //从头到尾扫描堆,找出被标记对象
            if isMarked(scan)
                forwardingAddress(scan) <- free //forwardingAddress(scan) 表示该对象的“转发地址”即整理后的地址,
                free <- free + size(scan)    //可能在该对象头信息中记录,也可能以字节图等形式记录        
            scan <- scan + size(scan)
    
    updateReferences(start , end):
        for each fld in Roots //更新根引用地址
            ref <- *fld
            if ref != null
                *fld <- forwardingAddress(ref)
        scan <- start
        
        while scan < end //扫描堆,更新其他对象信息
            if isMarked(scan)
                for each fld in Pointers(scan)
                    if *fld != null
                        *fld <- forwardingAddress(*fld)
            scan <- scan + size(scan)
    
    relocate(start , end):
        scan <- start
        while scan < end
            if isMarked(scan)
                dest <- forwardingAddress(scan)
                move(scan , dest) //真正移动对象
                unsetMarked(dest)  //去除记录的转发地址信息
        scan <- scan + size;
    “标记-整理”回收策略较大的问题在于执行效率,因为大都需要多次扫描堆,容易造成gc卡顿时间较长,再者类似list 2 这种用对象头部记录转发地址信息的方式,也在一定程度上造成空间浪费。

    三.复制式回收

        相对于“标记-整理”策略需要多次遍历堆进行回收,“复制式回收”只需要遍历一次堆,同时也清理了”内存碎片“,并保证了对象在堆中的相对顺序(提高了程序的空间局部性)。但是它有个致命的缺点是堆的可利用空间只有一半。下面是算法的主要思想:

    1.将堆空间平均分为两半(对象区和空闲区)

    2.遍历对象区,并把对象顺序移到空闲区

    3.遍历结束,对象被整理到空闲区,释放(即直接丢弃)原对象区

    atomic collect():
        flip()    //分割半区
        initList(workList)    //作为缓存栈
        for each fld in Roots
            process(fld)    //先处理根域
        while not isEmpty(workList) //处理worklist中对象
            ref <- remove(workList)
            scan(ref)
    //将堆平均分成两部分,假设HeapStart是存储数据的堆
    flip():
        extent <- (HeapEnd - HeapStart) / 2
        top <- HeapStart + extent
        free <- top
    
    //扫描给定对象的指针域
    scan(ref):
        for each fld in Pointers(ref)
            process(fld)
    
    //更新对象的指针地址
    process(fld):
        fromRef <- *fld
        if fromRef != null
            *fld <- forward(fromRef)
    //转移对象
    forward():
        toRef <- forwardingAddress(fromRef) //forwardingAddress 记录了对象的转移地址
        if toRef != null    //判断对象是否已经被转移
            toRef <- copy(fromRef)
        return toRef
    
    //将对象拷贝到堆的另一个半区
    copy(fromRef):
        toRef <- free
        free <- free + size(fromRef) //移动空闲指针
        move(fromRef , toRef)
        forwardingAddress(fromRef) <- toRef //记录转移地址
        add(workList , toRef)
        return toRef

    总结

        基本的基于”标记“的内存管理策略主要就是上面两种算法,其实优秀的内存管理策略肯定不会仅仅只使用某种单一策略,它们可能更倾向于多种策略同时使用,比如在内存充足时可能就直接使用“复制式回收策略”,内存不足时切换成“标记-回收策略”,当然每种算法都会有各种优化策略,基本就是基于“空间和时间局部性”做优化,可以很大程度提升回收器的效率,减少卡顿时间。

  • 相关阅读:
    正则与普通方法对字符串过滤的比较
    java基础练习笔记
    node.js-express路由基础+获取前端数据+rmvc架构开发
    解决powershell因为在此系统上禁止运行脚本"报错
    树、森林、二叉树的转换
    git提交代码时如何不提交node_modules文件
    node.js-静态资源目录搭建
    node.js路由基础
    sql server查询练习
    MYQL存储过程与事件
  • 原文地址:https://www.cnblogs.com/lixiang-share/p/5994843.html
Copyright © 2011-2022 走看看