虚拟存储器

#include <sys/mman.h> void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset); 返回:若成功时则为指向映射区域的指正,若出错则为MAP_FAILED(-1).

参数解释:

fd,start,length,offset:

mmap函数要求内核创建一个新的虚拟存储器区域,最好是从地址start开始的一个区域,并将文件描述符fd指定的对象的一个连续的片chunk映射到这个新的区域。

  • 连续对象片大小为length字节
  • 从据文件开始处偏移量为offset字节的地方开始。
  • statr地址仅仅是个暗示
    • 一般被定义为NULL,让内核自己安排。
prot

参数prot包含描述新映射的虚拟存储器区域的访问权限位。(对应区域结构中的vm_prot位)

  • PROT_EXEC:这个区域内的页面由可以被CPU执行的指令组成。
  • PROT_READ:这个区域内的页面可读。
  • PROT_WRITE: 这个区域内的页面可写。
  • PROT_NONE: 这个区域内的页面不能被访问。
flag

参数flag由描述被映射对象类型的组成。

  • MAP_ANON标记位:映射对象是一个匿名对象
  • MAP_PRIVATE标记位:被映射对象是一个私有的,写时拷贝的对象。
  • MAP_SHARED标记位:被映射对象是一个共享对象。
例子

bufp = mmap(NULL,size,PROT_READ,MAP_PRIVATE|MAP_ANON,0,0);

  • 让内核创建一个新的包含size字节的只读,私有,请求二进制零的虚拟存储区域。
  • 如果调用成功,那么bufp包含新区域地址。
munmap函数删除虚拟存储器的区域:

9.9 动态存储器分配

虽然可以使用更低级的mmapmunmap函数来创建和删除虚拟存储器的区域。

但是C程序员还是觉得用动态存储器分配器(dynamic memory allocator)更方便。

  • 动态存储器分配器维护着一个进程的虚拟存储区域,称为堆(heap)
    • 系统之间细节不同,但是不失通用型。
    • 假设
      • 是一个请求二进制零的区域。
      • 紧接着未初始化的bss区域,并向上生长(向更高的地址)。
      • 对于每个进程,内核维护一个变量brk(break),指向堆顶。
  • 分配器视为一组不同大小的块block的集合来维护。
    • 每个块就是一个连续的虚拟存储器,即页面大小。
    • 要么是已分配,要么是空闲
      • 已分配
        • 已分配的块显式地保留供应用程序使用。
        • 已分配的块保持已分配状态,直到它被释放
          • 这种释放要么是应用程序显示执行。
          • 要么是存储器分配器自身隐式执行(JAVA)。
      • 空闲
        • 空闲块可用于分配。
        • 空闲快保持空闲,直到显式地被应用分配。
  • 分配器有两种基本分格。
    • 都要求应用显式分配。
    • 不同之处在于那个实体负责释放已分配的块。
    • 显式分配器(explict allocator)
      • 要求应用程序显式释放
      • C语言中提供一种叫malloc程序显示分配器。
        • mallocfree
      • C++
        • newdelete
    • 隐式分配器(implicit allocator)
      • 要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。
      • 隐式分配器又叫做垃圾收集器(garbage collector).
        • 自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection).
      • Lisp,ML以及Java等依赖这种分配器。

本节剩余的部分讨论的是显示分配器的设计与实现。

9.9.1 malloc和free 函数

malloc

C标准库提供了一个称为malloc程序包的显示分配器

#include<stdlib.h>
void* malloc(size_t size);
                    返回:成功则为指针,失败为NULL
  • malloc 返回一个指针,指向大小为至少size字节的存储器块。
    • 不一定是size字节,很有可能是48倍数
      • 这个会为可能包含在这个内的任何数据对象类型做对齐
      • Unix系统用8字节对齐。
    • malloc不初始化它返回的存储器。
      • 如果想要初始化,可以用calloc函数。
        • callocmalloc一个包装函数。
    • 想要改变已分配块大小。
      • realloch函数
  • 如果malloc遇到问题。
    • 返回NULL, 并设置errno
  • 动态存储分配器,可以通过使用mmapmunmap函数,显示分配和释放堆存储器。
    • 或者可以使用sbrk函数。

      #include<unistd.h>
      
      void *sbrk(intptr_t incr);
      
                          返回:若成功则为旧的brk指针,若出错则为-1,并设置errno为ENOMEML.
      • sbrk函数通过将内核的brk指针增加incr(可为负)来收缩和扩展堆。
free

程序通过调用free函数来释放已分配的堆块。

#include<stdlib.h>

void free(void *ptr);
                返回:无
  • ptr参数必须指向一个从malloc,calloc,realloc获得的已分配块的起始位置。
    • 如果不是,那么free行为未定义。
    • 更糟糕的是,free没有返回值,不知道是否错了。

这里的字=4字节,且malloc是8字节对齐。

9.9.2 为什么要使用动态存储器分配

程序使用动态存储器分配的最重要原因是:

  • 经常直到程序实际运行时,它们才知道某些数据结构的大小。

9.9.3 分配器的要求和目标

约束

显式分配器有如下约束条件

  • 处理任意请求序列。
  • 立即响应请求。
    • 不允许为提高性能重新排列或缓冲请求。
  • 只使用
  • 对齐
    • 上文的8字节。
  • 不修改已分配的块。
目标

吞吐率最大化存储器使用率最大化。这两个性能要求通常是相互冲突的。

  • 目标1:最大化吞吐率
    • 假定n个分配和释放请求的某种序列R1,R2,R3.....Rn
      • 吞吐率 :每个单位时间完成的请求数。
    • 通过使分配和释放请求平均时间最小化 来最大化吞吐率
  • 目标2:最大化存储器利用率
    • 设计优秀的分配算法。
    • 需要增加分配和释放请求的时间

    • 评估使用的效率,最有效的标准是峰值利用率(peak utilization)

      • 假定n个分配和释放请求的某种序列R1,R2,R3.....Rn
        • 有效载荷(payload):如果一个应用程序请求一个p字节的块,那么得到的已分配块有效载荷p字节。(很有可能会分配p+1个字节之类的)
        • 聚集有效载荷(aggregate payload):请求Rk完成之后,Pk表示当前已分配块的有效载荷之后。又叫做聚集有效载荷
        • Hk表示堆的当前的大小(单调非递减的)。
      • 峰值利用率为Uk

  • 吞吐率存储器利用率是相互牵制的,分配器设计的一个有趣的挑战就是在两者之间找到一个平衡。

9.9.4 碎片

造成堆利用率很低的主要原因是一种称为碎片(fragmentation)的现象。

  • 碎片:虽然有未使用的存储器但不能满足分配要求时的现象。
    • 1.内部碎片:已分配块比有效载荷(实际所需要的)大时发生。
      • 比如:上文中只要5个字(有效载荷),却给了6个字(已分配块),那一个多的就是碎片.
      • 任何时刻,内部碎片的数量取决于以前请求的模式和分配器的实现方式。
        • 可计算的,可量化的。
    • 2.外部碎片:当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以处理这个请求发生的。
      • 外部碎片的量化十分困难。
        • 不仅取决于以前请求的模式和分配器的实现方式,还要知道将来请求的模式。
      • 优化: 需要启发式策略来用少量的大空闲块替换大量的小空闲块。

9.9.5 实现问题

一个实际的分配器要在吞吐率利用率把我平衡,必须考虑一下几个问题。

  • 空闲块组织: 如何记录空闲块? (对应 9.9.6)
  • 放置: 如何选择一个合适的空闲快来放置一个新分配的块? (对应 9.9.7)
  • 分割: 将一个新分配的块放入某个空闲块后,如何处理这个空闲快中的剩余部分?(对应9.9.8)
  • 合并: 我们如何处理一个刚刚被释放的块

9.9.6 隐式空闲链表(老本行了)

堆块(十分巧妙的利用了本该永远为0的低三位):

  • 一个由一个字的头部有效载荷,以及可能的填充组成。
    • 头部:编码了这个的大小(包括头部和填充),以及这个是否分配。
      • 假设是8字节对齐约束条件
        • 那么头部低三位一定是0
        • 所以释放低三位来表示一些其他信息。
          • 即块大小还是能表示0~2^32(只是必须是8的倍数),非0~2^29
          • 低三位就能表示是否分配之类的信息。

组织为一个连续的已分配块空闲块的序列。

这种结构就叫做隐式空闲链表

  • 隐式 :
    • 为什么叫隐式链表
      • 因为不是通过指针(next)来链接起来。
      • 而是通过头部的长度隐含地链接起来。
    • 终止头部(类似与普通链表的NULL)
      • 已分配,大小为的块
  • 优缺点:
    • 优点:简单
    • 缺点1:任何操作的开销都与已分配块和空闲块的总数呈线性关系O(N).
      • 放置分配的块。
      • 对空闲链表的搜索。
    • 缺点2: 即使申请一个字节,也会分配2的块。空间浪费。

9.9.7 放置已分配的块

当应用请求k字节的块,分配器搜索空闲链表,查找一个足够大可以放置请求的空闲块。

有一下几种搜索放置策略

  • 首次适配
    • 从头开始搜索空闲链表,选择第一个合适的空闲块。
  • 下一次适配
    • 首次适配很类似,但不是从头开始,而是从上一次查询的地方开始。
  • 最佳适配
    • 检查每个空闲块,找一个满足条件的最小的空闲块(贪心)。

优缺点

  • 首次适配
    • 优点
      • 往往将大的空闲块保留在链表后面。
    • 缺点
      • 小的空闲块往往在前面,增大了对较大快的搜索时间。
  • 下一次适配
    • 优点
      • 速度块。
    • 缺点
      • 存储器利用率
  • 最佳适配
    • 优点
      • 利用率高
    • 缺点
      • 要完整搜索链表,速度慢。
    • 后面有更加精细复杂的分离式空闲链表

9.9.8 分割空闲块

两种策略

  • 占用所有空闲块
    • 缺点:产生更多的内部碎片(但是如果内部碎片很少,可以接受)
    • 优点:能使得 空闲块+已分配块的数量减少
      • 能加快搜索速度
      • 有的外部碎片(几个字节,很有可能是外部碎片)可能根本放置不了东西,但是却占用了搜索时间,还不如当内部碎片算了
    • 放置策略趋向于产生好的匹配中使用。
      • 即占用所有空闲块,内部碎片也很少。
  • 分割空闲块
    • 缺点:更多的空闲块和已分配块,搜索速度降低。
    • 优点:空间利用率更高。

9.9.9 获取额外的堆存储器

如果分配器不能为请求块找到合适的空闲块将发生什么?

  • 合并相邻的空闲块(下一节描述)。
  • sbrk函数
    • 在最大化合并还不行的情况。
    • 向内核请求额外的堆存储器。
      • 并将其转为大的空闲块
      • 将块插入链表。

9.9.10 合并空闲块

假碎片: 因为释放,使得某些时候会出现相邻的空闲块。

  • 单独的放不下请求(碎片),合并却可以(假性),所以叫假碎片
何时合并?

重要的决策决定,何时执行合并?

  • 立即合并
    • 定义:被释放时,合并所有相邻的块。
    • 缺点:对于某些请求模式,会产生抖动
  • 推迟合并
    • 定义: 一个稍晚的时候,再合并。
      • 比如:上文中的找不到合适空闲块的时候。

在对分配器的讨论中,我们假设使用立即合并

但要知道,快速的分配器通常会选择某种形式的推迟合并

9.9.11 带边界标记的合并

Q:释放当前块后,如果要合并下一个块是十分简单,但是合并上一块复杂度却很高。


A:Knuth提出边界标记

  • 就是是头部的副本。
  • 其实就是双向链表啦。

  • 缺点:每个块保持一个头部和脚部,浪费空间。
    • 在应用程序操作许多个小块时,产生明显的存储器开销

Q: 如何解决这种开销

A: 使用边界标记优化方法.

  • 把前面块的已分配/空闲位存放到当前块多出来的低位(000)中。
    • 这样能快速判断前面的是否是分配/空闲
  • 如果是已分配的,不需要处理。
    • 所以已分配的不需要脚部。
  • 如果是未分配的,需要处理。
    • 未分配的依旧需要脚部。
    • 但是反正都是未分配的,占用一点不用的空间又怎样?

十分优美的优化

9.9.12 综合:实现一个简单的分配器

基于隐式空闲链表,使用立即边界标记合并方式,从头到尾讲述一个简单分配器的实现。

1.一般分配器设计
  • 序言块
    • 8字节的已分配块。
    • 只有一个头部和脚部组成。
    • 初始时创建,永不释放。
  • 普通块
    • mallocfree使用
  • 结尾块

    • 大小为0的已分配块。
  • 序言块和结尾块都是用来消除合并边界条件的小技巧。

之后具体的代码不一一描述了,需要的时候翻阅。

9.9.13 显式空闲链表

隐式空间链表就是一个玩具而已,用来介绍基本分配器概念。对于实际应用,还是太简单。

优化1 显式数据结构

根据定义,程序并不需要一个空闲块的主体。所以可以将空闲块组织成一种显式数据结构。

  • 双向链表

  • 优点:
    • 使得首次适配的分配时间从O(块总数)降低到O(空闲块总数)
  • 不过释放块时可能是线性,也可能是常数(普通的是常数)
    • 取决于空闲链表中块的排序策略。
      • 后进先出(LIFO)策略
        • 新释放的块直接放到双向链表的开始处。(释放常数级别)
          • 前继没有
          • 后继就是之前的在第一个的。
        • (处理的好的话,合并也是常数级别)
      • 地址优先
        • 释放是线性级别。
          • 寻找合适的前继要从头遍历。
        • 更好的空间利用率。
  • 缺点:
    • 最小的空闲块必须足够大,提高了内部碎片程度。

9.9.14 分离的空闲链表

分离存储: 维护多个空闲链表,其中每个链表中的块有大致相等的大小。

  • 一般的思路是将所有可能的块大小分成一些等价类,也叫做大小类(size class)
    • 有很多种方式定义大小类
      • 根据2的幂 : {1},{2},{3,4},{5~8},...{1025~2048},{2048~+oo}.
      • 小的块是本身,大块按2的幂:{1},{2},{3},{4},{5},{6},...{1025~2048},{2048~+oo}.

有关动态存储分配的文献描述了几十种 分离存储方法。

  • 主要的区别在于
    • 如何定义大小类。
    • 何时进行合并。
    • 何时向操作系统请求额外的堆存储器。
    • 是否允许分割。

我们介绍两种基本的方法

  • 简单分离存储(simple segregated storage)分离适配(segregated fit)
简单分离存储
  • 大小类
    • 每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。
      • 例如,{17~32}中,这个类的空闲链表全是32的块。
  • 如何分配
    • 检查相应大小最接近的空闲链表
      • 如果非空,简单的分配其中第一块的全部。
        • 不用分割,是全部哦
      • 如果为空,请求一个固定大小的额外存储器片,将这个片分割,然后加入对应的链表。
        • 然后继续跳回非空执行。
    • 常数级
  • 如何释放
    • 直接释放即可,然后分配器将释放后的块直接插入空闲链表
    • 常数级
  • 不分割,不合并。
    • 已分配块不需要头部。
    • 都不需要脚部。
  • 最显著的缺点
    • 很容易造成内部碎片外部碎片
分离适配

分配器维护着一个空闲链表的数组。

  • 每个空闲链表是和一个大小类相关联的,并且被组织称某种类型的显示或隐式链接。
  • 每个链表包含潜在的大小不同的块。
    • 这些块的大小大小类的成员。

有许多种不同的分离适配分配器,这里介绍一个简单版本。

  • 如何分配
    • 对适当的空闲链表做首次适配。
      • 成功
        • 那我们(可选的)分割它。
        • 并将剩余部分插入到适当的空闲链表
      • 失败
        • 继续找空闲链表
        • 如果找遍了都没有,就请求额外的堆存储器。
  • 释放,合并。
    • 释放一个块,并执行合并,存入相应的空闲链表

分离适配方法是一种常见的选择,C标准库提供的GUN malloc包就是采用的这种方法。

  • 快速
    • 搜索时间少
  • 对存储器的利用率
    • 分离空闲链表简单的首次适配搜索,其存储器利用率近似对堆的最佳适配搜索
3. 伙伴系统

伙伴系统(buddy system)是分离适配的一种特例,其中每个大小类都是2的幂。

  • 大小类
    • 都是2的幂,最大为2^m
  • 如何分配
    • 请求块大小向上舍入到最接近的2的幂,假设为2^k
    • 在空闲链表中找到第一个2^j,满足(k<=j<=m)
    • 二分变成2^(j-1)2^(j-1) 两部分,其中半块丢入空闲链表中。
      • 两者互相为伙伴
    • 不断上面步骤,直到j=k
    • 复杂度O(log(m)),很低
  • 如何释放,合并

    • 释放时,递归合并
      • 给定地址和块的大小,和容易计算它的伙伴地址。
    • 如果伙伴处于空闲就不断合并,否则就停止。
    • 复杂度O(log(m)),很低。

伙伴系统分配器的主要

  • 优点
    • 它的快速搜索和快速合并。
  • 缺点
    • 要求块大小为2的幂可能导致显著的内部碎片
    • 不适合通用目的的工作负载。
  • 对于预先知道其中块大小是2的幂的系统,伙伴系统分配器就很有吸引力。

9.10 GC_垃圾收集

垃圾收集器(garbage collector)是一种动态存储分配器。

  • 垃圾: 它自动释放不再需要的已分配块,这些块称为垃圾(garbage).
  • 垃圾收集(garbage collection) :自动回收堆存储的过程叫做垃圾收集
    • 应用显式分配堆块,但从不显式释放堆块。
    • 垃圾收集器定期识别垃圾快,并调用相应地free,将这些快放回空闲链表。

垃圾收集可以追溯到John McCarthy在20实际60年代早期在MIT开发的Lisp系统。

  • 它是Java,ML,PerlMathematic等现代语言系统的一个重要部分。
  • 有关文献描述了大量的垃圾收集方法,数量令人吃惊。
  • 我们讨论局限于McCarthy自创的Mark&Sweep(标记&清除)算法。
    • 这个算法很有趣。
    • 它可以建立已存在的malloc包的基础上,为C和C++提供垃圾收集。

9.10.1 垃圾收集器的基本知识

垃圾收集器将存储器视为一张有向可达图

  • 图的结点被分成一组根结点和一组堆结点
    • 堆结点对应于堆中一个已分配的块。
    • 根结点对应于这样一种不在堆中的位置。
      • 包含指向堆的指针寄存器栈里的变量,或者是虚拟存储区域中读写数据区域中的全局变量
    • 有向边p->q意味着块p中的某个位置指向块q中的某个位置
      • 实体化就是一个指针
  • 当存在一条任意从根结点出发到达p的有向路径时。
    • 我们说p可达的。
    • 否则是不可达的不可达结点对应于垃圾。

垃圾收集器的角色是维护可达图的某种表示,并释放不可达结点返回给空闲链表。

  • MLJava这样的语言的垃圾收集器,对应用如何创建和使用指针都有严格的控制。
    • 能够维护可达图的精确的表示,因而能回收所有垃圾。
  • CC++ 通常不能维护可达图的一种精确表示。这样的收集器叫做保守的垃圾收集器
    • 保守: 每个可达块都被标记为可达块,但有些不可达块也被标记为可达块。
    • 原因是,指针由自己管理,系统无法判定数据是否为指针,那么就不好精确的遍历。

如果malloc找不到合适的空闲块,就会调用垃圾收集器。回收一些垃圾到空闲链表。

  • 关键的思想是: 用收集器代替应用调用free

9.10.2 Mark&Sweep 垃圾收集器

Mark&Sweep 垃圾收集器由标记(mark)阶段和清除(sweep)阶段

  • 标记阶段:标记出根结点的所有可达和已分配的后继。
  • 清除阶段:后面的清除阶段释放每个未被标记的已分配块。
    • 头部的低位的一位用来表示是否被标记

标记的算法 就是从根结点开始,对结点的指针数据深搜并标记。

  • 通过isPtr()来判断是否是指针,p是否指向一个分配块的某个字。
    • 如果是,就返回该分配块的起始位置

清除的算法 就是遍历图,然后释放未被标记的。

9.10.3 C程序的保守Mark & Sweep(很有意思的一小节,败也指针)

C语言的isPtr()的实现有一些有趣的挑战。

  • C不会用任何类型信息来标记存储器位置。
    • 无法判断输入参数p是不是一个指针。
      • 所以在java等语言里面,指针全部由系统管理。
  • 即使假设是,isPtr()也没没有明显的方式判断p是否指向一个已分配块的有效载荷的某个位置。
    • 解决方法: 将已分配块维护成一颗平衡二叉树

      • 头部新增Left,和Right
        • Left:地址小于当前块
        • Right:地址大于当前块
    • 通过判断 addr<= p <= (addr + Size)判断是否属于这个块。

    • 这样子就能二分查找p 属于那个已分配块

C语言是保守的原因是,无法判断p逻辑上是指针,还是一个int标量

  • 因为,无论p是个什么玩意,都必须去访问,如果他是指针呢?
    • 而且这个int刚好还是某个不可到达块的地址。那么就会有残留。
  • 而且这种情况很常见,毕竟指针在数据段里毕竟不是特别多。

  • 但是在java等语言里,指针由系统统一管理,那么很容易就知道p是否是一个指针了。

  • 比如scanf("%d",a); 程序会把a的int值看作指针。而且运行中,无法判断。

9.11 C程序中常见的与存储器有关的错误

9.11.1 间接引用坏指正

scanf("%d",&val);
scanf("%d",val);
  • 最好的情况 : 以异常中止。
  • 有可能覆盖某个合法的读/写区域,造成奇怪的困惑的结果。

9.11.2 读未初始化的存储器

堆存储器并不会初始化。

  • 正确做法
    • 使用calloc.
    • 显示y[i]=0;

9.11.3 允许栈缓冲区溢出(不太懂,还没接触I/O)

程序不检查输入串的大小就写入栈中的目标缓冲区

  • 那么就有缓冲区溢出错误(buffer overflow bug)
  • gets()容易引起这样的错误
    • fgets()限制大小。

9.11.4 假设指针和它们所指向对象是相同大小。

有的系统里,intint *都是四字节,有的则不同。

9.11.5 越界

没啥好说的。

9.11.6 引用指针,而不是它所指向的对象

对指针的优先级用错。

例 :*size-- 本意 (*size)--

  • 错误:先操作的指针-1,再访问。

9.11.7 误解指针的运算

忘记了指针的算术操作是以它们指向的对象的大小为单位来进行的,这种大小不一定是字节。

9.11.8 引用不存在的变量

返回一个指针,指向栈里面一个变量的地址。但是这个变量在返回的时候已经从栈里被弹出。

  • 地址是正确的,指向了栈。
  • 但是却没有指向想指向的变量。

9.11.9 引用空闲堆块的数据

引用了某个已经free掉的块。在C++多态中经常容易犯这个错误。

9.11.10 引起存储器泄露

  • 即是没有回收垃圾。导致内存中垃圾越来越多。
    • 只有重启程序,才能释放。
  • 对于守护进程服务器这样的程序,存储器泄露是十分严重的事。
    • 因为一般情况,不能随便重启。

9.12 小结

虚拟存储器是对主存的一个抽象。

  • 使用一种叫虚拟寻址的间接形式来引用主存。
    • 处理器产生虚拟地址,通过一种地址翻译硬件来转换为物理地址
      • 通过使用页表来完成翻译。
        • 又涉及到各级缓存的应用。
        • 页表的内容由操作系统提供

虚拟存储器提供三个功能

  • 它在主存中自动缓存最近使用的存放在磁盘上的虚拟地址空间内容。
    • 虚拟存储器缓存中的块叫做
  • 简化了存储器管理,
    • 进而简化了链接
    • 进程间共享数据
    • 进程的存储器分配以及程序加载
  • 每条页表条目里添加保护位,从而简化了存储器保护

地址翻译的过程必须和系统中所有的硬件缓存的操作集合。

  • 大多数条目位于L1高速缓存中。
    • 但是又通过一个TLB的页表条目的片上高速缓存L1

现代系统通过将虚拟存储器片和磁盘上的文件片关联起来,以初始化虚拟存储器片,这个过程叫做存储器映射

  • 存储器映射共享数据创建新的进程 以及加载数据提供一种高效的机制。
  • 可以用mmap 手工维护虚拟地址空间区域

    • 大多数程序依赖于动态存储器分配,例:malloc
      • 管理虚拟地址空间一个称为堆的区域
      • 分配器两种类型。
        • 显示分配器
          • CC++
        • 隐式分配器
          • JAVA

GC是通过不断递归访问指针来标记已分配块,在需要的时刻进行Sweep

  • C,C++无法辨认指针导致无法实现完全的GC
    • 只有保守的GC
    • 需要配合平衡树进行查找p所指向的
    <p class="postfoot">
        posted on <span id="post-date">2016-05-25 07:19</span> <a href='http://www.cnblogs.com/zy691357966/'>DDUPzy</a> 阅读(<span id="post_view_count">...</span>) 评论(<span id="post_comment_count">...</span>)  <a href ="https://i.cnblogs.com/EditPosts.aspx?postid=5525684" rel="nofollow">编辑</a> <a href="#" onclick="AddToWz(5525684);return false;">收藏</a>
    </p>
</div>
<script src="//common.cnblogs.com/highlight/9.1.0/highlight.min.js?id=20160127"></script><script>markdown_highlight();</script><script type="text/javascript">var allowComments=true,cb_blogId=284659,cb_entryId=5525684,cb_blogApp=currentBlogApp,cb_blogUserGuid='fc86d0d0-df16-e611-9fc1-ac853d9f53cc',cb_entryCreatedDate='2016/5/25 7:19:00';loadViewCount(cb_entryId);</script>

</div><a name="!comments"></a><div id="blog-comments-placeholder"></div><script type="text/javascript">var commentManager = new blogCommentManager();commentManager.renderComments(0);</script>
fixPostBody(); setTimeout(function () { incrementViewCount(cb_entryId); }, 50); deliverAdT2(); deliverAdC1(); deliverAdC2(); loadNewsAndKb(); loadBlogSignature(); LoadPostInfoBlock(cb_blogId, cb_entryId, cb_blogApp, cb_blogUserGuid); GetPrevNextPost(cb_entryId, cb_blogId, cb_entryCreatedDate); loadOptUnderPost(); GetHistoryToday(cb_blogId, cb_blogApp, cb_entryCreatedDate);

导航

统计

  • 随笔 - 306
  • 文章 - 0
  • 评论 - 2
  • 引用 - 0

公告


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值