存储管理(Storage Management in Chez Scheme)

垃圾回收

Scheme程序不会显式地释放诸如序对,字符串和过程之类的Scheme对象。 相反,一旦存储管理系统证明不再可以访问该对象,它就会自动回收与该对象关联的存储。 为了回收此存储,Chez Scheme使用了一个垃圾收集器,该垃圾收集器在程序运行时定期运行。 垃圾收集器从一组已知的根开始(例如,机器注册),查找所有可访问对象,并在大多数情况下复制它们,以消除可访问对象之间的碎片,并回收不可访问对象所占用的存储。

回收由默认的collect-request处理程序自动触发,该处理程序通过collect-request中断调用,该中断在分配了大约n个字节的存储空间后发生,其中n是参数collect-trip-bytes 的值。 默认的collect-request处理程序通过调用不带参数的collect程序来进行会睡。 可以通过更改参数collect-request-handler 的值来重新定义collect-request处理程序。 程序还可以通过直接调用collect来导致在collect-request中断之间发生回收。

Chez Scheme的回收器是基于分代 的。它根据对象的年龄(大致来说,回收幸存数)来对对象进行分类,且老对象比年轻对象回收的频率要低。由于年轻对象比老对象更快地变为不可访问,因此结果是大多数的回收花费的时间更少。系统还维护一个静态代(static generation, 类似Java的永久代),不会对此存储进行回收。仅当压缩堆(Scompact_heap)或要collecttarget-generation 参数为static符号时,才将对象放入静态代中。

非静态代的编号从最年轻的世代(开始于0)到collect-maximum-generation的当前值。存储管理器将新分配的对象放入第0代。在第0代的回收过程中,默认情况下,将第0代的对象挪到第1代,类似地,在第1代回收期间,存活的第0代和第1代对象移动到第2代,依此类推。在最大非静态对象回收的过程中,所有幸存的非静态对象都将移动(可能返回)到最大非静态代中。 通过这种机制,一个对象有可能跳过一个或多个世代,但这在许多对象上不太可能发生,并且如果这些对象变得不可访问,则最终将回收它们的存储。

维护内部计数器gc-trip来控制何时回收每一代。 每次调用不带参数的collect 时(从默认的collect-request处理程序开始),gc-trip都会加1。在collect-generation基数为r 的情况下,回收的世代编号为g ,其gc-trip是$r^g$的倍数。 如果将collect-generation-radix设置为4,则系统将每次收集0代,每4次收集1代,每16次收集2代,依此类推。

每次某个世代g 调用collect时,该世代g 被回收且gc-trip前进到下个$r^g$的边界,但是不会超过$r^{g+1}$的边界,r 不变还是collect-generation-radix的值。

如果使用第二个参数tg 调用collect,则tg 确定目标代。 当g 是最大的非静态代时,tg 必须为g 或为static。 否则,tg 必须为gg + 1 。 当目标代是static符号时,非静态代中的所有数据都将移动到静态代中。静态代中的对象从不会被回收。 这在加载和初始化应用程序的永久代码和数据结构之后非常有用,以减少后续回收的开销。

通过设置本节中描述的参数,可以对回收器的行为进行实质性的调整。通过重新定义collect-request处理程序且使用显式的gtg 参数调用collect,甚至有可能完全覆盖收集器的默认策略来确定何时回收每个世代。例如,程序员可以通过使用显式的gtg 参数调用collect来重新定义处理程序,以在长时间内将最大的非静态代视为静态代,该参数在该时间段内绝不等于最大的非静态代。

(collect) | (collect g) | (collect g tg)

g 必须是不大于最大非静态代(collect-maximum-generation返回的值)的非负确定编号。 如果g 已经是最大的非静态代编号,则tg 必须是一个等于g 的fixnum或static符号。 否则,tg 必须是一个等于g 或大于g 的fixnum。

此过程使存储管理器执行垃圾回收。 collect是通过collect-request处理程序定期调用的,但是也可以显式调用它,以在特定时间(例如,在计时计算之前)强制进行回收。 在Chez Scheme的线程版本中,调用collect的线程必须是唯一的活动线程。

系统将根据gtg (如果提供)确定回收哪些世代,如本节的介绍中所述。

(collect-rendezvous)

请求垃圾回收的方式应该与由系统自动发起的GC的方式相一致。所有正在运行的线程经过协调,以便其中一个调用collect-request处理程序,而其他线程暂停直到处理程序返回。

请注意,如果collect-request处理程序(请参阅collect-request-handler)没有调用collect,那么collect-rendezvous实际上不会执行垃圾回收。

collect-notify

如果将collect-notify设置为true,则每当运行GC时,回收器都会打印一条消息。 默认情况下,collect-notify设置为#f。

collect-trip-bytes

Chez Scheme在内部以大块分配内存,并通过内联操作将这些块细分以提高效率。存储管理器确定是否为每个分配的大块仅请求一次回收。此外,在存储管理器请求回收和兑现回收请求之间可能会花费一些时间,尤其是如果通过with-interrupts-disableddisable-interrupts临时禁用了中断时。因此,collect-trip-bytes仅是一种近似度量。

collect-generation-radix

此参数确定默认情况下的collect-request处理程序调用不带参数的collect时回收每一代的频率。每$r^g$次发生一次对应世代的回收,其中rcollect-generation-radix的值,g 是世代数。

collect-generation-radix设置为1会强制所有世代每次都被回收, 将collect-generation-radix设置为非常大的数目将无限期有效地延迟较早的一代的回收。

collect-maximum-generation

此参数确定当前可以使用的最大非静态世代数,它的值是1到254范围内的精确整数。设置为1时,仅使用两个非静态生成。 设置为2时,将使用三个非静态世代,依此类推。 当设置为254时,将使用255个非静态代,再加上一个静态代,总共256个世代。增加世代数可以减少了收集旧对象的频率,潜在地减少了收集开销,但同时也潜在地增加了系统中保留的不可访问对象的数量,从而增加了所需的内存总量。

collect-request-handler

collect-request-handler的值必须是一个过程。当系统认为应该要进行GC时(即,自上次GC以来,系统分配了由参数collect-trip-bytes规定的存储量之后),该过程在不带参数的情况下被调用。

默认情况下,collect-request-handler仅调用不带参数的collect。 可以通过将collect-request-handler设置为不执行任何操作的过程来禁用自动收集,例如:

(collect-request-handler void)

也可以利用防止任何中断的critical-section来临时禁用GC

release-minimum-generation

此参数的值必须介于0到collect-maximum-generation的值(包括)之间,并且默认为collect-maximum-generation的值。

当分配新数据且进行GC时,storage-management会自动地从操作系统中请求额外的虚拟内存地址。相应地,在堆显著减小的情况下,系统尝试将先前从操作系统获得的某些虚拟内存返回给操作系统。默认情况下,系统仅在针对最大非静态时代的GC之后才这样做。也可以让系统在对更年轻的世代回收之后就执行此操作,方法是将release-minimum-generation的值更改为小于collect-maximum-generation的值。由参数指定的世代,或者任何较老的世代是GC的目标世代时,存储管理系统将在GC之后尝试将不需要的虚拟内存返回给操作系统。

collect-maximum-generation设置为一个新值g 时,release-minimum-generation也同时隐式地更改为g ,有两个前提:(a)修改前这两个参数具有相同的值;(b)release-minimum-generation的值大于g

heap-reserve-ratio

此参数确定了保留的内存的大概数量(没有返回给OS,如release-minimum-generation所描述的)与当前已占用的内存量(不包含已变为静态的内存区域)的比例,它的值必须是不精确的非负整数值; 如果设置为精确的实数值,则精确的值将转换为不精确的值。默认值1.0,为每个当前占用的非静态页面保留一页内存。 将其设置为较小的值可能会导致较小的平均虚拟内存占用量,而将其设置为较大的值可能会导致较少的操作系统调用以请求和释放内存空间。

弱序对,暂时序对和守护者Weak Pairs, Ephemeron Pairs, and Guardians

weak pairs允许程序维护指向对象的弱指针。 指向对象的弱指针不会阻止存储管理系统回收该对象,但是只要该对象在系统中是可访问的,它仍然有效。

ephemeron pairs与weak pairs类似,但是ephemeron pairs拥有两个指针,其中仅仅在第一个指针存在的情况第二个指针才能存在

guardians允许程序保护对象免遭垃圾回收器的重分配,并确定该对象何时被重新分配。

weak paris, ephemeron pairs 和guardians允许程序将有关对象的信息保留在单独的数据结构(例如哈希表)中,而无需担心维护此信息将导致对象无限期地保留在系统中。

另外,guardians允许无限期地从释放对象中保存对象,以便可以重用它们,或者可以使用存储在对象中的数据执行清理或其他操作。

(weak-cons obj1 obj2)

返回:一个新的弱序对

obj1 是新对的car, obj2构成了新对的cdr。弱序对和普通对是无法区分的,除了这两种方式:

  • 弱序对可以使用**weak-pair?**这个谓词来区别普通对
  • 弱序对维护了一个指向(car obj)的弱指针

弱序对的car中的弱指针就像普通指针一样,只要它指向的对象可以通过系统中某个地方的普通(非弱)指针访问即可。 但是,如果垃圾收集器在某个时候识别出不存在指向该对象的非弱指针,则它将每个指向该对象的弱指针替换为“ broken weak-pointer”对象**#!bwp**,并丢弃该对象。

弱序对的cdr字段不是弱指针,因此可以使用弱序对来构造弱保持对象的列表。可以像使用普通的列表的处理操作(例如length,map和assv)来操作这些list。弱序对可以使用set-car!和set-cdr!来修改; 在set-car!之后,car字段包含指向新对象的弱指针,代替了旧对象。弱序对的打印方式与普通对相同。 弱序对没有reader语法。弱序对在被写入然后被读取时成为普通对。

(define x (cons 'a 'b))
(define p (weak-cons x '()))
(car p)  (a . b)

(define x (cons 'a 'b))
(define p (weak-cons x '()))
(set! x '*)
(collect)
(car p)  #!bwp

(weak-pair? obj)

(weak-pair? (weak-cons a b))  #t
(weak-pair? (cons a b))  #f
(weak-pair? "oops")  #f

(ephemeron-cons obj1 obj2)

obj1 是新对的car, obj2构成了新对的cdr。暂时序对和普通对是无法区分的,除了这两种方式:

  • 暂时序对可以使用**ephemeron-pair?**这个谓词来区别普通对
  • 暂时序对维护了一个指向(car obj)的弱指针,并且仅仅在pair的car存在时cdr才能保留

暂时序对的行为与弱序对类似,不过cdr有特殊的处理:如果car被设置为**#!bwp的同时也会将cdr设置为#!bwp**。由于同时将car和cdr字段设置为为**#!bwp**,因此可以通过cdr对象引用car对象这一事实本身并不意味着必须保留car(与弱序对不同)。 相反,出于某种原因,car必须独立于cdr对象保存。

与弱序对和其他对一样,暂时序对使用**set-car!set-cdr!**来修改数据,暂时序对的打印方式与普通对一样,但没有reader语法

(define x (cons 'a 'b))
(define p (ephemeron-cons x x))
(car p)  (a . b)
(cdr p)  (a . b)

(define x (cons 'a 'b))
(define p (ephemeron-cons x x))
(set! x '*)
(collect)
(car p)  #!bwp
(cdr p)  #!bwp

(define x (cons 'a 'b))
(define p (weak-cons x x)) ; not an ephemeron pair
(set! x '*)
(collect)
(car p)  (a . b)
(cdr p)  (a . b)

与弱序对一样,如果在将x设置为*之前进行了垃圾回收将该pair提升为较老的一代,则上面中间示例的最后两个表达式实际上可能返回(a . b)。 但是,在上面的最后一个示例中,最后两个表达式的结果将始终为(a . b),因为弱序对的cdr持有非弱引用,并且该非弱引用阻止car字段变** #!bwp**。

(ephemeron-pair? obj)

(ephemeron-pair? (ephemeron-cons 'a 'b))  #t
(ephemeron-pair? (cons 'a 'b))  #f
(ephemeron-pair? (weak-cons 'a 'b))  #f
(ephemeron-pair? "oops")  #f

(bwp-object? obj)

返回:如果obj是断开的broken weak-pair对象,则返回#t,否则返回#f

(bwp-object? #!bwp)  #t
(bwp-object? 'bwp)  #f

(define x (cons 'a 'b))
(define p (weak-cons x '()))
(set! x '*)
(collect (collect-maximum-generation))
(car p)  #!bwp
(bwp-object? (car p))  #t

(make-guardian)

Guardians由要保护的对象组的过程表示。创建guardian后,注册对象组为空。 通过将对象作为参数传递给守护者,可以向guardian注册对象:

(define G (make-guardian))
(define x (cons 'aaa 'bbb))
x  (aaa . bbb)
(G x)

注册对象时也可以指定“representative”(即,y)对象。 继续上面的示例:

(define y (cons 'ccc 'ddd))
y  (ccc . ddd)
(G y 'rep)

与守护者关联的一组注册对象在逻辑上细分为两个不相交的子组:一个子组称为“可访问”对象,一个子组称为“不可访问”对象。不可访问的对象是已被证明无法访问的对象(通过guardian机制本身或通过弱序对或暂时序对的car字段除外),可访问的对象是未经证明的对象。“已证明”一词在这里很重要:可能是可访问组中的某些对象确实是不可访问的,但这尚未得到证明。 在某些情况下,直到对象实际上变得不可访问很久之后(在当前实现中,直到发生包含对象的世代的垃圾回收),才可能做出这种证明。

向guardian注册的对象最初被放置在可访问组中,并在它们变得不可访问后的某个时刻移入不可访问组。 不可访问组中的对象是通过调用不带参数的guardian来检索的。 如果不可访问组中没有对象,则guardian返回#f。 继续上面的示例:

(G)  #f
(set! x #f)
(set! y #f)
(collect)
(G)  (aaa . bbb) ; 也有可能这个后打印出来
(G)  rep         ; 这个先打印出来
(G)  #f

对G的初始调用返回#f,因为绑定到x和y的对是向G注册的唯一对象,并且仍然可以通过这些绑定访问这些序对。调用collect时,对象将移入不可访问的组。 因此,对G的两个调用返回先前绑定到x的序对和先前绑定到y的序对的representative,尽管可能与所示顺序相反。 (如上所述,对于弱序对,如果对象已迁移到较老的一代,则调用collect实际上可能不足以证明该对象不可访问。)

实际上,从guardian那里获取的对象在任何方面都没有特殊的地位。 此功能避免了共享或循环结构可能会出现的问题。 由不可访问对象组成的共享或循环结构将被完整保留,将注册由guardian保护的部分都放置在该guardian不可访问的集合中。 然后,程序员可以完全控制结构的处理顺序。

一个对象可以在guardian处多次注册,在这种情况下,可以多次检索该对象:

(define G (make-guardian))
(define x (cons 'aaa 'bbb))
(G x)
(G x)
(set! x #f)
(collect)
(G)  (aaa . bbb)
(G)  (aaa . bbb)

它也可以向不止一个guardian注册,并且guardian本身也可以向其他guardian注册。 在没有“representative”的情况下向guardian注册的对象,并放置在一个弱序对或暂时对的car字段中,其直到从guardian处将返回并由程序丢弃,或者直到guardian本身被丢弃为止。

(define G (make-guardian))
(define x (cons 'aaa 'bbb))
(define p (weak-cons x '()))
(G x)
(set! x #f)
(collect)
(set! y (G))
y  (aaa . bbb)
(car p)  (aaa . bbb)
(set! y #f)
(collect 1)
(car p)  #!bwp

另一方面,如果指定了representative(对象本身除外),则在从guardian处获得representative的同时,也会从弱序对或暂时序对的car字段中丢弃受保护的对象。

(define G (make-guardian))
(define x (cons 'aaa 'bbb))
(define p (weak-cons x '()))
(G x 'rep)
(set! x #f)
(collect)

(G)  rep  ; 获得representative的值
(car p)  #!bwp ; 自动丢弃

下面的示例说明了当guardian本身丢弃时,该对象已被释放并且弱序对的car字段设置为#!bwp:

(define G (make-guardian))
(define x (cons aaa 'bbb))
(define p (weak-cons x '()))
(G x)
(set! x #f)
(set! G #f)
(collect)
(car p)  #!bwp

下面的示例演示了如何使用guardian来释放外部存储,就像由C库“malloc”和“free”操作管理存储。

(define malloc
  (let ([malloc-guardian (make-guardian)])
    (lambda (size)
      ; first free any storage that has been dropped.  to avoid long
      ; delays, it might be better to deallocate no more than, say,
      ; ten objects for each one allocated
      (let f ()
        (let ([x (malloc-guardian)])
          (when x
		    (do-free x)
			(f))))
      ; then allocate and register the new storage
      (let ([x (do-malloc size)])
        (malloc-guardian x)
        x))))

do-malloc必须返回一个Scheme对象“header”,该header封装一个指向外部存储的指针(可能是无符号整数),并且必须通过此header对外部存储进行所有访问。特别是,必须注意在删除相应的header之后,在Scheme之外不存在指向外部存储的指针。 do-free必须使用封装的指针释放外部存储。这两个原语都可以使用外部分配和外部无关的定义,也可以作为外部过程导入的C库“malloc”和“free”运算符进行定义。

如果不希望调用malloc来释放存储,则可以使用collect-request处理器来检查并释放已丢弃的存储,如下所示:

(define malloc)
(let ([malloc-guardian (make-guardian)])
  (set! malloc
    (lambda (size)
      ; allocate and register the new storage
      (let ([x (do-malloc size)])
        (malloc-guardian x)
        x)))

  (collect-request-handler
    (lambda ()
      ; first, invoke the collector
      (collect)
      ; then free any storage that has been dropped
      (let f ()
        (let ([x (malloc-guardian)])
          (when x
            (do-free x)
            (f)))))))

通过一点重构,就有可能将封装的外部地址注册为带header的representative,在这种情况下,do-free将仅将外部地址作为参数。 这将使标头一旦变得不可访问,便可以将其从Scheme堆中删除。

锁对象Locking Objects

来自C语言的变量或数据结构到Scheme对象的所有指针,通常应在输入(或重新输入)Scheme之前丢弃。 当无法遵循该准则时,可以通过锁定对象或等效的C库过程Slock_object锁定该对象。

(lock-object obj)

锁定对象可防止存储管理器收回或重定位该对象。 应谨慎使用锁定,因为它会导致内存碎片并增加存储管理开销。

如果未解锁对象,锁定也会导致意外保留存储空间。 可以通过解锁对象或等效的C库过程Sunlock_object来解锁对象。

锁定立即数(例如,fixnum,布尔值和字符)或已被静态化的对象是不必要但无害的。

(unlock-object obj)

通过连续调用lock-object,Slock_object或同时调用这两个对象,可以多次锁定对象,在这种情况下,必须先通过相等次数的对unlock-objectSunlock_object的调用来将其解锁。

除非存在指向对象的单独的C指针,否则也无需锁定包含在锁定对象中的对象(例如,锁定序对的car中的对象)。也就是说,如果仅允许通过外部对象来间接访问的内部对象,则应将其解锁,以便回收器在回收期间可以自由地重分配它。

解锁立即值(例如,fixnum,布尔值和字符)或已设为静态的对象是不必要的,无效的,但无害。

(locked-object? obj)

返回:如果obj是锁定的,立即的或静态的,返回#t

如果回收器无法重分配或回收obj,则该谓词将返回true,包括立即值,例如fixnums,布尔值和字符以及已设为静态的对象。