65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 中的垃圾回收

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2001年4月17日

CPOL

10分钟阅读

viewsIcon

446953

downloadIcon

1942

使用托管 C++ 快速了解 .NET 中的垃圾回收

背景

.NET 运行时包含垃圾回收功能,消除了跟踪和释放内存分配的需要。在托管环境中编程时,您使用 new 运算符在托管堆上分配内存,但不是删除或释放该内存,而是简单地移除指向该内存的所有引用(例如,将指向该内存的指针设置为 NULL),然后让垃圾收集器 (GC) 处理其余的工作。请注意,我们只讨论内存,而不是资源。如果您在托管堆上创建了一个新对象,并且该对象分配了句柄或连接等资源,那么您必须确保该对象在将该对象交给 GC 处理之前已释放其资源。

传统的 C 运行时堆本质上是一个链表,每次进行内存分配时都需要遍历。一旦找到合适的内存块,该块就会被分割并返回内存位置。相比之下,.NET 中的托管堆包含一个始终指向内存中下一个可用位置的指针,并且在分配对象时,指向堆顶的指针会相应移动。无需遍历或分割,这意味着在托管堆上分配内存的速度几乎与在堆栈上分配内存一样快。

这种内存分配模型在您拥有无限内存的情况下效果很好——但实际上您不可能拥有无限内存。那么,当您尝试在托管堆的末尾分配一个内存块但发现内存不足时会发生什么?当然是垃圾回收。在典型的应用程序中,您通常会在应用程序的逻辑过程中分配和取消分配内存。在托管应用程序中,不会发生这种取消分配,而是不再使用的内存会一直存在,直到您耗尽所有可用内存,GC 才会被迫回收所有这些未使用的内存。

垃圾回收

垃圾回收过程从 GC 假定托管堆上的所有内存都是垃圾开始。然后,它列出应用程序的所有全局和静态内存指针、包含指向堆上对象的引用的局部或参数变量以及 CPU 寄存器,然后使用这些对象构建一个图,其中包含应用程序直接或间接引用的堆上所有对象。托管堆上任何可以被应用程序访问的对象都将被标记。有优化措施可以消除循环内存引用导致无限循环的可能性,并确保引用链只被处理一次。

图表完成后,GC 就拥有了堆上对象是垃圾还是非垃圾的完整图景。然后,GC 会通过将非垃圾项移到一起并将其“下一个可用内存槽”指针重置到这个新的、压缩后的堆的顶部来压缩堆。在此过程中,GC 还负责更新堆中所有指针的值,以便引用已移动到堆上的对象的引用仍然有效。

大对象(> 85,000 字节)的处理方式与小对象略有不同。此类大小的对象分配在一个单独的大堆上,当发生垃圾回收时,这些对象不会被移动,因为移动如此大的内存块会大大降低速度。

如果垃圾回收发生后,内存分配请求仍未获得足够的内存,则会引发 OutOfMemoryException 异常。

世代

虽然在托管堆上分配内存很快,但 GC 本身可能需要一些时间。考虑到这一点,已经进行了多项优化以提高性能。GC 支持代(generations)的概念,该概念基于这样一个假设:对象在堆上存在的时间越长,它可能就会继续存在在那里。当对象在堆上分配时,它属于第 0 代。每次垃圾回收该对象存活下来,它的代就会增加 1(目前支持的最高代是 2)。显然,搜索和垃圾回收堆上对象的一个子集要快得多,因此 GC 可以选择只回收第 0、1 或 2 代对象(或它选择的任何组合,直到有足够的内存)。即使只回收较年轻的对象,GC 也可以确定旧对象是否引用了新对象,以确保它不会无意中忽略正在使用的对象。

System.GC 包含垃圾回收对象,并且有许多静态方法可用于直接控制该过程。

void GC::Collect() 调用所有代的 GC,而 void GC::Collect(int Generation) 仅调用指定代及以下的所有代。

可以通过查询 GC::MaxGeneration 来找到支持的最大代数,如果您想知道您的对象当前属于哪一代,可以调用 int GC::GetGeneration(Object* obj)

您通常会让 GC 自己处理,但如果您知道您的应用程序即将开始快速需要大量内存,或者您知道您已经用完了大量内存并且有一些空闲时间,那么可以考虑给 GC 一个提示,告诉它它的服务是需要的。

如果您想知道当前分配了多少字节,只需调用

_int64 GC::GetTotalMemory(bool forceFullCollection);

参数 forceFullCollection 决定了函数在报告内存量之前是否应等待 GC 发生。

弱引用

您可能需要分配一个对象到内存中,但之后只偶尔需要引用该对象。如果您可以跟踪该对象,但在内存开始变得紧张时将其释放给 GC,那将非常有用。这可以通过弱引用实现。本质上,您分配一个对象,创建一个指向该对象的弱引用,然后删除您对该对象的任何直接引用,以便 GC 在需要时可以回收它。

弱引用使用 WeakReference 类创建,然后通过访问 WeakReference::Target 来重新创建对象的强引用。完成对象使用后,您可以再次将其作为弱引用释放。

// Create your managed object
MyObject* myObj = new MyObject();

// Create a weak reference
WeakReference* weak = new WeakReference(myObj);

// Remove your direct (strong) reference
myObj = 0;

... 

// Now do all accessing of your object via the weak reference
object  myObj = weak->Target; 
if (myObj)
{ 
    // The object still exists - We can use it!
    ... 
    // Release all strong references again. Use weak->Target to grab it again later.
    myObj = 0;
} 
else
{
    // The object is shark bait. Time to create another and use it instead :(
}    

检查弱引用是否仍然有效的另一个选项是查询 WeakReference::IsAlive 属性。

那么,您如何维护对对象的引用(即使是弱引用),同时仍允许 GC 在调用 GC 时回收该对象?当创建弱引用时,会在弱引用表中创建一个条目,其中包含对象的地址(这将是您的对象指针的值)。GC 在遍历应用程序对象时不会考虑此表,但在 GC 列出所有垃圾对象后,它会查找弱引用表,并将所有指向垃圾的条目设置为 NULL,然后回收对象。因此,下次您尝试通过弱引用访问对象时,将获得 NULL。

内存分配

托管堆上内存分配的一个重要方面是,当您分配连续的内存块时,您可以确信您将获得连续的内存位置。通过使相关对象在物理上靠近,您可以获得更少的页面错误和对象更有可能在缓存中的好处。这是一个很好的性能提升。

要实际声明和创建一个可供垃圾回收器访问的对象,您需要在类声明前加上 __gc 关键字。例如

__gc class CMyGCClass
{
    int n;
};

这将声明一个名为 CMyGCClass 的类并将其标记为托管,这意味着它必须在托管堆上声明,并且将受到垃圾回收的约束。要创建对象,您需要使用 new 关键字在堆上创建它

CMyGCClass *gc = new MyGCClass();

要释放对象,只需将其引用设置为 NULL

gc = 0;

析构函数、Dispose 和资源管理

基 .NET 对象 System::Object 包含一个可重写的 Finalize 方法,该方法由框架自动调用,以便在对象销毁之前有机会释放所有资源。其签名是

protected: virtual void Object::Finalize();

在 .NET Framework 的 Beta 版本中,可以重写此函数,但在 .NET 最终发布版中,对于托管 C++ 和 C# 则不再如此。现在,您可以使用标准的析构函数语法。这既方便又安全,因为它意味着键入更少,并保证将调用基类的 Finalize 实现。在幕后,当您编写

~CMyGCClass()
{
   // Cleanup code
}

编译器会将其翻译为

protected:
    void Finalize()
    {
       try
       {
          // Cleanup code
       }
       finally
       {
          CMyGCBaseClass::Finalize();
       }
    }

您的析构函数,因此 Finalize 方法将在对象经过垃圾回收后在不确定的时间被垃圾回收器调用。您不能指望在释放对象引用时立即调用它。这意味着您的对象可能分配的资源(例如句柄或数据库连接)可能会一直保留到程序结束(除非您调用 GC::Collect(),但这就像用一把大锤子解决一个小问题)。实现终结方法还会将对象提升到更高的代,这意味着它们可能会在内存中停留更长时间。对于应用程序退出时仍然存活的对象,也无法保证会发生终结(这允许应用程序更快地关闭)。资源会被回收,但不是以一种优雅的方式。此外,可终结对象分配需要更长时间。

您应该继承您的类自 IDisposable 接口并实现 Dispose 方法,而不是实现 Finalize 方法。

public virtual void Dispose();

此方法应负责所有资源释放,并调用 GC:SuppressFinalization(this),该方法指示运行时不要调用对象的 Finalize,因为您已经完成了所有清理。调用 Finalize 会产生一些开销,因此如果您可以避免,请这样做。

推荐的模式是声明一个 Dispose 方法(并继承自 IDisposable),并在您想要释放对象资源时调用它。

CMyClass *mc = new CMyClass();
mc->DoSomething();
mc->Dispose();
mc = 0;

上面的代码片段分配了一个对象,使用了该对象(在此过程中可能会分配资源),然后在将其释放给 GC 之前,以一种确定的方式强制对象进行清理。您可以完全控制所有资源管理,并将托管堆管理留给 GC。

实现这样一个类如下所示

__gc class CMyClass: public Object, public IDisposable
{
public:
    CMyClass()  
    {
        m_bDisposed = false;
    }

public:
    // You are encouraged to not allow a disposed object to be reused
    void MyMethod()
    {
       if (!m_bDisposed)
       {
           // do something
       }
       else
       {
           // throw an exception
       }
    }

public:
    // In case you would like to call 'Close' instead of 'Dispose'
    void Close() 
    {
        Dispose();
    }

    // defined in IDisposable
    void Dispose()
    {
        if (!m_bDisposed)
        {
            m_bDisposed = true;

            // Free all resources

            GC::SuppressFinalize(this);  
        }
    }

protected:
    bool m_bDisposed;
};

强烈建议在对象被 Dispose 后禁止使用该对象。请注意,在上面的代码中,我们同时提供了一个将自动调用的 Finalize 方法和一个必须由用户调用或通过包含对象调用的 Dispose 方法。在 Dispose 方法中,有这样一行

GC::SuppressFinalize(this); 

这指示 GC **不要**调用 Finalize 方法。我们已经完成了清理,因此不希望承担调用 Finalize 的开销。

示例代码

下面是一个简单的代码片段,演示了垃圾回收的一些要点

Console::WriteLine("Garbage Collection demonstration\n");

__int64 TotalMem = GC::GetTotalMemory(true);
Console::WriteLine("Currently used {0} bytes of memory", TotalMem.ToString());

// Wanton disregard for all the things we've been taught
Console::WriteLine("Creating a ton of junk");
for (int i = 0; i < 10000; i++)
    new MyGCClass();

TotalMem = GC::GetTotalMemory(false);
Console::WriteLine("Have now used {0} bytes of memory", TotalMem.ToString());

GC::Collect();

TotalMem = GC::GetTotalMemory(false);
Console::WriteLine("After GC, used {0} bytes of memory", TotalMem.ToString());

// -------------------------------------------------------------
// Demonstration of Generations...

Console::WriteLine("\nDemonstration of Generations\n");

String* str = new String("This is a string");
Console::WriteLine("We have created the string '{0}'", str);

// How old is it?
int nMaxGen = GC::MaxGeneration;
int nGen = GC::GetGeneration(str);
Console::WriteLine("The object's generation is '{0} (max {0})'", 
    nGen.ToString(), nMaxGen.ToString());

// Let's make it older and wiser
Console::WriteLine("Garbage Collecting...");
GC::Collect();
nGen = GC::GetGeneration(str);
Console::WriteLine("The object's generation is '{0} (max {0})'", 
    nGen.ToString(), nMaxGen.ToString());

Console::WriteLine("Garbage Collecting...");
GC::Collect();
nGen = GC::GetGeneration(str);
Console::WriteLine("The object's generation is '{0} (max {0})'", 
    nGen.ToString(), nMaxGen.ToString());

Console::WriteLine("Garbage Collecting...");
GC::Collect();
nGen = GC::GetGeneration(str);
Console::WriteLine("The object's generation is '{0} (max {0})'", 
    nGen.ToString(), nMaxGen.ToString());

// -------------------------------------------------------------
// Demonstration of Weak References...

Console::WriteLine("\nDemonstration of Weak References\n");

Console::WriteLine("Creating a weak reference.");
WeakReference* weak = new WeakReference(str);
str = 0;

if (weak->IsAlive)
{
    str = (String*) weak->Target;
    Console::WriteLine("Object is alive. [{0}]", str);
    str = 0;
}
else
    Console::WriteLine("Object is gone");

GC::Collect(0);

if (weak->IsAlive)
{
    str = (String*) weak->Target;
    Console::WriteLine("Object survived GC of generation 0. [{0}]", str);
    str = 0;
}
else
    Console::WriteLine("Object is gone (collected with Gen 0)");


GC::Collect(1);

if (weak->IsAlive)
{
    str = (String*) weak->Target;
    Console::WriteLine("Object survived GC of generation 1. [{0}]", str);
    str = 0;
}
else
    Console::WriteLine("Object is gone (collected with Gen 1)");


GC::Collect(2);

if (weak->IsAlive)
{
    str = (String*) weak->Target;
    Console::WriteLine("Object survived GC of generation 2. [{0}]", str);
    str = 0;
}
else
    Console::WriteLine("Object is gone (collected with Gen 2)");

// -------------------------------------------------------------
// Demonstration of Finalize/Dispose...

Console::WriteLine("\nDemonstration of Finalize/Dispose\n");


Console::WriteLine("Creating a CFinalizeTest object.");
CFinalizeTest* ft = new CFinalizeTest();
Console::WriteLine("Calling 'Close' then allowing the GC to collect it.");
ft->Close(); // or ft->Dispose() if you want
ft = 0;
GC::Collect();

Console::WriteLine("Creating a CFinalizeTest object, and will now delete it");
ft = new CFinalizeTest();
delete ft;
ft = 0;


Console::WriteLine("Creating a CFinalizeTest object, no Close or release to the GC.");
ft = new CFinalizeTest();

Console::WriteLine(" -- PROGRAM ENDS --\nPress Enter to continue");
Console::ReadLine();

历史

2001 年 4 月 26 日 - 添加了演示应用程序,更新了源代码示例。

2001 年 10 月 16 日 - 更新以支持 Beta 2。

2002 年 6 月 18 日 - 更新以支持 RTM。

© . All rights reserved.