确定性终止
开销分散,没有统一一个大的回收操作
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.csdn.net/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';
那么str1和str2都变成了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 char&