61阅读

垃圾回收-浅谈PHP5中垃圾回收算法(Garbage Collection)的演化

发布时间:2018-03-26 所属栏目:flash as

一 : 浅谈PHP5中垃圾回收算法(Garbage Collection)的演化

前言:PHP是一门托管型语言,在PHP编程中程序员不需要手工处理内存资源的分配与释放(使用C编写PHP或Zend扩展除外),这就意味着PHP本身实现了垃圾回收机制(Garbage Collection)。现在如果去PHP官方网站(php.net)可以看到,目前PHP5的两个分支版本PHP5.2和PHP5.3是分别更新的,这是因为许多项目仍然使用5.2版本的PHP,而5.3版本对5.2并不是完全兼容。PHP5.3在PHP5.2的基础上做了诸多改进,其中垃圾回收算法就属于一个比较大的改变。本文将分别讨论PHP5.2和PHP5.3的垃圾回收机制,并讨论这种演化和改进对于程序员编写PHP的影响以及要注意的问题。

PHP变量及关联内存对象的内部表示

垃圾回收说到底是对变量及其所关联内存对象的操作,所以在讨论PHP的垃圾回收机制之前,先简要介绍PHP中变量及其内存对象的内部表示(其C源代码中的表示)。

PHP官方文档中将PHP中的变量划分为两类:标量类型和复杂类型。标量类型包括布尔型、整型、浮点型和字符串;复杂类型包括数组、对象和资源;还有一个NULL比较特殊,它不划分为任何类型,而是单独成为一类。

所有这些类型,在PHP内部统一用一个叫做zval的结构表示,在PHP源代码中这个结构名称为“_zval_struct”。zval的具体定义在PHP源代码的“Zend/zend.h”文件中,下面是相关代码的摘录。

 

typedef union _zvalue_value { 	long lval; /* long value */ 	double dval; /* double value */ 	struct {  char *val;  int len; 	} str; 	HashTable *ht; /* hash table value */ 	zend_object_value obj; } zvalue_value; struct _zval_struct { 	/* Variable information */ 	zvalue_value value; /* value */ 	zend_uint refcount__gc; 	zend_uchar type;	/* active type */ 	zend_uchar is_ref__gc; };

其中联合体“_zvalue_value”用于表示PHP中所有变量的值,这里之所以使用union,是因为一个zval在一个时刻只能表示一种类型的变量。可以看到_zvalue_value中只有5个字段,但是PHP中算上NULL有8种数据类型,那么PHP内部是如何用5个字段表示8种类型呢?这算是PHP设计比较巧妙的一个地方,它通过复用字段达到了减少字段的目的。例如,在PHP内部布尔型、整型及资源(只要存储资源的标识符即可)都是通过lval字段存储的;dval用于存储浮点型;str存储字符串;ht存储数组(注意PHP中的数组其实是哈希表);而obj存储对象类型;如果所有字段全部置为0或NULL则表示PHP中的NULL,这样就达到了用5个字段存储8种类型的值。

而当前zval中的value(value的类型即是_zvalue_value)到底表示那种类型,则由“_zval_struct”中的type确定。_zval_struct即是zval在C语言中的具体实现,每个zval表示一个变量的内存对象。除了value和type,可以看到_zval_struct中还有两个字段refcount__gc和is_ref__gc,从其后缀就可以断定这两个家伙与垃圾回收有关。没错,PHP的垃圾回收全靠这俩字段了。其中refcount__gc表示当前有几个变量引用此zval,而is_ref__gc表示当前zval是否被按引用引用,这话听起来很拗口,这和PHP中zval的“Write-On-Copy”机制有关,由于这个话题不是本文重点,因此这里不再详述,读者只需记住refcount__gc这个字段的作用即可。

PHP5.2中的垃圾回收算法——Reference Counting

PHP5.2中使用的内存回收算法是大名鼎鼎的Reference Counting,这个算法中文翻译叫做“引用计数”,其思想非常直观和简洁:为每个内存对象分配一个计数器,当一个内存对象建立时计数器初始化为1(因此此时总是有一个变量引用此对象),以后每有一个新变量引用此内存对象,则计数器加1,而每当减少一个引用此内存对象的变量则计数器减1,当垃圾回收机制运作的时候,将所有计数器为0的内存对象销毁并回收其占用的内存。而PHP中内存对象就是zval,而计数器就是refcount__gc。

例如下面一段PHP代码演示了PHP5.2计数器的工作原理(计数器值通过xdebug.org得到):

<?php

$val1 = 100; //zval(val1).refcount_gc = 1;
$val2 = $val1; //zval(val1).refcount_gc = 2,zval(val2).refcount_gc = 2(因为是Write on copy,当前val2与val1共同引用一个zval)
$val2 = 200; //zval(val1).refcount_gc = 1,zval(val2).refcount_gc = 1(此处val2新建了一个zval)
unset($val1); //zval(val1).refcount_gc = 0($val1引用的zval再也不可用,会被GC回收)

?>

Reference Counting简单直观,实现方便,但却存在一个致命的缺陷,就是容易造成内存泄露。很多朋友可能已经意识到了,如果存在循环引用,那么Reference Counting就可能导致内存泄露。例如下面的代码:

<?php

$a = array();
$a[] = & $a;
unset($a);

?>

这段代码首先建立了数组a,然后让a的第一个元素按引用指向a,这时a的zval的refcount就变为2,然后我们销毁变量a,此时a最初指向的zval的refcount为1,但是我们再也没有办法对其进行操作,因为其形成了一个循环自引用,如下图所示:

image

其中灰色部分表示已经不复存在。由于a之前指向的zval的refcount为1(被其HashTable的第一个元素引用),这个zval就不会被GC销毁,这部分内存就泄露了。

这里特别要指出的是,PHP是通过符号表(Symbol Table)存储变量符号的,全局有一个符号表,而每个复杂类型如数组或对象有自己的符号表,因此上面代码中,a和a[0]是两个符号,但是a储存在全局符号表中,而a[0]储存在数组本身的符号表中,且这里a和a[0]引用同一个zval(当然符号a后来被销毁了)。希望读者朋友注意分清符号(Symbol)的zval的关系。

在PHP只用于做动态页面脚本时,这种泄露也许不是很要紧,因为动态页面脚本的生命周期很短,PHP会保证当脚本执行完毕后,释放其所有资源。但是PHP发展到目前已经不仅仅用作动态页面脚本这么简单,如果将PHP用在生命周期较长的场景中,例如自动化测试脚本或deamon进程,那么经过多次循环后积累下来的内存泄露可能就会很严重。这并不是我在耸人听闻,我曾经实习过的一个公司就通过PHP写的deamon进程来与数据存储服务器交互。

由于Reference Counting的这个缺陷,PHP5.3改进了垃圾回收算法。

PHP5.3中的垃圾回收算法——Concurrent Cycle Collection in Reference Counted Systems

PHP5.3的垃圾回收算法仍然以引用计数为基础,但是不再是使用简单计数作为回收准则,而是使用了一种同步回收算法,这个算法由IBM的工程师在论文Concurrent Cycle Collection in Reference Counted Systems中提出。

这个算法可谓相当复杂,从论文29页的数量我想大家也能看出来,所以我不打算(也没有能力)完整论述此算法,有兴趣的朋友可以阅读上面的提到的论文(强烈推荐,这篇论文非常精彩)。

我在这里,只能大体描述一下此算法的基本思想。

首先PHP会分配一个固定大小的“根缓冲区”,这个缓冲区用于存放固定数量的zval,这个数量默认是10,000,如果需要修改则需要修改源代码Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES然后重新编译。

由上文我们可以知道,一个zval如果有引用,要么被全局符号表中的符号引用,要么被其它表示复杂类型的zval中的符号引用。因此在zval中存在一些可能根(root)。这里我们暂且不讨论PHP是如何发现这些可能根的,这是个很复杂的问题,总之PHP有办法发现这些可能根zval并将它们投入根缓冲区。

当根缓冲区满额时,PHP就会执行垃圾回收,此回收算法如下:

1、对每个根缓冲区中的根zval按照深度优先遍历算法遍历所有能遍历到的zval,并将每个zval的refcount减1,同时为了避免对同一zval多次减1(因为可能不同的根能遍历到同一个zval),每次对某个zval减1后就对其标记为“已减”。

2、再次对每个缓冲区中的根zval深度优先遍历,如果某个zval的refcount不为0,则对其加1,否则保持其为0。

3、清空根缓冲区中的所有根(注意是把这些zval从缓冲区中清除而不是销毁它们),然后销毁所有refcount为0的zval,并收回其内存。

如果不能完全理解也没有关系,只需记住PHP5.3的垃圾回收算法有以下几点特性:

1、并不是每次refcount减少时都进入回收周期,只有根缓冲区满额后在开始垃圾回收。

2、可以解决循环引用问题。

3、可以总将内存泄露保持在一个阈值以下。

PHP5.2与PHP5.3垃圾回收算法的性能比较

由于我目前条件所限,我就不重新设计试验了,而是直接引用PHP Manual中的实验,关于两者的性能比较请参考PHP Manual中的相关章节:http://www.php.net/manual/en/features.gc.performance-considerations.php。

首先是内存泄露试验,下面直接引用PHP Manual中的实验代码和试验结果图:

<?php
class Foo
{
    public $var = '3.1415962654';
}

$baseMemory = memory_get_usage();

for ( $i = 0; $i <= 100000; $i++ )
{
    $a = new Foo;
    $a->self = $a;
    if ( $i % 500 === 0 )
    {
        echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "n";
    }
}
?>

PHP内存泄露试验

可以看到在可能引发累积性内存泄露的场景下,PHP5.2发生持续累积性内存泄露,而PHP5.3则总能将内存泄露控制在一个阈值以下(与根缓冲区大小有关)。

另外是关于性能方面的对比:

<?php
class Foo
{
    public $var = '3.1415962654';
}

for ( $i = 0; $i <= 1000000; $i++ )
{
    $a = new Foo;
    $a->self = $a;
}

echo memory_get_peak_usage(), "n";
?>

这个脚本执行1000000次循环,使得延迟时间足够进行对比,然后使用CLI方式分别在打开内存回收和关闭内存回收的的情况下运行此脚本:

time php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php
# and
time php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php

在我的机器环境下,运行时间分别为6.4s和7.2s,可以看到PHP5.3的垃圾回收机制会慢一些,但是影响并不大。

与垃圾回收算法相关的PHP配置

可以通过修改php.ini中的zend.enable_gc来打开或关闭PHP的垃圾回收机制,也可以通过调用gc_enable( )或gc_disable( )打开或关闭PHP的垃圾回收机制。在PHP5.3中即使关闭了垃圾回收机制,PHP仍然会记录可能根到根缓冲区,只是当根缓冲区满额时,PHP不会自动运行垃圾回收,当然,任何时候您都可以通过手工调用gc_collect_cycles( )函数强制执行内存回收。

本文基于署名-非商业性使用 3.0许可协议发布,欢迎转载,演绎,但是必须保留本文的署名张洋(包含链接),且不得用户商业目的。

二 : c# -- 对象销毁和垃圾回收

有些对象需要显示地销毁代码来释放资源,比如打开的文件资源,锁,操作系统句柄和非托管对象。[www.61k.com]在.NET中,这就是所谓的对象销毁,它通过IDisposal接口来实现。不再使用的对象所占用的内存管理,必须在某个时候回收;这个被称为无用单元收集的功能由CLR执行。

对象销毁和垃圾回收的区别在于:对象销毁通常是明确的策动;而垃圾回收完全是自动地。换句话说,程序员负责释放文件句柄,锁,以及操作系统资源;而CLR负责释放内存。

本章将讨论对象销毁和垃圾回收,还描述了C#处理销毁的一个备选方案--Finalizer及其模式。最后,我们讨论垃圾回收器和其他内存管理选项的复杂性。

对象销毁垃圾回收
1)IDisposal接口
2) Finalizer
垃圾回收
对象销毁用于释放非托管资源垃圾回收用于自动释放不再被引用的对象所占用的内存;并且垃圾回收什么时候执行时不可预计的
为了弥补垃圾回收执行时间的不确定性,可以在对象销毁时释放托管对象占用的内存 

IDisposal,Dispose和Close

销毁 c# -- 对象销毁和垃圾回收

.NET Framework定义了一个特定的接口,类型可以使用该接口实现对象的销毁。该接口的定义如下:

public interface IDisposable { void Dispose(); }

C#提供了鴘语法,可以便捷的调用实现了IDisposable的对象的Dispose方法。比如:

using (FileStream fs = new FileStream ("myFile.txt", FileMode.Open)) { // ... Write to the file ... }

编译后的代码与下面的代码是一样的:

FileStream fs = new FileStream ("myFile.txt", FileMode.Open); try { // ... Write to the file ... } finally { if (fs != null) ((IDisposable)fs).Dispose(); }

finally语句确保了Dispose方法的调用,及时发生了异常,或者代码在try语句中提前返回。

在简单的场景中,创建自定义的可销毁的类型值需要实现IDisposable接口即可

sealed class Demo : IDisposable { public void Dispose() { // Perform cleanup / tear-down. ... } }

请注意,对于sealed类,上述模式非常适合。在本章后面,我们会介绍另外一种销毁对象的模式。对于非sealed类,我们强烈建议时候后面的那种销毁对象模式,否则在非sealed类的子类中,也希望实现销毁时,会发生非常诡异的问题。

对象销毁的标准语法

Framework在销毁对象的逻辑方面遵循一套规则,这些规则并不限用于.NET Framework或C#语言;这些规则的目的是定义一套便于使用的协议。这些协议如下:

  • 一旦销毁,对象不可恢复。对象不能被再次激活,调用对象的方法或者属性抛出ObjectDisposedException异常
  • 重复地调用对象的Disposal方法会导致错误
  • 如果一个可销毁对象x包含,或包装,或处理另外一个可销毁对象y,那么x的Dispose方法自动调用x的Dispose方法,除非另有指令(不销毁y)

这些规则同样也适用于我们平常创建自定义类型,尽管它并不是强制性的。没有谁能阻止你编写一个不可销毁的方法;然而,这么做,你的同事也许会用高射炮攻击你。

对于第三条规则,一个容器对象自动销毁其子对象。最好的一个例子就是,windows容器对象比如Form对着Panel。一个容器对象可能包含多个子控件,那你也不需要显示地销毁每个字对象:关闭或销毁父容器会自动关闭其子对象。另外一个例子就是如果你在DeflateStream包装了FileStream,那么销毁DeflateStream时,FileStream也会被销毁--除非你在构造器中指定了其他的指令。

Close和Stop

有一些类型除了Dispose方法之外,还定义了Close方法。Framework对于Close方法并没有保持完全一致性,但在几乎所有情况下,它可以:

  • 要么在功能上与Dispose一致
  • 或只是Dispose的一部分功能

对于后者一个典型的例子就是IDbConnecton类型,一个Closed的连接可以再次被打开;而一个Disposed的连接对象则不能。另外一个例子就是Windows程序使用ShowDialog的激活某个窗口对象:Close方法隐藏该窗口;而Dispose释放窗口所使用的资源。

有一些类定义Stop方法(比如Timer或HttpListener)。与Dipose方法一样,Stop方法可能会释放非托管资源;但是与Dispose方法不同的是,它允许重新启动。

何时销毁对象

销毁对象应该遵循的规则是“如有疑问,就销毁”。一个可以被销毁的对象--如果它可以说话--那么将会说这些内容:

“如果你结束对我的使用,那么请让我知道。如果只是简单地抛弃我,我可能会影响其他实例对象、应用程序域、计算机、网络、或者数据库”

如果对象包装了非托管资源句柄,那么经常会要求销毁,以释放句柄。例子包括Windows Form控件、文件流或网络流、网络sockets,GDI+画笔、GDI+刷子,和bitmaps。与之相反,如果一个类型是可销毁的,那么它会经常(但不总是)直接或间接地引用非托管句柄。这是由于非托管句柄对操作系统资源,网络连接,以及数据库锁之外的世界提供了一个网关(出入口),这就意味着使用这些对象时,如果不正确的销毁,那么会对外面的世界代码麻烦。

但是,遇到下面三种情形时,要销毁对象

  • 通过静态成员或属性获取一个共享的对象
  • 如果一个对象的Dispose方法与你的期望不一样
  • 从设计的角度看,如果一个对象的Dispose方法不必要,且销毁对象给程序添加了复杂度

第一种情况很少见。多数情形都可以在System.Drawing命名空间下找到:通过静态成员或属性获取的GDI+对象(比如Brushed.Blue)就不能销毁,这是因为该实现在程序的整个生命周期中都会用到。而通过构造器得到的对象实例,比如new SolidBrush,就应该销毁,这同样适用于通过静态方法获取的实例对象(比如Font.FromHdc)。

第二种情况就比较常见。下表以System.IO和System.Data命名空间下类型举例说明

类型销毁功能何时销毁
MemoryStream防止对I/O继续操作当你需要再次读读或写流
StreamReader,
StreamWriter
清空reader/writer,并关闭底层的流当你希望底层流保持打开时(一旦完成,你必须改为调用StreamWriter的Flush方法)
IDbConnection释放数据库连接,并清空连接字符串如果你需要重新打开数据库连接,你需要调用Close方法而不是Dispose方法
DataContext
(LINQ to SQL)
防止继续使用当你需要延迟评估连接到Context的查询

第三者情况包含了System.ComponentModel命名空间下的这几个类:WebClient, StringReader, StringWriter和BackgroundWorker。这些类型有一个共同点,它们之所以是可销毁的是源于它们的基类,而不是真正的需要进行必要的清理。如果你需要在一个方法中使用这样的类型,那么在using语句中实例化它们就可以了。但是,如果实例对象需要持续一段较长的时间,并记录何时不再使用它们以销毁它们,就会给程序带来不惜要的复杂度。在这样的情况下,那么你就应该忽略销毁对象。

选择性地销毁对象

正因为IDisposable实现类可以使用using语句来实例化,因而这可能很容易导致该实现类的Dispose方法延伸至不必要的行为。比如:

public sealed class HouseManager : IDisposable { public void Dispose() { CheckTheMail(); } ... }

想法是该类的使用者可以选择避免不必要的清理--简单地说就是不调用Dispose方法。但是,这就需要调用者知道HouseManager类Dispose方法的实现细节。及时是后续添加了必要的清理行为也破坏了规则。

public void Dispose() { CheckTheMail(); // Nonessential LockTheHouse(); // Essential }

在这种情况下,就应该使用选择性销毁模式

public sealed class HouseManager : IDisposable { public readonly bool CheckMailOnDispose; public Demo (bool checkMailOnDispose) { CheckMailOnDispose = checkMailOnDispose; } public void Dispose() { if (CheckMailOnDispose) CheckTheMail(); LockTheHouse(); } ... }

这样,任何情况下,调用者都可以调用Dispose--上述实现不仅简单,而且避免了特定的文档或通过反射查看Dispose的细节。这种模式在.net中也有实现。System.IO.Compression空间下的DeflateStream类中,它的构造器如下

public DeflateStream (Stream stream, CompressionMode mode, bool leaveOpen)

非必要的行为就是在销毁对象时关闭内在的流(第一个参数)。有时候,你希望内部流保持打开的同时并销毁DeflateStream以执行必要的销毁行为(清空bufferred数据)

这种模式看起来简单,然后直到Framework 4.5,它才从StreamReader和StreamWriter中脱离出来。结果却是丑陋的:StreamWriter必须暴露另外一个方法(Flush)以执行必要的清理,而不是调用Dispose方法(Framework 4.5在这两个类上公开一个构造器,以允许你保持流处于打开状态)。System.Security.Cryptography命名空间下的CryptoStream类,也遭遇了同样的问题,当需要保持内部流处于打开时你要调用FlushFinalBlock销毁对象。

销毁对象时清除字段

在一般情况下,你不要在对象的Dispose方法中清除该对象的字段。然而,销毁对象时,应该取消该对象在生命周期内所有订阅的事件。退订这些事件避免了接收到非期望的通知--同时也避免了垃圾回收器继续对该对象保持监视。

设置一个字段用以指明对象是否销毁,以便在使用者在该对象销毁后访问该对象抛出一个ObjectDisposedException,这是非常值得做的。一个好的模式就是使用一个public的制度的属性:

public bool IsDisposed { get; private set; }

尽管技术上没有必要,但是在Dispose方法清除一个对象所拥有的事件句柄(把句柄设置为null)也是非常好的一种实践。这消除了在销毁对象期间这些事件被触发的可能性。

偶尔,一个对象拥有高度秘密,比如加密密钥。在这种情况下,那么在销毁对象时清除这样的字段就非常有意义(避免被非授权组件或恶意软件发现)。System.Security.Cryptography命令空间下的SymmetricAlgorithm类就属于这种情况,因此在销毁该对象时,调用Array.Clear方法以清除加密密钥。

自动垃圾回收机制

无论一个对象是否需要Dispose方法以实现销毁对象的逻辑,在某个时刻,该对象在堆上所占用的内存空间必须释放。这一切都是由CLR通过GC自动处理. 你不需要自己释放托管内存。我们首先来看下面的代码

public void Test() { byte[] myArray = new byte[1000]; }

当Test方法执行时,在内存的堆上分配1000字节的一个数组;该数组被变量myArray引用,这个变量存储在变量栈上。当方法退出后,局部变量myArray就失去了存在的范畴,这也意味着没有引用指向内存堆上的数组。那么该孤立的数组,就非常适合通过垃圾回收机制进行回收。

垃圾回收机制并不会在一个对象变成孤立的对象之后就立即执行。与大街上的垃圾收集不一样,.net垃圾回收是定期执行,尽享不是按照一个估计的计划。CLR决定何时进行垃圾回收,它取决于许多因素,比如,剩余内存,已经分配的内存,上一次垃圾回收的时间。这就意味着,在一个对象被孤立后到期占用的内存被释放之间,有一个不确定的时间延迟。该延迟的范围可以从几纳秒到数天。

垃圾回收和内存占用
垃圾收集试图在执行垃圾回收的时间与程序的内存占用之间建立一个平衡。因此,程序可以占用比它们实际需要更多的内存,尤其特现在程序创建的大的临时数组。
你可以通过Windows任务管理器监视某一个进程内存的占用,或者通过编程的方式查询性能计数器来监视内存占用:

// These types are in System.Diagnostics: string procName = Process.GetCurrentProcess().ProcessName; using (PerformanceCounter pc = new PerformanceCounter ("Process", "Private Bytes", procName)) Console.WriteLine (pc.NextValue());

上面的代码查询内部工作组,返回你当前程序的内存占用。尤其是,该结果包含了CLR内部释放,以及把这些资源让给操作系统以供其他的进程使用。

根就是指保持对象依然处于活着的事物。如果一个对象不再直接或间接地被一个根引用,那么该对象就适合于垃圾回收。

一个跟可以是:

  • 一个正在执行的方法的局部变量或参数(或者调用栈中任意方法的局部变量或参数)
  • 一个静态变量
  • 存贮在结束队列中的一个对象

正在执行的代码可能涉及到一个已经删除的对象,因此,如果一个实例方法正在执行,那么该实例方法的对象必然按照上述方式被引用。

请注意,一组相互引用的对象的循环被视作无根的引用。换一种方式,也就是说,对象不能通过下面的箭头指向(引用)而从根获取,这也就是引用无效,因此这些对象也将被垃圾回收器处理。

销毁 c# -- 对象销毁和垃圾回收

Finalizers

在一个对象从内存释放之前,如果对象包含finalizer,那么finalizer开始运行。一个finalizer的声明类似构造器函数,但是它使用~前缀符号

class Test {  ~Test()  {  // finalizer logic ...  } }

(尽管与构造器的声明相似,finalizer不能被声明为public或static,也不能有参数,还不能调用其基类)

Finalizer是可能的,因为垃圾收集工作在不同的时间段。首先,垃圾回收识别没有使用的对象以删除该对象。这些待删除的对象如果没有Finalizer那么就立即删除。而那些拥有finalizer的对象会被保持存活并存在放到一个特殊的队列中。

在这一点上,当你的程序在继续执行的时候,垃圾收集也是完整的。而Finalizer线程却在你程序运行时,自动启动并在另外一个线程中并发执行,收集拥有Finalizer的对象到特殊队列,然后执行它们的终止方法。在每个对象的finalizer方法执行之前,它依然非常活跃--排序行为视作一个跟对象。而一档这些对象被移除队列,并且这些对象的fainalizer方法已经执行,那么这些对象就变成孤立的对象,会在下一阶段的垃圾回收过程中被回收。

Finalizer非常有用,但它们也有一些限制:

  • Finalizer减缓内存分配和收集(因为GC需要追踪那些Finalizer在运行)
  • Finalizer延长对象及其所引用对象的生命周期(这些对象只有在下一次垃圾回收运行过程中被真正地删除)
  • 对于一组对象,Finalizer的调用顺序是不可预测的
  • 你不能控制一个对象的finalizer何时被调用
  • 如果一个对象的finalizer被阻塞,那么其他对象不能处置(Finalized)
  • 如果程序没有卸载(unload)干净,那么finalizer会被忽略

总之,finalizer在一定程度上就好比律师--一旦有诉讼那么你确实需要他们,一般你不想使用他们,除非万不得已。如果你使用他们,那么你需要100%确保你了解他们会为你做什么。

下面是实施finalizer的一些准则:

  • 确保finalizer快速执行
  • 绝对不要在finalier中使用阻塞
  • 不要引用其他可finalizable对象
  • 不要抛出异常

在Finalizer中调用Dispose

一个流行的模式是使finalizer调用Dispose方法。这么做是有意义的,尤其是当清理工作不是紧急的,并且通过调用Dispose加速清理;那么这样的方式更多是一个优化,而不是一个必须。

下面的代码展示了该模式是如何实现的

class Test : IDisposable { public void Dispose() // NOT virtual { Dispose (true); GC.SuppressFinalize (this); // Prevent finalizer from running. } protected virtual void Dispose (bool disposing) { if (disposing) { // Call Dispose() on other objects owned by this instance. // You can reference other finalizable objects here. // ... } // Release unmanaged resources owned by (just) this object. // ... } ?Test() { Dispose (false); } }

Dispose方法被重载,并且接收一个bool类型参数。而没有参数的Dispose方法并没有被声明为virtual,只是在该方法内部调用了带参数的Dispose方法,且传递的参数的值为true。

带参数的Dispose方法包含了真正的处置对象的逻辑,并且它被声明为protected和virtual。这样就可以保证其子类可以添加自己的处置逻辑。参数disposing标记意味着它在Dispose方法中被正确的调用,而不是从finalizer的最后采取模式所调用。这也就表明,如果调用Dispose时,其参数disposing的值如果为false,那么该方法,在一般情况下,都会通过finalizer引用其他对象(因为,这样的对象可能自己已经被finalized,因此处于不可预料的状态)。这里面涉及的规则非常多!当disposing参数是false时,在最后采取的模式中,仍然会执行两个任务:

释放对操作系统资源的直接引用(这些引用可能是因为通过P/Invoke调用Win32 API而获取到)

删除由构造器创建的临时文件

为了使这个模式更强大,那么任何会抛出异常的代码都应包含在一个try/catch代码块中;而且任何异常,在理想状态下,都应该被记录。此外,这些记录应当今可能既简单又强大。

请注意,在无参数的Dispose方法中,我们调用了GC.SuppressFinalize方法,这会使得GC在运行时,阻止finalizer执行。从技术角度讲,这没有必要,因为Dispose方法必然会被重复调用。但是,这么做会改进性能,因为它允许对象(以及它所引用的对象)在单个循环中被垃圾回收器回收。

复活

假设一个finalizer修改了一个活的对象,使其引用了一个“垂死”对象。那么当下一次垃圾回收发生时,CLR会查看之前垂死的对象是否确实没有任何引用指向它--从而确定是否对其执行垃圾回收。这是一个高级的场景,该场景被称作复活(resurrection)。

为了证实这点,假设我们希望创建一个类管理一个临时文件。当类的实例被回收后,我们希望finalizer删除临时文件。这看起来很简单

public class TempFileRef { public readonly string FilePath; public TempFileRef (string filePath) { FilePath = filePath; } ~TempFileRef() { File.Delete (FilePath); } }

实际,上诉代码存在bug,File.Delete可能会抛出一个异常(引用缺少权限,或者文件处于使用中) 。这样的异常会导致拖垮整个程序(还会阻止其他finalizer执行)。我们可以通过一个空的catch代码块来“消化”这个异常,但是这样我们就不能获取任何可能发生的错误。 调用其他的错误报告API也不是我们所期望的,因为这么做会加重finalizer线程的负担,并且会妨碍对其他对象进行垃圾回收。 我们期望显示finalization行为简单、可靠、并快速。

一个好的解决方法是在一个静态集合中记录错误信息:

public class TempFileRef { static ConcurrentQueue<TempFileRef> _failedDeletions = new ConcurrentQueue<TempFileRef>(); public readonly string FilePath; public Exception DeletionError { get; private set; } public TempFileRef (string filePath) { FilePath = filePath; } ~TempFileRef() { try { File.Delete (FilePath); } catch (Exception ex) { DeletionError = ex; _failedDeletions.Enqueue (this); // Resurrection } } }

把对象插入到静态队列_failedDeletions中,使得该对象处于引用状态,这就确保了它仍然保持活着的状态,直到该对象最终从队列中出列。

GC.ReRegisterForFinalize

一个复活对象的finalizer不会再次运行--除非你调用GC.ReRegisterForFinalize

在下面的例子中,我们试图在一个finalizer中删除一个临时文件。但是如果删除失败,我们就重新注册带对象,以使其在下一次垃圾回收执行过程中被回收。

public class TempFileRef { public readonly string FilePath; int _deleteAttempt; public TempFileRef (string filePath) { FilePath = filePath; } ~TempFileRef() { try { File.Delete (FilePath); } catch { if (_deleteAttempt++ < 3) GC.ReRegisterForFinalize (this); } } }

如果第三次尝试失败后,finalizer会静悄悄地放弃删除临时文件。我们可以结合上一个例子增强该行为--换句话说---那就是在第三次失败后,把该对象加入到_failedDeletions队列中。

垃圾回收工作原理

标准的CLR使用标记和紧凑的GC对存储托管堆上的对象执行自动内存管理。GC可被视作一个可被追踪的垃圾回收器,在这个回收器中,它(GC)不与任何对象接触;而是被间歇性地被唤醒,然后跟踪存储在托管堆对象图,以确定哪些对象可以被视为垃圾,进而对这些对象执行垃圾回收。

当(通过new关键字)执行内存分配是,或当已经分配的内存达到了某一阀值,亦或当应用程序占用的内存减少时,GC启动一个垃圾收集。这个过程也可以通过手动调用System.GC.Collect方法启动。在一个垃圾回收过程中,所有线程都可能被冻结。

GC从根对象引用开始,查找贵根对象对应的整个对象图,然后把所有的对象标记为可访问的对象。一旦这个过程完成,所有被标记为不再使用的对象,将被垃圾回收器回收。

没有finalizer的不再使用的对象立即被处置;而拥有finalizer的不再使用对象将会在GC完成之后,在finalizer线程上排队以等待处理。这些对象(在finalizer线程上排队的对象)会在下一次垃圾回收过程中被回收(除非它们又复活了)。

而那些剩余的“活”对象(还需要使用的对象),被移动到堆叠开始位置(压缩),这样以腾出更多空间容纳更多对象。改压缩过程有两个目的:其一是避免了内存碎片,这样就使得在为新对象分配空间后,GC只需使用简单的策略即可,因为新的对象总是分配在堆的尾部。其二就是避免了维护一个非常耗时的内存片段列表任务。

在执行完一次垃圾回收之后,为新对象分配内存空间时,如果没有足够的空间可以使用,操作系统不能确保更多的内存使用时,抛出OutOfMemoryException。

优化技术

GC引入了各种优化技术来减少垃圾回收的时间。

通用垃圾回收

最重要的优化就是垃圾回收时通用的。其优点是:尽管快速分配和处置大量对象,某些对象是长存内存,因此他们不需要被垃圾回收追踪。

基本上,GC把托管堆分为三类:Gen0是在堆上刚刚分配的对象;Gen1经过一次垃圾回收后仍然存活的对象;剩余的为Gen2。

CLR限制Gen0的大小(在32位CLR中,最大16MB,一般大小为数百KB到几MB)。当Gen0空间耗尽,GC便触发一个Gen0垃圾回收--该垃圾回收发生非常频繁。对于Gen1,GC也应用了一个相似的大小限制,因为Gen1垃圾回收也是相当频繁并且快速完成。Gen2包含了所有类型的垃圾回收,然而,发生在Gen2的垃圾回收执行时间长,并且也不会经常发生。下图展示了一个完全垃圾回收:

销毁 c# -- 对象销毁和垃圾回收

如果真要列出一组大概的数字,那么Gen0垃圾回收执行耗费少于1毫秒,在一个应用程序中一般不会被注意到。而全垃圾回收,如果程序包含大的图形对象,则可能会耗费100毫秒。执行时间受诸多因素影响二次可能会有不同,尤其是Gen2的垃圾回收,它的尺寸是没有限定的。

段时间存活的对象,如果使用GC会非常有效。比如下面示例代码中的StringBuilder,就会很快地被发生在Gen0上的垃圾回收所回收。

string Foo() { var sb1 = new StringBuilder ("test"); sb1.Append ("..."); var sb2 = new StringBuilder ("test"); sb2.Append (sb1.ToString()); return sb2.ToString(); }

大对象堆

GC为大对象(大小超过85,000字节)使用单独的堆。这就避免了大量消耗Gen0堆。因为在Gen0上没有大对象,那么就不会出现分配一组16MB的对象(这些对象由大对象组成)之后,马上触发垃圾回收。

大对象堆不适合于压缩,这是因为发生垃圾回收时,移动内存大块的代价非常高。如果这么做,会带来下面两个后果:

  • 内存分配低效,这是因为GC不能总是把对象分配在堆的尾部,它还必须查看中间的空隙,那么这就要求维护一个空白内存块链表。
  • 大对象堆适合于片段化。这意味着冻结一个对象,会在大对象堆上生成一个空洞,这个空洞很难在再被填充。比如,一个空洞留下了86000字节的空间,那么这个空间就只能被一个85000字节或86000自己的对象填充(除非与另外的一个空洞连接在一起,形成更大的空间)

大对象堆还是非通用的堆,大对象堆上的所有对象被视作Gen2

并发回收和后台回收

GC在执行垃圾回收时,必须释放(阻塞)你的程序所使用的线程。在这个期间包含了Gen0发生的时间和Gen1发生的时间。

由于执行Gen2回收可能占用较长的时间,因此GC会在你的程序运行时,堆Gen2回收进行特殊的尝试。该优化技术仅应用于工作站的CLR平台,一般应用于windows桌面系统(以及所有运行独立程序的Windows)。原因是由于阻塞线程进行垃圾回收所带来的延迟对于没有用户接口的服务器应用程序一般不会带来问题。

这种对于工作站的优化历史上称之为并发回收。从CLR4.0kaishi ,它发生了革新并重命名为后台回收。后台回收移除了一个限制,由此,并发回收不再是并发的,如果Gen0部分已经执行完而Gen2回收还正在执行。这就意味着,从CLR4.0开始,持续分配内存的应用程序会更加敏感。

GC通知(适用于服务端CLR)

从Framework 3.5 SP1开始,服务器版本的CLR在一个全GC将要发生时,向你发送通知。你可以在服务器池配置中配置该特性:在一个垃圾回收执行之前,把请求转向到另外一台服务器。然后你立即调查垃圾回收,并等待其完成,在垃圾回收执行完成之后,把请求转回到当前服务器。

通过调用GC.RegisterForFullGCNotification,可以启用GC通知。然后,启动另外一个线程,该线程首先调用GC.WaitForFullGCApproach,当该方法返回GCNotificationStatus指明垃圾回收已经进入等待执行的队列,那么你就可以把请求转向到其他的服务器,然后手执行一次手动垃圾回收(见下节)。然后,你调用GC.WaitForFullGCComplete方法,当该方法返回时,GC完成;那么该服务器就可以开始再次接收请求。然后在有需要的时候,你可以再次执行上述整个过程。

强制垃圾回收

通过调用GC.Collect方法,你可以随时手动强制执行一次垃圾回收。调用GC.Collect没有提供任何参数会执行一次完全垃圾回收。如果你提供一个整数类型的参数,那么执行对应的垃圾回收。比如GC.Collect(0)执行Gen0垃圾回收。

// Forces a collection of all generations from 0 through Generation. // public static void Collect(int generation) {  Collect(generation, GCCollectionMode.Default) } // Garbage Collect all generations. // [System.Security.SecuritySafeCritical] // auto-generated public static void Collect() {  //-1 says to GC all generations.  _Collect(-1, (int)InternalGCCollectionMode.Blocking); }

一般地,允许GC去决定何时执行垃圾回收可以得到最好的性能;这是因为强制垃圾回收会把Gen0的对象不必要地推送到Gen1(Gen1不必要地推送到Gen2),从而影响性能。这还会扰乱GC自身的调优能力--在程序运行时,GC动态地调整每种垃圾回收的临界值以最大限度地提高性能。

但是,也有另外。最常见的可以执行手动垃圾回收的场景就是当一个应用程序进入休眠状态,比如执行日常工作的windows服务。这样的程序可能使用了System.Timters.Timer以每隔24小时触发一次行为。当该行为完成之后,在接着的24小时之内没有任何代码会执行,那就意味着,在这段时间内,不会分配任何内存,因此GC就没有机会被激活。服务在执行时所消耗的任何内存,在接着的24小时都会被持续占用--甚至是空对象图。那么解决方法就是在日常的行为完成之后调用GC.Collect()方法进行垃圾回收。

为了回收由于finalizer延迟回收的对象,你可以添加一行额外的代码以调用WaitForPendingFinalizers,然后再调用一次垃圾回收

GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();

另外一种调用GC.Collect方法的场景是当你测试一个有Finazlier的类时。

内存压力

.NET运行时基于一些列因素决定何时启动垃圾回收,其中一个因素就是机器内存的总负载。 如果程序使用了非托管内存,那么运行时会对其内存的使用情况持盲目地乐观的态度,这是因为CLR之关心托管内存。通过告诉CLR已经分配了特定量的非托管内存内存,来减轻CLR的盲目性;调用CG.AddMemoryPresure方法可以完成该目的。如果取消该行为(当所占用的托管内存已经被释放),那么可以调用GC.RemoveMemoryPressure。

管理内存泄漏

在非托管语言中,比如C++,你必须记住当对象不再使用时,应手动地释放内存;否则,将导致内存泄漏。在托管世界中,内存泄漏这种错误时不可能发生的,这归功于CLR的自动垃圾回收。

尽管如此,大型的和复杂的.NET程序也会出现内存泄漏;只不错内存泄漏的方式比较温和,但具有相同的症状和结果:在程序的生命周期内,它消耗越来越多的内存,到最后导致程序重启。好消息是,托管内存泄漏通常容易诊断和预防。

托管内存泄漏是由不再使用的活对象引起,这些对象之所以存活是凭借不再使用引用或者被遗忘的引用。一种常见的例子就是事件处理器--它们堆目标对象保存了一个引用(除非目标是静态方法)。比如,下面的类:

class Host { public event EventHandler Click; } class Client { Host _host; public Client (Host host) { _host = host; _host.Click += HostClicked; } void HostClicked (object sender, EventArgs e) { ... } }

下面的测试类包含1个方法实例化了1000个Client对象

class Test { static Host _host = new Host(); public static void CreateClients() { Client[] clients = Enumerable.Range (0, 1000) .Select (i => new Client (_host)) .ToArray(); // Do something with clients ... } }

你可能会认为,当CeateClients方法结束后,这个1000个Client对象理解适用于垃圾回收。很不幸,每个Client对象都包含一个引用:_host对象,并且该对象的Click事件引用每个Client实例。 如果Click事件不触发,那么就不会引起注意,或者HostClicked方法不做任何事情也不会引起注意。

解决这个问题的一种方式就是使Client类实现接口IDisposable,并且在dispose方法中,移除时间处理器

public void Dispose() { _host.Click -= HostClicked; }

Client实例的使用者,在使用完实例之后,调用Client类的dispose方法处置该实例

Array.ForEach (clients, c => c.Dispose());

下面的对比展示两种方式的差别

CLR Profiler
Index
实现IDisposable未实现IDisposable
Time line销毁 c# -- 对象销毁和垃圾回收销毁 c# -- 对象销毁和垃圾回收
Heap statistics销毁 c# -- 对象销毁和垃圾回收销毁 c# -- 对象销毁和垃圾回收
GC Generatation Sizes销毁 c# -- 对象销毁和垃圾回收销毁 c# -- 对象销毁和垃圾回收

计时器

不要忘记timmers也会引起内存泄漏。根据计时器的种类,会引发两种不同的内存泄漏。首先我们来看System.Timers命名空间下的计时器。在下面的例子中,Foo类每秒调用一次tmr_Elapsed方法

using System.Timers; class Foo { Timer _timer; Foo() { _timer = new System.Timers.Timer { Interval = 1000 }; _timer.Elapsed += tmr_Elapsed; _timer.Start(); } void tmr_Elapsed (object sender, ElapsedEventArgs e) { ... } }

很不幸,Foo的实例决定不会被回收。原因在于.NET Framework本身持有对计活动的时器的引用,从而导致.net framework会触发这些计时器的Elapsed事件。因此

  • .NET Framework将使_timer处于活动状态
  • 通过tmr_Elapsed事件处理器,_timer将使Foo实现处于活动状态

当你意识到Timer实现了IDisposable接口之后,解决的方法就在也明显不过了。处置Timer实例以停止计时器,并确保.NET Framework不再引用该计时器对象。

class Foo : IDisposable { ... public void Dispose() { _timer.Dispose(); } }

相对于我们上面讨论的内容,WPF和Windows窗体的计时器表现出完全相同的方式。

然而,System.Threading命名空间下的计时器确是一个特例。.NET Framework没有引用活动线程计时器;想法,却直接引用回调代理。这就意味着如果你忘记处置线程计时器,那么finalizer会自动触发并停止计时器然后处置该计时器。比如:

static void Main() { var tmr = new System.Threading.Timer (TimerTick, null, 1000, 1000); GC.Collect(); System.Threading.Thread.Sleep (10000); // Wait 10 seconds } static void TimerTick (object notUsed) { Console.WriteLine ("tick"); }

如果上面的代码编译为发布模式,那么计时器会被回收,并且在它再次触发之前被处置(finalized)。同样地,我们可以在计时器结束后通过处置该计数器以修复这个问题

using (var tmr = new System.Threading.Timer (TimerTick, null, 1000, 1000)) { GC.Collect(); System.Threading.Thread.Sleep (10000); // Wait 10 seconds }

using语句会隐式地调用tmr.Dispose方法,以确保tmr变量确实处于“使用(活动状态)”;因此不会在代码块结束之前被当作是死对象。讽刺的是,调用Dispose方法实际上使对象存活的时间更长了。

诊断内存泄漏

避免托管内存泄漏的最简单方式就是在编写应用程序时就添加监控内存占用。你可以在程序中通过调用下面的代码来获取当前内存的使用情况

long memoryUsed = GC.GetTotalMemory (true);

如果你采取测试驱动开发,那么你可以使用单元测试判断是否按照期望释放了内存。入股这样的判断失败,那么接着你就应该检查你最近对程序所作的修改。

如果你已经有一个大型程序,并且该程序存在托管内存泄漏问题,那么你应该使用windgb.exe工具来帮助你解决问题。当然你还可以使用其他的图形化工具,比如CLR Profiler, SciTech的Memory Profiler,或者Red Gate的ANTS Memory Profiler。

弱引用

有时候,引用一个对GC而言是“隐形”的对象,并且对象保持活动状态,这非常有用。这既是弱引用,它由System.WeakReference类实现。使用WeakReference,使用其构造器函数并传入目标对象。

var sb = new StringBuilder ("this is a test"); var weak = new WeakReference (sb); Console.WriteLine (weak.Target); // This is a test

如果目标对象仅仅由一个或多个弱引用所引用,那么GC会把其加入到垃圾回收队列中。如果目的对象被回收,那么WeakReference的Target属相则为NULL。

var weak = new WeakReference(new StringBuilder("weak")) Console.WriteLine(weak.Target); // weak GC.Collect(); Console.WriteLine(weak.Target == null); // (true)

为了避免目标对象在测试其为null和使用目标对象之间被回收,把目标对象分配给一个局部变量

var weak = new WeakReference (new StringBuilder ("weak")); var sb = (StringBuilder) weak.Target; if (sb != null) { /* Do something with sb */ }

一旦目标对象分配给一个局部变量,那么目的对象就有了一个强类型根对象,从而在局部变量使用期间不会被回收。

下面例子中的类通过弱引用追踪所有被实例化的Widget对象,从而使这些实例不会被回收

class Widget { static List<WeakReference> _allWidgets = new List<WeakReference>(); public readonly string Name; public Widget (string name) { Name = name; _allWidgets.Add (new WeakReference (this)); } public static void ListAllWidgets() { foreach (WeakReference weak in _allWidgets) { Widget w = (Widget)weak.Target; if (w != null) Console.WriteLine (w.Name); } } }

这样一个系统的唯一缺点就是,静态列表会随着时间推移而增加,逐渐累积对应null对象的弱引用。因此,你需要自己实现一些清理策略。

弱引用和缓存

使用弱引用的目的之一是为了缓存大对象图。通过弱引用,使得耗费内存的数据可以进行简要的缓存而不是造成内存的大量占用。

_weakCache = new WeakReference (...); // _weakCache is a field ... var cache = _weakCache.Target; if (cache == null) { /* Re-create cache & assign it to _weakCache */ }

在实际上,该策略只会发挥一半的作用,这是因为你不能控制GC何时运行,并且也不能控制GC会会执行哪一类回收。尤其是,当你的缓存是在Gen0中,那么这类内存会在微妙级别类被回收。因此,至少,你需要使用两类缓存,通过它们,首先你拥有一个强类型,然后不时地把该强类型转换成弱类型。

弱引用和事件

在前面的章节中,我们看到事件是如何引起内存泄漏。而且解决这种内存泄漏的最简单方法是避免时间订阅,或者对为订阅事件的对象实现Dispose方法。此外,弱引用也提供了另外一种解决方案。

假设一个带来对其目标持有一个弱引用。那么这样的一个代理并不会使其目标为活动状态,除非这些目标对象有独立的引用。当然,这并不会阻止一个被触发的代理,在目标对象进入回收队列之后但在GC开始对该目标对象执行回收前的时间段中,击中一个未被引用的目标。为了该方法高效,你的代码必须非常稳定。下面的代码就是就是采用这种方式的具体实现:

public class WeakDelegate<TDelegate> where TDelegate : class { class MethodTarget { public readonly WeakReference Reference; public readonly MethodInfo Method; public MethodTarget (Delegate d) { Reference = new WeakReference (d.Target); Method = d.Method; } } List<MethodTarget> _targets = new List<MethodTarget>(); public WeakDelegate() { if (!typeof (TDelegate).IsSubclassOf (typeof (Delegate))) throw new InvalidOperationException ("TDelegate must be a delegate type"); } public void Combine (TDelegate target) { if (target == null) return; foreach (Delegate d in (target as Delegate).GetInvocationList()) _targets.Add (new MethodTarget (d)); } public void Remove (TDelegate target) { if (target == null) return; foreach (Delegate d in (target as Delegate).GetInvocationList()) { MethodTarget mt = _targets.Find (w => d.Target.Equals (w.Reference.Target) && d.Method.MethodHandle.Equals (w.Method.MethodHandle)); if (mt != null) _targets.Remove (mt); } } public TDelegate Target { get { var deadRefs = new List<MethodTarget>(); Delegate combinedTarget = null; foreach (MethodTarget mt in _targets.ToArray()) { WeakReference target = mt.Reference; if (target != null && target.IsAlive) { var newDelegate = Delegate.CreateDelegate ( typeof (TDelegate), mt.Reference.Target, mt.Method); combinedTarget = Delegate.Combine (combinedTarget, newDelegate); } else deadRefs.Add (mt); } foreach (MethodTarget mt in deadRefs) // Remove dead references _targets.Remove (mt); // from _targets. return combinedTarget as TDelegate; } set { _targets.Clear(); Combine (value); } } }

上述代码演示了许多C#和CLR的有趣的地方。首先,我们在构造器中检查了TDelegate是一个代理类型。这是因为C#本身的限制--因为下面的语句不符合C#的语法

... where TDelegate : Delegate // Compiler doesn't allow this

由于必须要进行类型限制,所以我们在构造器中执行运行时检查。

在Combine方法和Remove方法中,我们执行了引用转换,通过as运算符(而没有使用更常见的转换符)把target对象转换成Delegate类型。这是由于C#不允许转换符使用类型参数--因为它不能分清这是一个自定义的转换还是一个引用抓换(下面的代码不能拖过编译)。

foreach(Delegate d in ((Delegate)target).GetInvocationList())  _targets.Add(new MethodTarget(d));

当调用GetInvocationList,由于这些方法可能被一个多播代理调用,多播代理就是一个代理有多余一个的方法接收。

对于Target属性,我们使其为一个多播代理--通过一个弱引用包含所有的代理引用,从而使其目标对象保持活动。然后我们清楚剩余的死引用,这样可以避免_targets列表无限制的增长。下面的代码演示了如何使用我们上面创建的实现了事件的代理类:

public class Foo { WeakDelegate<EventHandler> _click = new WeakDelegate<EventHandler>(); public event EventHandler Click { add { _click.Combine (value); } remove { _click.Remove (value); } } protected virtual void OnClick (EventArgs e) { EventHandler target = _click.Target; if (target != null) target (this, e); } }

请注意,在触发事件时,在检查和调用之前,我们把_click.Target对象赋值给一个临时变量。这就避免了目标对象被GC回收的可能性。

参考

http://msdn.microsoft.com/en-US/library/system.idisposable.aspx

三 : Flash AS3 垃圾回收机制详解

Part1:

目前我暂时在研究ActionScript3.0,它的能力让我很激动。(www.61k.com)它的原生执行速度带来诸多可能(此句原 文The raw execution speed by itself provides so many possibilities. raw本意未加工,原始的,这里的意思是指引入AVM2之后,ActionScript3.0在执行速度上有了很大提高,所以使支持更复杂的组件成为可 能,译者注)。它引入了E4X、sockets、byte 数组对象、新的显示列表模型、正则表达式、正式化的事件和错误模型以及其它特性,它是一个令人炫目的大杂烩。
能 力越大责任越大(译者:出自蜘蛛侠),这对ActionScript3.0来说一点没错。引入这些新控件带来一个副作用:垃圾收集器不再支持自动为你收集 垃圾等假设。也就是说Flash开发者转到ActionScript3.0之后需要对关于垃圾收集如何工作以及如何编程使其工作更加有效具备较深入的理 解。没有这方面的知识,即使创建一个看起来很简单的游戏或应用程序也会出现SWF文件内存泄露、耗光所有系统资源(CPU/内存)导致系统挂起甚至机器重启。
  要理解如何优化你的ActionScript3.0代码,你首先要理解垃圾收集器如何在FlashPlayer 9中工作。Flash有两种方法来查找非活动对象并移除它们。本文解释这两种技术并描述它们如何影响你的代码。  本文结尾你会找到一个运行在FlashPlayer9中的垃圾收集器模拟程序,它生动演示了这里解释过的概念。
关于垃圾收集器  垃圾收集器是一个后台进程它负责回收程序中不再使用的对象占用的内存。非活动对象就是不再有任何其他活动对象引用 它。为便于理解这个概念,有一点非常重要,就是要意识到除了非原生类型(Boolean, String, Number, uint, int除外),你总是通过一个句柄访问对象,而非对象本身。当你删除一个变量其实就是删除一个引用,而非对象本身。  以下代码很容易说明这一点: ActionScript代码

  1. // create a new object, and put a reference to it in a:
  2. var a:Object = {foo:"bar"}
  3. // copy the reference to the object into b:
  4. var b:Object = a;
  5. // delete the reference to the object in a:
  6. delete(a);
  7. // check to see that the object is still referenced by b:
  8. trace(b.foo); // traces "bar", so the object still exists.

复制代码

如果我改变上述示例代码将b也删除,它会使我创建的对象不再有活动引用并等待对垃圾收集器回收。ActionScript3.0 垃圾回收器使用两种方法定位无引用的对象 : 引用计数法和标识清除法。  
引 用计数法  引用计数法是一种用于跟踪活动对象的较为简单的方法,它从ActionScript1.0开始使用。当你创建一个指向某个对象的引用,该对象的引用计数器 加1;当你删除该对象的一个引用,该计数器减1。当某对象的计数器变成0,该对象将被标记以便垃圾回收器回收。  这是一个例子: ActionScript代码

  1. var a:Object = {foo:"bar"}
  2. // the object now has a reference count of 1 (a)
  3. var b:Object = a;
  4. // now it has a reference count of 2 (a & b)
  5. delete(a);
  6. // back to 1 (b)
  7. delete(b);
  8. // down to 0, the object can now be deallocated by the GC

复制代码

引用计数法简单,它不会非CPU带来巨大的负担;多数情况下它工作正常。不幸地是,采用引用计数法的垃圾回收器在遇到循环引用时效率不高。循环引用是指对象 交叉引用(直接、或通过其他对象间接实现)的情况。即使应用程序不再引用该对象,它的引用计数器仍然大于0,因此垃圾收集器永远无法收集它们。下面的代码 演示循环引用是怎么回事: ActionScript代码

  1. var a:Object = {}
  2. // create a second object, and reference the first object:
  3. var b:Object = {foo:a};
  4. // make the first object reference the second as well:
  5. a.foo = b;
  6. // delete both active application references:
  7. delete(a);
  8. delete(b);

复制代码

上述代码中,所有应用程序中活动的引用都被删除。我没有任何办法在程序中再访问这两个对象了,但这两个对象的引用计数器都是1,因为它们相互引 用。循环引用 还可以更加负责 (a 引用 c, c引用b, b引用a, 等等) 并且难于用代码处理。FlashPlayer 6 和 7的XML对象有很多循环引用问题: 每个 XML 节点被它的孩子和父亲引用,因此它们从不被回收。幸运的是FlashPlayer 8 增加了一个叫做标识-清除的新垃圾回收技术。
标 识-清除法  ActionScript3.0 (以及FlashPlayer 8) 垃圾回收器采用第2种策略标识-清除法查找非活动对象。FlashPlayer从你的应用程序根对象开始(ActionScript3.0中简称为 root)直到程序中的每一个引用,都为引用的对象做标记。  接下来,FlashPlayer遍历所有标记过的对象。它将按照该特性递归整个对象树。并将从一个活动对象开始能到达的一切都标记。该过程结束 后,FlashPlayer可以安全的假设:所有内存中没有被标记的对象不再有任何活动引用,因此可以被安全的删除。图1 演示了它如何工作:绿色引用(箭头)曾被FlashPlayer 标记过程中经过,绿色对象被标记过,白色对象将被回收。

Figure 1.FlashPlayer采用标记清除方法标记不再有活动引用的对象  标记-清除法非常准确。但是,由于FlashPlayer 遍历你的整个对象结构,该过程对CPU占用太多。FlashPlayer 9 通过调整迭代标识-清除缩减对CPU的占用。该过程跨越几个阶段不再是一次完成,变成偶尔运行。
延期(执行)垃圾回收器和不确定性  
FlashPlayer 9垃圾回收器操作是延期的。这是一个要理解的非常重要的概念:当你的对象的所有引用删除后,它不会被立即删除。而是,它们将在未来一个不确定的时刻被 删除(从开发者的角度来看)。垃圾收集器采用一系列启发式技巧诸如查看RAM分配和内存栈空间大小以及其他方法来决定何时运行。作为开发者,你必须接受这 样的事实:不可能知道非活动对象何时被回收。你还必须知道非活动对象将继续存在直到垃圾收集器回收它们。所以你的代码会继续运行(enterFrame 事件会继续)、声音会继续播放、装载还会发生、其它事件还会触发等等。  

扩展:as3垃圾回收机制 / java垃圾回收机制详解 / as3事件机制


记住,在FlashPlayer中你无权控制何时运行垃圾收集器去回收对象。作为开发者,你需要尽可能把你的游戏或应用程序中无用的对象应用清除。

part 2
AS3给开发者带来了更快的代码运行速度和更多功能更强大的API。但不幸的是,伴随着功能的增强,对开发者的专业能力要求也越来越高。这篇文章将重点讲述AS3在资源管理方面的特性和这些特性可能会引起的一些让人头疼的问题。下一篇文章,我将介绍一些可供我们使用的对策。
在AS3中对资源管理影响最大的是其新引入的display list 模型。在flash8和其以前的版本中,当一个display object被移除后(用removeMovie 或unloadMovie),那么这个对象和 其所有的子节点将会被立即从内存中删除,并且终止运行该对象的所有代码。Flash Player 9 引入了更灵活的display list 模型,把display objects(Sprites, MovieClips, etc)看做一般的对象。这就意味着开发者可以做很多有趣的事情,比从新定义display object的容器(将一个display object从一个display list移到另一个中),从已生成的swf文件中读取已实例化的display object。不幸的是:这些display object将会被垃圾收集器等同于一般对象来处理。这就会引发一些潜在的问题。

问题1:动态的内容
其 中一个比较明显的问题出现在与Sprite相关或其他容器的动态实例中,当你想要在某段时间后移除该对象时,Sprites(或其他容器)就会出现一个比 较明显的问题:当你从舞台(stage)上将其移除后,此时display object已经不在display list上了,但事实上它仍然在内存中没有被清除。如果你没有清空这个剪辑的所有的引用或监听器,它可能用远也不会被从内存中删除。
灰常值得我们注意的是:display object不仅仅仍然占用着内存,而且它仍然在执行其内部的代码,例如Timer,enterFrames和其相关的外部监听器。

现在有一个应用于游戏的sprite正在执行一个内部的enterFrame事件, 每一帧它都会执行一些运算,并判断出它附近的其他游戏元素。在AS3中,即使你将其从display list中删除(removeChild)并且将其引用全部设置为null,在其被垃圾收集器回收之前,它将继续执行enterFrame内的代码。在删 除sprite之前,你必须明确:“enterFrame的监听器已经被删除”。
假设有一个影片剪辑监听来自舞台的鼠标移动事件,在你移除其监听器之前,即使是该剪辑已经被删除(deleted),每有一次鼠标移动事件,其代码都会被执行,也就是这个剪辑将会被永远运行下去,作为一个来自舞台事件派发的引用。
假设:在一些相互关联而且各自处于不同的状态的sprite(例如一些处于实例化,一些已经被删除),又或是在你删除对象前没能将其所有的引用清空时。你可能不会发觉就是这个细节令你的cpu使用率直线上升,降低你的程序或者游戏的执行速度,或者使用户的电脑进入无响应状态。没有方法可以强制flash播放器去立即删除一个display object并且停止其代码执行。你必须在从一个容器中删除一个display object时手动的做一些处理。下一篇文章我将介绍对策。
这里有一个例子(需要flash 9播放器),单击create(创建) 按钮来创建一个新的Sprite事例,Sprite将会带有一个计数器,并会显示出来。单击remove按钮并且观察计数器是怎么变化的,事实上sprite的所以引用都已经被置空。你可以多创建几个实例来观察这个问题是如何让一个程序恶化的。代码会在本篇文章的最底端。
http://www.gskinner.com/blog/arc ... _resource_ma_1.html
问题2:加载的内容
设 想把加载的swf文件也同样当做一般的对象来对待,很容易想到一些你会遇到的问题。正如display objects一样,没有一种明确而有效的方法将swf的加载内容从内存中删除,或者立即停止其运行。置空其调用的Loader对加载的swf的所有引 用,它最终还是会被GC(垃圾收集器)收集的。

考虑2个这样的情景:
1.你创建了一个 shell来进行flash实验。这个实验是最前沿的,并且它会使cpu使用率到达最高。一个用户点击按钮加载了一个实验,观察后,又点击按钮加载了第二 个实验。即使是第一个的引用全部被清空,它仍然在后台运行,在第二个实验运行的同时,两个实验的负荷已经超出了cpu最大运算能力。
2.一个客户 要求你建立一个程序来读取其他开发者的swf文件。那些开发者给舞台添加了监听器,或者是用其他方法创建了一些指向swf内部的一些引用。你现在没办法删 除它的内容,直到关闭应用程序之前,它将一直在内存中并占用cpu。即使它们没有“内部引用”,它们也将继续执行下去,知道下一次GC(垃圾收集器)将它 们释放。
出于项目安全的考虑,当你加载第三方内容时,你必须要了解的是:你是无法控制其删除或执行的。当一个swf被加载后,其很容易伴随你的程序一直在运行,即使是被删除了,当发生交互时被加载的swf可能会继续捕捉或干扰用户。
另一个例子和第一个问题一样,只是每次加载swf时,不使用动态实例。
上述有两个提及两个情景事例在下面的页面中:http://www.gskinner.com/blog/arc ... _resource_ma_1.html

问题3:时间轴
希望这个问题能在最终版本中解决。(译者:此篇文章是2006年的)
在as3中时间轴是可以用代码来操作的。当执行播放时,它会动态的实例或删除display object。这就意味着会遇到与问题一相同的问题。在某一帧虽然剪辑从舞台被删除,但是它仍然会继续保存在内存中,在被回收前,它会继续执行其内部的所有代码。这不是程序员所期望的,当然也不是flash设计者所期望的。
这里有一个例子,同样的原理,不过这次是在两帧之间删除和实例化。
http://www.gskinner.com/blog/arc ... _resource_ma_1.html
Adobe在想啥?或者,为什么会出现这个问题?
Java开 发者在看到这一问题时可能会说:“那又如何?”。这种差异是可以理解的, flash开发人员并不习惯于手动干涉内存管理(因为以前就没这问题),而Java、C++的开发人员又已经习惯了强大的GC(无论是自动的还是手动 的)。这些问题是最新的内存管理语言自身带来的缺陷,不幸的是这些问题是无法被避免的。

扩展:as3垃圾回收机制 / java垃圾回收机制详解 / as3事件机制


另一方面,Flash带来了很多在其他语言中罕见的问题 (包括Flex中的一部分也是)。Flash内容中往往会有很多空闲或被动代码在被执行,尤其是 java和Flex在交互时(通常来说:很多密集型运算都是和入户的输入紧密相连的)。Flash工程会更经常读取外部第三方内容(其代码质量通常是很差 的)。Flash 开发人员可利用的工具,资料和框架都比较少。而且据我所知负责开发flash的工作者的背景一般都是来自:音乐,艺术,商业,哲学或是其他的什么,但是除 了专业程序员。
这种多元化的组合带来了令人惊奇的创造力和内容,但是他们并没有准备去讨论资源管理的问题。
总结
资源管理将会成为 AS3开发的一个重要部分。忽略这一问题,可能会导致程序运行缓慢,或是完全的拖垮用户的系统。目前为止还没有明确的方法能立即将display object从内存中删除并停止其内部代码运行,这就意味这我们有责任去妥善的处理我们创造出来的对象。希望经过交流,可以探索出一个最佳的方法和框架来 更轻松解决这个问题。

在第三部分,我们会集中于讲解一些新的工具(AS3 / Flex2)使内存管理更有效。在内存管理方面官方的只有两个函数和内存管理直接相关,但是它们都灰常有用。下面是一些非官方的补充,当然这并不是唯一方法。
System.totalMemory
这是一个简单的工具,但是它灰常重要,因为它是开发者在flash中第一个可以实时应用的工具。它可以让你监听到flash播放器实时的内存占用大小,更重要的是:你可以利用这个来判断是否抛出异常,来终止即将给用户带来的负面体验。
下面是一个例子:

  1. import flash.system.System;
  2. import flash.net.navigateToURL;
  3. import flash.net.URLRequest;
  4. ...
  5. // check our memory every 1 second:
  6. var checkMemoryIntervalID:uint = setInterval(checkMemoryUsage,1000);
  7. ...
  8. var showWarning:Boolean = true;
  9. var warningMemory:uint = 1000*1000*500;
  10. var abortMemory:uint = 1000*1000*625;
  11. ...
  12. function checkMemoryUsage() {
  13. if (System.totalMemory > warningMemory && showWarning) {
  14.   // show an error to the user warning them that we're running out of memory and might quit
  15.   // try to free up memory if possible
  16.   showWarning = false; // so we don't show an error every second
  17. } else if (System.totalMemory > abortMemory) {
  18.   // save current user data to an LSO for recovery later?
  19.   abort();
  20. }
  21. }
  22. function abort() {
  23. // send the user to a page explaining what happpened:
  24. navigateToURL(new URLRequest("memoryError.html"));
  25. }

复制代码

这一行为可以通过很多方法来实现,但是至少证明了这一行为的目的是好的。

灰常值得我们注意的是:totalMemory是一个被一个进程(a single process)使用的“全局变量”(shared value),一个进程可能只有一个窗口,或多个浏览窗口,这些取决与浏览器,操作系统或是有多少个窗口被打开。
弱引用
在AS3众多的新特性中,我灰常高兴的看到“weak references”。这种引用不会被垃圾收集器作为判定object是否被回收的依据。它的作用是:如果当一个对象仅仅剩下弱引用时,这个对象将会被垃圾收集器在下一轮回收。但是弱引用只支持两种类型:第一种是经常会因为内存管理机制带来麻烦的事件监听器,我强烈的建议:每当添加监听器时,都将其第五个参数选项,即弱引用设置为true。下面是其对应的参数设置的例子:

  1. someObj.addEventListener("eventName",listenerFunction,useCapture,priority,weakReference);
  2. stage.addEventListener(Event.CLICK,handleClick,false,0,true);
  3. // the reference back to handleClick (and this object) will be weak.

复制代码

更多关于弱引用请浏览:http://www.gskinner.com/blog/arc ... s3_weakly_refe.html

另一个弱引用支持的是Dictionary object。一般情况下在初始化时设置其第一个参数为true,下面是例子:

  1. var dict:Dictionary = new Dictionary(true);
  2. dict[myObj] = myOtherObj;
  3. // the reference to myObj is weak, the reference to myOtherObj is strong

复制代码

更多关于 dictionaries 在 ActionScript 3的介绍和应用, 请点击

比较爽的就是可以利用弱引用支持Dictionary这个特性,将弱引用“钩”到其他内容上。例如:使用弱引用创建WeakReference 和WeakProxyReference类来实现任何对象都可以创建弱引用。
WeakReference 类
WeakReference 利用了Dictionary可以存储弱引用的特点来实现将弱引用“钩”到其他任意对象的功能。这个类在实例化和访问上会有一些开销,所以我建议将其应用在 一些可能得不到释放而且较大的对象上。这些代码虽然不能取代那些使对象正常“分解”的代码,但是它可以帮你确保大型数据对象被垃圾收集器正常分解。

  1. import com.gskinner.utils.WeakReference;
  2. var dataModelReference:WeakReference;
  3. function registerModel(data:BigDataObject):void {
  4. dataModelReference = new WeakReference(data);
  5. }
  6. ...
  7. function doSomething():void {
  8. // get a local, typed reference to the data:
  9. var dataModel:BigDataObject = dataModelReference.get() as BigDataObject;
  10. // call methods, or access properties of the data object:
  11. dataModel.doSomethingElse();

    扩展:as3垃圾回收机制 / java垃圾回收机制详解 / as3事件机制

  12. }

复制代码

从良好的代码结构来说,这是一个好的解决方案,因为它保 证了你的数据类型带来良好的安全性,而且没有二义性(non-ambiguou)。这些代码是那些希望快速实现这一功能的人准备的,我还将其整合到另一个 WeakProxyReference类(同时也是一个学习代理(Proxy)的好例子 )中。

WeakProxyReference 类
WeakProxyReference 使用了Proxy类来代理弱引用对象。它的效果和 WeakReference类是基本是一样的,WeakProxyReference可以直接调用弱引用对象的方法并且是直接传递给目标。 WeakProxyReference的问题就是失去了类型安全性,并且有一点点二义性代码。也就是说它可能会抛出运行时错误。(特别是当你试图去访问一 个对象中不存的属性)但是不会出现编译错误。

  1. import com.gskinner.utils.WeakProxyReference;
  2. var dataModel:Object; // note that it is untyped, and not named as a reference
  3. function registerModel(data:BigDataObject):void {
  4. dataModel = new WeakProxyReference(data);
  5. }
  6. function doSomething():void {
  7. // don't need to get() the referent, you can access members directly on the reference:
  8. dataModel.doSomethingElse();
  9. dataModel.length++;
  10. delete(dataModel.someProperty);

  11. // if you do need access to the referent, you need to use the weak_proxy_reference namespace:
  12. var bdo:BigDataObject = dataModel.weak_proxy_reference::get() as BigDataObject;
  13. }

复制代码

一种可以强制执行垃圾收集的方法(不推荐使用)
在我的前一篇文章中,我说过在AS3中垃圾收集周期是不确定的,没有方法可以知道它下一次什么时候运行。严格的讲这句话也不完全的对,有一个技巧可以强制让flash播放器执行一次垃圾收集,这个技巧很方便你去探索垃圾收集和在开发期内测试你的程序,但是它绝不能出现在开发完成的产品中,因为它会破坏处理器的负载能力。同时官方也是不推荐使用的,所以你不能靠它的功能来完成实质功能上提升。
强制执行垃圾收集(表示计数法或引用清除法),你所要做的就是 执行两次相同的LocalConnection。这样做系统会抛出一个异常,所以你必须为它准备好异常捕捉(try/catch)

  1. try {
  2. new LocalConnection().connect('foo');
  3. new LocalConnection().connect('foo');
  4. } catch (e:*) {}
  5. // the GC will perform a full mark/sweep on the second call.

复制代码

再次重复一次:这个方法仅仅可以用于开发周期内的测试。它绝不能出现在开发完成的产品中!
总结
毫无疑问的是:ActionScript 3给开发者在资源管理方面带来了更多的工作。虽然我们只有刚刚提到的一些应对工具,但是有对策总是比没有的好,而且 Adobe至少也注意到这个问题了。采取有效的对策和方法并合理的搭配这些工具,相信你可以很好的管理Flash 9和 Flex 2的工程。
Download WeakReference and WeakProxyReference.
http://www.gskinner.com/blog/assets/WeakReference.zip

尾声
补充资料:
原文出处:http://www.tan66.cn/?p=11
在AIR程序中发现了一种快速激活GC sweep的办法。
那就是将窗口最小化,瞬间就环保了。
当然为了做到悄无声息,还要将窗口还原。
这两句代码写在一起就可以了。前提条件是当前状态不是最小化。
stage.window.minimize();
stage.window.restore();
问题是,这样会看到窗口缩小又变回来的过程。所以在实际开发程序的时候可以将主程序窗口隐形
visible=false;
将要显示的内容放在其它窗口中,当然不要忘记加入控制主程序窗口退出的代码。

扩展:as3垃圾回收机制 / java垃圾回收机制详解 / as3事件机制

四 : 收垃圾的叔叔

  中午,我因身有不适来到校医室。可这时校医室还没有开门,我只好耐心等待。时间一分一秒地过去,渐渐地我闻到了一阵越来越浓的恶臭味,忍不住转过身来,看到了在一旁忙忙碌碌收垃圾的叔叔。

  已是深秋了,秋风萧瑟,他却只穿一件单薄的上衣和一条肮脏得不成样子的长裤,外衣却已经湿透,头发上汗滴也大颗大颗地掉下来;而我却穿着毛衣,还冷得瑟瑟发抖。这场景不禁让我想起老舍《骆驼祥子》中那可怜的拉车夫——祥子。

  那一双满是污垢的手在一袋袋苍蝇乱飞的垃圾袋中不停翻着,似乎已经失去了知觉,只是麻木机械地操作着。好不容易找出一个饮料盒,放到地上“啪”地一声踩扁,又弯腰捡起,扔到分类的垃圾篓子里。又找出一个罐子,重复了刚才的动作,又扔到另一个篓子里。我的目光此时也机械了,跟随着叔叔的手动。

  一个又一个,一袋又一袋,一桶又一桶,收拾完所有垃圾后,搬运工又运来一桶,仿佛那垃圾是永远收不完的。

  这样辛苦地工作,换来的只是一点微薄的工资。

  那饱经沧桑的脸上刻下了岁月的痕迹,带着多年工作的苦涩与无奈……

  这世间,有多少这样默默为人服务的人,有老有少。此刻,他们大概也在重复机械的动作吧……也许,他们累了,也该休息了……

 

本文标题:垃圾回收-浅谈PHP5中垃圾回收算法(Garbage Collection)的演化
本文地址: http://www.61k.com/1137064.html

61阅读| 精彩专题| 最新文章| 热门文章| 苏ICP备13036349号-1