引用记数+垃圾回收+.NETGC

本文深入探讨了垃圾回收机制的工作原理,对比了引用计数与垃圾回收的优势与不足,介绍了标记-整理、增量收集、分代收集等算法,并讨论了.NET运行时中的垃圾回收机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

确定性终止

开销分散,没有统一一个大的回收操作

Cache-miss率低

 

循环引用

线程安全

变更1个指针,开销大,老的新的都要动1

引用计数的空间开销

 

1. 引用计数的优点

引用计数提供了3点重要好处:

      确定性终止(Deterministic Finalization)。当一个对象的引用计数到零时,线程执行的下一个操作是销毁对象和其他使用的资源。这称为确定性终止,因为您可以预知该对象将被销毁(终止)的确切时间。

      资源共享(Resource Sharing)。引用计数能将对对象的引用安全地传递到程序的其他部分。引用计数规则只是简单地要求当结束对象的使用时,用户应调用对象的Release方法。在Visual Basic(.NET之前)中,当一个对象引用变量超出了作用域,就会自动执行该操作。实际上,资源共享的好处源于下面一个优点。

      生存期封装(Lifetime Encapsulation)。对象负责维护自身的对象引用计数器。当最后的用户释放对象后,对象将销毁自身。也就是说,用户无需关心是保留对象的最后引用还是保留多个引用中的任意一个。

2. 引用计数的缺点

至此,引用计数似乎是一个很好的解决方案,那为什么.NET却采用垃圾回收来代替它呢?答案在于引用计数的缺点:

      循环引用(Circular References)。如果对象A包含了对对象B的引用,而对象B又包含了对对象A的引用,在这种情况下就会出现循环调用。如果没有其他外部干涉,这种循环引用不会被打破。同时这些对象连同分配给它们的资源,将会在应用程序整个生命周期被占用。

      线程安全(Thread Safety)。引用计数机制听起来非常明了,但是如果考虑到多个线程共享一个对象时,就不再是一件简单的事了。为了解决这一问题,必须使用专门的Windows API,针对对象完成引用计数器的递增和递减,以确保每一个操作和相应的测试自动完成。否则,由于到不可预期的上下文切换,引用计数无法同步。在面对多线程时,为了保证计数器安全地进行递增和递减操作,分配两个对象引用这样极为普通任务,也是相当地麻烦。

 

 

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

 

垃圾回收的内部机理

在大多数情况下,您无需关心垃圾回收是如何工作的。可是,有时不必关心如何回收对象却需要了解对象的分配过程,有时又需要了解垃圾回收对性能的影响。因此最好理解了垃圾回收的工作机理,才能有助于清楚地认识,使思维更为清晰。

首先,垃圾回收器只有在必要的时候才会工作。运行库根据各种因素综合考虑做出是否运行的决定,其中考虑的主要因素是托管堆是否已满。它还考虑当前线程活动,确保是安全停止了线程,以便运行垃圾回收器。当垃圾回收器运行时,运行库暂停应用程序中的所有线程。其原因稍后会揭晓。

一旦垃圾回收器启动,它首先对应用程序“根”引用进行定位。这些“根”引用均是全局的、静态的,或在线程堆栈(也就是局部变量引用)中已分配的。然后检查每一个根对象,搜索引用数据成员。接下来,逐一检查这些成员对象,搜索更多的引用成员,以此类推。当垃圾回收器执行这一操作时,它在图表中记录访问的每一个对象。这样就可以告诉它已经访问了的引用对象,将不再试图访问。如果没有这种机制,那么循环引用可能会使垃圾回收器陷入永无休止的循环中。

以上过程完成后,结果图表将显示“可获取的”应用程序对象。根据该信息,垃圾回收器将图表中的对象与在托管堆中分配的对象进行比较。如果发现托管堆中的对象没有在图表中(也即“不可获得的”),将会采取下面操作中的一种:如果对象没有实现Finalize方法,垃圾回收器立即销毁该对象并重新申明内存空间;如果对象实现了Finalize方法,那么运行库在被称为freachable队列的内部结构中为该对象放置一个引用。这点在后面进行解释。

垃圾回收器清理完未使用的对象之后,托管堆内的当前存活对象之间存在因清除不用的对象后而遗留的间隙。为了提高托管堆的分配速度,使整个托管堆更为紧凑,垃圾回收器删除全部的空闲间隙。当然,这就需要在堆内部移动当前存活对象,同时为反射它们的新位置而更新应用程序中的全部引用。这就是垃圾回收器运行时,运行库必须暂停所有应用程序线程的原因。

前面已经提到,垃圾回收器以不同方式处理实现Finalize的对象。当垃圾回收器判断出对象是不可获得的(unreachable),它将为该对象在freachable队列中添加一个引用。从本质上讲,这使得以前不可获得的对象成为可以获得(reachable)的对象。换句话说,这使得对象再次复活,但在它后半部分生存期中使用受到局限。在垃圾回收器完成对不可获得对象的清除后,它将启动另一个线程,调用freachable序列中每个引用的Finalize方法,然后清除队列。最后,在下一次垃圾回收时,把终止的对象从托管堆中清除掉。

http://book.youkuaiyun.com/bookfiles/156/1001566744.shtml

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

如果让你用C++写一个实用的字符串类,我想下面的方案是很多人最先想到的:

class ClxString
{
public:
    ClxString();
    ClxString(
const char *pszStr);
    ClxString(
const ClxString &str);
    ClxString& 
operator=(const ClxString &str);
    ~ClxString();

private:
    
char *m_pszStr;
};

ClxString::ClxString(
const char *pszStr)
{
    
if (pszStr)
    {
        m_pszStr = 
new char[strlen(pszStr) + 1];
        strcpy(m_pszStr, pszStr);
    }
    
else
    {
        m_pszStr = 
new char[1];
        *m_pszStr = '/0';
    }
}

ClxString::ClxString(
const ClxString &str)
{
    m_pszStr = 
new char[strlen(str.m_pszStr) + 1];
    strcpy(m_pszStr, str.m_pszStr);
}

ClxString& ClxString::
operator=(const ClxString &str)
{
    
if (this == &str)
        
return *this;

    
delete []m_pszStr;

    m_pszStr = 
new char[strlen(str.m_pszStr) + 1];
    strcpy(m_pszStr, str.m_pszStr);

    
return *this;
}

ClxString::~ClxString()
{
    
delete []m_pszStr;
}

    设计分析:
   
如果有下面的代码

ClxString str1 = str2 = str3 = str4 =  str5 = str6 = "StarLee";

    那么,字符串StarLee在内存中就有6个副本,而且每执行一次赋值(=)操作,都会有内存的释放和开辟。

 

这样,内存的使用效率和程序的运行效率都不高。
   
解决方案:
   
使用引用计数(Reference Counting)。

引用计数(Reference Counting
   
如果字符串的内容相同,就把ClxString类里的指针指向同一块存放字符串值的内存。为每块共享的内存设置一个引用计数。当有新的指针指向该内存块时,计数加一;当有一个字符串销毁时,计数减一;直到计数为零,就表示没有任何指针指向该内存块,再将其销毁掉。
   
下面就是用引用计数(Reference Counting)设计的解决方案:

class ClxString
{
public:
    ClxString();
    ClxString(
const char *pszStr);
    ClxString(
const ClxString &str);
    ClxString& 
operator=(const ClxString &str);
    ~ClxString();

private:
    
// 这里用一个结构来存放指向共享内存块的指针和该内存块的引用计数
    struct StringValue
    {
        
int iRefCount;
        
char *pszStrData;

        StringValue(
const char *pszStr);
        ~StringValue();
    };

    StringValue *m_pData;
};

// struct StringValue
ClxString::StringValue::StringValue(const char *pszStr)
{
    iRefCount = 1;

    pszStrData = 
new char[strlen(pszStr) + 1];
    strcpy(pszStrData, pszStr);
}

ClxString::StringValue::~StringValue()
{
    
delete []pszStrData;
}
// struct StringValue

// class ClxString
ClxString::ClxString(const char *pszStr)
{
    m_pData = 
new StringValue(pszStr);
}

ClxString::ClxString(
const ClxString &str)
{
    
// 这里不必开辟新的内存空间
    // 
只要让指针指向同一块内存
    // 
并把该内存块的引用计数加一
    m_pData = str.m_pData;
    m_pData->iRefCount++;
}

ClxString& ClxString::
operator=(const ClxString &str)
{
    
if (m_pData == str.m_pData)
        
return *this;

    
// 将引用计数减一
    m_pData->iRefCount--;

    
// 引用计数为0,也就是没有任何指针指向该内存块
    // 
销毁内
    if (m_pData->iRefCount == 0)
        
delete m_pData;

    
// 这里不必开辟新的内存空间
    // 
只要让指针指向同一块内存
    // 
并把该内存块的引用计数加一
    m_pData = str.m_pData;
    m_pData->iRefCount++;

    
return *this;
}

ClxString::~ClxString()
{
    
// 析构时,将引用计数减一
    m_pData->iRefCount--;

    
// 引用计数为0,也就是没有任何指针指向该内存块
    // 
销毁内存
    if (m_pData->iRefCount == 0)
        
delete m_pData;
}
// class ClxString

    设计分析:
   
这个版本的String类用上了引用计数(Reference Counting),内存的使用效率和程序的运行效率都有所提高,那么这就是我们需要的最终版本吗?答案当然是--不!
   
如果共享字符串被修改,比如给ClxString添加上索引操作符([]),而出现了如下代码:

ClxString str1 = str2 = "StarLee";
str1[6] = 'a';

    那么str1str2都变成了StarLea。这显然不是我们希望看到的!
   
解决方案:
   
写入时复制(Copy-On-Write)。

写入时复制(Copy-On-Write
   
添加一个布尔变量来标志是否共享字符串。当发生写入操作时,重新开辟一块内存,并将共享的字符串值拷贝到新内存中。
   
下面就是用写入时复制(Copy-On-Write)设计的解决方案:

class ClxString
{
public:
    ClxString();
    ClxString(
const char *pszStr);
    ClxString(
const ClxString &str);
    ClxString& 
operator=(const ClxString &str);
    ~ClxString();

    
// 索引操作符重载
    const charoperator[](int iIndex) const;
    
charoperator[](int iIndex);

private:
    
struct StringValue
    {
        
int iRefCount;
        
char *pszStrData;
        
// 表示是否共享字符串值的变量
        bool bShareable;

        StringValue(
const char *pszStr);
        ~StringValue();
    };

    StringValue *m_pData;

    
// 判断是否共享字符串值
    // 
如果不共享,就开辟新的内存块
    ShareStrOrNot(const ClxString &str);
};

// struct StringValue
ClxString::StringValue::StringValue(const char *pszStr)
{
    iRefCount = 1;
    
// 默认共享
    bShareable = true;

    pszStrData = 
new char[strlen(pszStr) + 1];
    strcpy(pszStrData, pszStr);
}

ClxString::StringValue::~StringValue()
{
    
delete []pszStrData;
}
// struct StringValue

// class ClxString
ClxString::ClxString(const char *pszStr)
{
    m_pData = 
new StringValue(pszStr);
}

ClxString::ClxString(
const ClxString &str)
{
    ShareStrOrNot(str);
}

ClxString& ClxString::
operator=(const ClxString &str)
{
    
if (m_pData == str.m_pData)
        
return *this;

    m_pData->iRefCount--;
    
if (m_pData->iRefCount == 0)
        
delete m_pData;

    ShareStrOrNot(str);

    
return *this;
}

ClxString::~ClxString()
{
    m_pData->iRefCount--;

    
if (m_pData->iRefCount == 0)
        
delete m_pData;
}

const char& ClxString::operator[](int iIndex) const
{
    
return m_pData->pszStrData[iIndex];

}

char& ClxString::operator[](int iIndex)
{
    
// 有写入操作,开辟新的内存
    if (m_pData->iRefCount > 1)
    {
        m_pData->iRefCount--;

        m_pData = 
new StringValue(m_pData->pszStrData);
    }

    
// 并设置为不共享字符串值
    m_pData->bShareable = false;

    
return m_pData->pszStrData[iIndex];
}

ClxString::ShareStrOrNot(
const ClxString &str)
{
    
if (str.m_pData->bShareable)
    {
        m_pData = str.m_pData;
        m_pData->iRefCount++;
    }
    
// 不共享字符串值,开辟新的内存块
    else
    {
        m_pData = 
new StringValue(str.m_pData->pszStrData);
    }
}
// class ClxString

    设计分析:
    这个版本的ClxString类既使用了引用计数(Reference Counting),也实现了写入时复制(Copy-On-Write),应该时一个很完善的版本了吧?答案依然是--不!
   
这个版本所实现的根本不是真正的写入时复制(Copy-On-Write)!因为上面的代码根本无法区别下面两行使用了索引操作符([])的代码:

ClxString str = "StarLee";
str[6] = 'a'; 
// 1
char c = str[6]; // 2

    对于代码行1来说是真正的写入操作,应该为字符串重新开辟一块内存还存放字符串值,并且不共享该块内存。
   
而对于代码行2来说,这其实是一个读操作,而上面设计的ClxString类却也把这行代码当成了写操作。
   
解决方案:使用代理(Proxy

代理(Proxy
   
这里的代理(Proxy)其实是设计模式的一种。意图是为其他对象提供一种代理来控制对这个对象的访问,也就是说只有在我们确实需要这个对象时才对它进行创建和初始化。
   
对于我们这里的ClxString类来说,由于要区分索引操作符([])是读操作还是写操作,可以给字符添加一个代理。我们可以修改operator[],让它返回一个字符的代理,而不是字符本身。然后我们可以看这个代理如何被运用。这样就可以用代理来区分读取和写入操作。我们也可以实现一个真正的写入时拷贝(Copy-On-Write)函数。
   
下面就是用代理(Proxy)设计的解决方案:

class ClxString
{
public:
    
// 字符代理类
    class CharProxy
    {
    
public:
        CharProxy(ClxString &str, 
int iIndex);
        CharProxy& 
operator=(const CharProxy &rhs); // 写操作
        CharProxy& operator=(char c); // 写操作
        charoperator&(); // 写操作
        operator char() const// 读操作

    
private:
        ClxString &m_lxStr; 
// 代理的字符所在的字符串
        int m_iIndex; // 代理的字符在字符串中的索引

        // 
真正的Copy-On-Write函数
        void CopyOnWrite();
    };

    
// 设置为友元类,使CharProxy可以访问ClxString的所有成员
    friend class CharProxy;

    ClxString();
    ClxString(
const char *pszStr);
    ClxString(
const ClxString &str);
    ~ClxString();

    ClxString& 
operator=(const ClxString &str);

    
// 重载[]操作符,返回字符代理
    // 
将对ClxString[]操作转接给CharProxy处理
    // 
CharProxy判断是读取还是写入操作
    const CharProxy& operator[](int iIndex) const;
    CharProxy& 
operator[](int iIndex);

private:
    
struct StringValue
    {
        
int iRefCount;
        
char *pszStrData;
        
bool bShareable;

        StringValue(
const char *pszStr);
        ~StringValue();
    };

    StringValue *m_pData;

    ShareStrOrNot(
const ClxString &str);
};

// class CharProxy
ClxString::CharProxy::CharProxy(ClxString &str, int iIndex)
: m_lxStr(str), m_iIndex(iIndex)
{
}

// 重载=操作符
// 
写操作
// 
用来应对下面的代码
// ClxString str1 = "Star"; ClxString str2 = "Lee"; str1[1] = str2[2];
ClxString::CharProxy& ClxString::CharProxy::operator=(const CharProxy &rhs)
{
    CopyOnWrite();

    m_lxStr.m_pData->pszStrData[m_iIndex] = rhs.m_lxStr.m_pData->pszStrData[rhs.m_iIndex];

    
return *this;
}

// 重载=操作符
// 
写操作
// 
用来应对下面的代码
// ClxString str = "StarLee"; str[6] = 'a';
ClxString::CharProxy& ClxString::CharProxy::operator=(char c)
{
    CopyOnWrite();

    m_lxStr.m_pData->pszStrData[m_iIndex] = c;

    
return *this;
}

//  重载&操作符
// 
很有可能发生写操作
// 
用来应对下面的代码
// ClxString str = "StarLee"; char *p = &str[6]; *p = 'a';
char* ClxString::CharProxy::operator&()
{
    CopyOnWrite();

    
// 这里必须设置为不共享
    // 
因为有指针指向字符串内部的字符,有随时改写字符的权力
    m_lxStr.m_pData->bShareable = false;

    
return &(m_lxStr.m_pData->pszStrData[m_iIndex]);
}

// 重载了char操作符
// 
读操作
// 
用来应对下面的代码
// ClxString str = "StarLee"; cout << str[6];
ClxString::CharProxy::operator char() const
{
    
return m_lxStr.m_pData->pszStrData[m_iIndex];
}

void ClxString::CharProxy::CopyOnWrite()
{
    
if (m_lxStr.m_pData->iRefCount > 1)
    {
        m_lxStr.m_pData->iRefCount--;
        m_lxStr.m_pData = 
new StringValue(m_lxStr.m_pData->pszStrData);
    }
}
// class CharProxy

// struct StringValue
ClxString::StringValue::StringValue(const char *pszStr)
{
    iRefCount = 1;
    bShareable = 
true;

    pszStrData = 
new char[strlen(pszStr) + 1];
    strcpy(pszStrData, pszStr);
}

ClxString::StringValue::~StringValue()
{
    
delete []pszStrData;
}
// struct StringValue

// class ClxString
ClxString::ClxString(const char *pszStr)
{
    m_pData = 
new StringValue(pszStr);
}

ClxString::ClxString(
const ClxString &str)
{
    ShareStrOrNot(str);
}

ClxString& ClxString::
operator=(const ClxString &str)
{
    
if (m_pData == str.m_pData)
        
return *this;

    m_pData->iRefCount--;
    
if (m_pData->iRefCount == 0)
        
delete m_pData;

    ShareStrOrNot(str);

    
return *this;
}

const ClxString::CharProxy& ClxString::operator[](int iIndex) const
{
    
return CharProxy(const_cast<ClxString&>(*this), iIndex);
}

ClxString::CharProxy& ClxString::
operator[](int iIndex)
{
    
return CharProxy(*this, iIndex);
}

ClxString::~ClxString()
{
    m_pData->iRefCount--;

    
if (m_pData->iRefCount == 0)
        
delete m_pData;
}

ClxString::ShareStrOrNot(
const ClxString &str)
{
    
if (str.m_pData->bShareable)
    {
        m_pData = str.m_pData;
        m_pData->iRefCount++;
    }
    
else
    {
        m_pData = 
new StringValue(str.m_pData->pszStrData);
    }
}
// class ClxString

    C++ Tips类的引用成员变量和常量成员变量必须在构造函数的初始化列表里赋值。

 

 

 

 

 

 

 

 

 

 

 

 

标记-整理( Mark-Compact )算法
标记-整理算法是标记-清除算法和复制算法的有机结合。把标记-清除算法在内存占用上的优点和复制算法在执行效率上的特长综合起来,这是所有人都希望看到的结果。不过,两种垃圾收集算法的整合并不像 1 1 等于 2 那样简单,我们必须引入一些全新的思路。 1970 年前后, G. L. Steele C. J. Cheney D. S. Wise 等研究者陆续找到了正确的方向,标记-整理算法的轮廓也逐渐清晰了起来:

在我们熟悉的餐厅里,这一次,垃圾收集机器人不再把餐厅分成两个南北区域了。需要执行垃圾收集任务时,机器人先执行标记-清除算法的第一个步骤,为所有使用中的餐巾纸画好标记,然后,机器人命令所有就餐者带上有标记的餐巾纸向餐厅的南面集中,同时把没有标记的废旧餐巾纸扔向餐厅北面。这样一来,机器人只消站在餐厅北面,怀抱垃圾箱,迎接扑面而来的废旧餐巾纸就行了。

实验表明,标记-整理算法的总体执行效率高于标记-清除算法,又不像复制算法那样需要牺牲一半的存储空间,这显然是一种非常理想的结果。在许多现代的垃圾收集器中,人们都使用了标记-整理算法或其改进版本。

增量收集( Incremental Collecting )算法
对实时垃圾收集算法的研究直接导致了增量收集算法的诞生。

最初,人们关于实时垃圾收集的想法是这样的:为了进行实时的垃圾收集,可以设计一个多进程的运行环境,比如用一个进程执行垃圾收集工作,另一个进程执行程序代码。这样一来,垃圾收集工作看上去就仿佛是在后台悄悄完成的,不会打断程序代码的运行。

在收集餐巾纸的例子中,这一思路可以被理解为:垃圾收集机器人在人们用餐的同时寻找废弃的餐巾纸并将它们扔到垃圾箱里。这个看似简单的思路会在设计和实现时碰上进程间冲突的难题。比如说,如果垃圾收集进程包括标记和清除两个工作阶段,那么,垃圾收集器在第一阶段中辛辛苦苦标记出的结果很可能被另一个进程中的内存操作代码修改得面目全非,以至于第二阶段的工作没有办法开展。

M. L. Minsky D. E. Knuth 对实时垃圾收集过程中的技术难点进行了早期的研究, G. L. Steele 1975 年发表了题为多进程整理的垃圾收集( Multiprocessing compactifying garbage collection 的论文,描述了一种被后人称为“ Minsky-Knuth-Steele 算法的实时垃圾收集算法。 E. W. Dijkstra L. Lamport R. R. Fenichel J. C. Yochelson 等人也相继在此领域做出了各自的贡献。 1978 年, H. G. Baker 发表了串行计算机上的实时表处理技术( List Processing in Real Time on a Serial Computer 一文,系统阐述了多进程环境下用于垃圾收集的增量收集算法。

增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对进程间冲突的妥善处理,允许垃圾收集进程以分阶段的方式完成标记、清理或复制工作。详细分析各种增量收集算法的内部机理是一件相当繁琐的事情,在这里,读者们需要了解的仅仅是: H. G. Baker 等人的努力已经将实时垃圾收集的梦想变成了现实,我们再也不用为垃圾收集打断程序的运行而烦恼了。

分代收集( Generational Collecting )算法
和大多数软件开发技术一样,统计学原理总能在技术发展的过程中起到强力催化剂的作用。 1980 年前后,善于在研究中使用统计分析知识的技术人员发现,大多数内存块的生存周期都比较短,垃圾收集器应当把更多的精力放在检查和清理新分配的内存块上。这个发现对于垃圾收集技术的价值可以用餐巾纸的例子概括如下:

如果垃圾收集机器人足够聪明,事先摸清了餐厅里每个人在用餐时使用餐巾纸的习惯——比如有些人喜欢在用餐前后各用掉一张餐巾纸,有的人喜欢自始至终攥着一张餐巾纸不放,有的人则每打一个喷嚏就用去一张餐巾纸——机器人就可以制定出更完善的餐巾纸回收计划,并总是在人们刚扔掉餐巾纸没多久就把垃圾捡走。这种基于统计学原理的做法当然可以让餐厅的整洁度成倍提高。

D. E. Knuth T. Knight G. Sussman R. Stallman 等人对内存垃圾的分类处理做了最早的研究。 1983 年, H. Lieberman C. Hewitt 发表了题为基于对象寿命的一种实时垃圾收集器( A real-time garbage collector based on the lifetimes of objects 的论文。这篇著名的论文标志着分代收集算法的正式诞生。此后,在 H. G. Baker R. L. Hudson J. E. B. Moss 等人的共同努力下,分代收集算法逐渐成为了垃圾收集领域里的主流技术。

分代收集算法通常将堆中的内存块按寿命分为两类,年老的和年轻的。垃圾收集器使用不同的收集算法或收集策略,分别处理这两类内存块,并特别地把主要工作时间花在处理年轻的内存块上。分代收集算法使垃圾收集器在有限的资源条件下,可以更为有效地工作——这种效率上的提高在今天的 Java 虚拟机中得到了最好的证明。

应用浪潮
Lisp
是垃圾收集技术的第一个受益者,但显然不是最后一个。在 Lisp 语言之后,许许多多传统的、现代的、后现代的语言已经把垃圾收集技术拉入了自己的怀抱。随便举几个例子吧:诞生于 1964 年的 Simula 语言, 1969 年的 Smalltalk 语言, 1970 年的 Prolog 语言, 1973 年的 ML 语言, 1975 年的 Scheme 语言, 1983 年的 Modula-3 语言, 1986 年的 Eiffel 语言, 1987 年的 Haskell 语言……它们都先后使用了自动垃圾收集技术。当然,每一种语言使用的垃圾收集算法可能不尽相同,大多数语言和运行环境甚至同时使用了多种垃圾收集算法。但无论怎样,这些实例都说明,垃圾收集技术从诞生的那一天起就不是一种曲高和寡的学院派技术。

对于我们熟悉的 C C++ 语言,垃圾收集技术一样可以发挥巨大的功效。正如我们在学校中就已经知道的那样, C C++ 语言本身并没有提供垃圾收集机制,但这并不妨碍我们在程序中使用具有垃圾收集功能的函数库或类库。例如,早在 1988 年, H. J. Boehm A. J. Demers 就成功地实现了一种使用保守垃圾收集算法( Conservative GC Algorithmic )的函数库(参见 http://www.hpl.hp.com/personal/Hans_Boehm/gc )。我们可以在 C 语言或 C++ 语言中使用该函数库完成自动垃圾收集功能,必要时,甚至还可以让传统的 C/C++ 代码与使用自动垃圾收集功能的 C/C++ 代码在一个程序里协同工作。

1995 年诞生的 Java 语言在一夜之间将垃圾收集技术变成了软件开发领域里最为流行的技术之一。从某种角度说,我们很难分清究竟是 Java 从垃圾收集中受益,还是垃圾收集技术本身借 Java 的普及而扬名。值得注意的是,不同版本的 Java 虚拟机使用的垃圾收集机制并不完全相同, Java 虚拟机其实也经过了一个从简单到复杂的发展过程。在 Java 虚拟机的 1.4.1 版中,人们可以体验到的垃圾收集算法就包括分代收集、复制收集、增量收集、标记-整理、并行复制( Parallel Copying )、并行清除( Parallel Scavenging )、并发( Concurrent )收集等许多种, Java 程序运行速度的不断提升在很大程度上应该归功于垃圾收集技术的发展与完善。

尽管历史上已经有许多包含垃圾收集技术的应用平台和操作系统出现,但 Microsoft .NET 却是第一种真正实用化的、包含了垃圾收集机制的通用语言运行环境。事实上, .NET 平台上的所有语言,包括 C# Visual Basic .NET Visual C++ .NET J# 等等,都可以通过几乎完全相同的方式使用 .NET 平台提供的垃圾收集机制。我们似乎可以断言, .NET 是垃圾收集技术在应用领域里的一次重大变革,它使垃圾收集技术从一种单纯的技术变成了应用环境乃至操作系统中的一种内在文化。这种变革对未来软件开发技术的影响力也许要远远超过 .NET 平台本身的商业价值。

大势所趋
今天,致力于垃圾收集技术研究的人们仍在不懈努力,他们的研究方向包括分布式系统的垃圾收集、复杂事务环境下的垃圾收集、数据库等特定系统的垃圾收集等等。

但在程序员中间,仍有不少人对垃圾收集技术不屑一顾,他们宁愿相信自己逐行编写的 free delete 命令,也不愿把垃圾收集的重任交给那些在他们看来既蠢又笨的垃圾收集器。

我个人认为,垃圾收集技术的普及是大势所趋,这就像生活会越来越好一样毋庸置疑。今天的程序员也许会因为垃圾收集器要占用一定的 CPU 资源而对其望而却步,但二十多年前的程序员还曾因为高级语言速度太慢而坚持用机器语言写程序呢!在硬件速度日新月异的今天,我们是要吝惜那一点儿时间损耗而踟躇不前,还是该坚定不移地站在代码和运行环境的净化剂——垃圾收集的一边呢?

 

 

 

 

 

 

 

要搞清楚.Net 运行时之垃圾回收机制,首先需要搞清楚.Net运行时内存分配之情况。.Net 运行时对受管资源(Managed Resource)采用对象引用之堆式分配(Heap Allocation)方法。这种分配方法大多数时候是非常快之。一个实际系统之内存总是有限之,当系统之剩余之可分配之内存资源不多时,.Net 运行时便会预见到下面之内存资源将可能不会满足下面之内存分配请求,于是它便会开始执行垃圾回收释放那些系统不再引用之内存资源。.Net垃圾回收器采用之是一种叫做标志紧缩”(Mark and Compat)之算法。每当垃圾收集开始,.Net垃圾收集器从运行时目前之根对象(包括全局对象,本之对象,静态对象,CPU寄存器对象),开始寻找那些被根对象引用之所有对象,这些对象便是在垃圾收集时运行时正在应用之对象,除去这些,其他之受管运行时对象(Managed Runtime Object)便是系统不再使用之对象,于是便可以进行垃圾收集。所有引用之对象被向下拷贝到运行时受管堆(Runtime Managed Heap),同时修改它们之引用指针。需要指出之是,在.Net垃圾收集器移动引用对象和改变引用指针时,系统不能在这些对象上有任何操作,这一点由运行时之互斥机制来保证,无需程序员干涉。
    
遍历目前所有被引对象之耗费往往是巨大之,实际上也不必这样做。一个经验之认识是,当一个对象在内存中驻留之时间越长,那么它越有可能继续被引用留在内存中。相反,一个对象在内存中驻留时间越短,它越有可能被收集。.Net收集器采用一种称作代分Generation Division之方法来体现这种经验之内存驻留理论。它把受管堆中之对象分成三代(Generations. 第一代是没有经历过垃圾收集驻留在内存中之对象,他们通常是一些局部变量,它们之生命最短。第二代是仅经历过一次垃圾收集仍然驻留在内存中之对象,它们通常是一些如表单,列表,按钮等生命较短之对象。第三代是经历过两次及两次以上之垃圾收集后仍然驻留在内存中之对象,它们通常是一些应用程序对象,它们往往要在内存中驻留很长时间。当运行时收集器开始执行垃圾收集时,它首先对第一代对象进行垃圾收集,这通常会释放较大之内存空间,往往会满足下面之内存请求。如果这一代之收集结果不理想,那么便会对第二代读像进行收集,同样如果还不理想,便进行第三代之垃圾收集。

    .Net
垃圾回收机制支持一种称作终止化(Finalizaion)之概念。这有点类似C++中之析构函数。终止化操作在垃圾收集执行后进行一些非受管资源之清除工作,它在.Net运行时里有很多限制,往往不被推荐实现。当程序员对一个对象实现了终止器(Finalizer)后,运行时便会将这个对象之引用加入一个称作终止化对象引用集之链表,作为要求终止化之标志。当垃圾收集开始时,若一个对象不再被引用但它被加入了终止化对象引用集之链表,那么运行时将此对象标志为要求终止化操作对象。待垃圾收集完成后,终止化线程便会被运行时唤醒执行终止化操作。显然这之后要从终止化对象引用集之链表中将之删去。容易看出来,终止化操作会给系统带来额外之开销。终止化是通过启用线程机制来实现之,这有一个线程安全之问题。.Net运行时不能保证终止化执行之顺序,也就是说如果对象A有一个指向对象B之引用,两个对象都有终止化操作,但对象A在终止化操作时并不一定有有效之对象A引用。所以.Net运行时不推荐对对象进行终止化操作,只是在有非受管资源如数据库之连接,文件之打开等需要严格释放时,才应用终止化操作。
     
大多数时候,垃圾收集应该交由.Net运行时来控制,但有些时候,可能需要人为之控制一下垃圾回收操作。例如在操作了一次大规模之对象集合后,我们确信不再在这些对象上进行任何之操作了,那我们可以强制垃圾回收立即执行,这通过调用System.GC.Collect() 方法即可实现,但频繁之收集会显著之降低系统之性能。还有一种情况,已经将一个对象放到了终止化对象引用集之链上了,但如果我们在程序中某些之方已经做了终止化之操作,在那之后便可以通过调用System.GC.SupressFinalize()来将对象之引用从终止化对象引用集链上摘掉,以忽略终止化操作。始终应该清楚之是,终止化操作之系统负担是很重之。

    
综上所述,.Net垃圾回收机制负责回收系统不再使用之受管内存资源,它通过一定之优化算法来选择收集之对象和时间。程序员只有在释放大量受管资源时可以进行立即强制垃圾收集,在释放非受管资源时采用终止化操作来处理,其它时间将资源之回收交由.Net垃圾收集起来做

 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值