.NET 内存管理:弱引用、关键终结对象与资源管理
1. 弱引用概述
弱引用是 .NET 框架中一个非常实用的功能。它与强引用不同,强引用是通过
new
操作符创建的常规引用,只要存在强引用指向对象,对象就会一直保留在内存中。而弱引用不足以将对象保留在内存中,当内存不足时,具有弱引用的对象可能会被垃圾回收。在使用弱引用对象之前,需要确认该对象是否已被回收。
2. 弱引用的创建与使用
弱引用通常先以强引用的形式存在,然后再转换为弱引用。以下是创建和使用弱引用的步骤:
1.
创建强引用对象
:
XNames objTemp = new XNames();
-
创建弱引用对象
:使用
WeakReference类的构造函数,将强引用对象作为参数传入,然后将强引用置为null。
XNames objTemp = new XNames();
WeakReference weaknames = new WeakReference(objTemp);
objTemp = null;
-
使用弱引用对象
:在使用对象之前,通过
WeakReference.Target属性获取强引用。如果Target为null,则表示对象已被回收,需要重新加载。
if (weaknames.Target == null)
{
// 执行重新加载操作
}
3. 示例代码:读取文件中的名称列表
以下是一个读取文件中名称列表的示例代码:
class XNames
{
public XNames()
{
StreamReader sr = new StreamReader("names.txt");
string temp = sr.ReadToEnd();
_Names = temp.Split('\n');
}
private string [] _Names;
public IEnumerator<string> GetEnumerator()
{
foreach(string name in _Names)
{
yield return name;
}
}
}
4. 弱引用应用示例:Weak 应用程序
在
Weak
应用程序中,使用
XNames
类读取文件中的名称并显示在列表框中。以下是相关的代码:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void btnFill_Click(object sender, EventArgs e)
{
if (weaknames.Target == null)
{
DialogResult result = MessageBox.Show(
"Rehydrate?",
"Names removed from memory.",
MessageBoxButtons.YesNo);
if (result == DialogResult.No)
{
return;
}
else
{
weaknames.Target = new XNames();
}
}
foreach(string name in (XNames)weaknames.Target)
{
lblNames.Items.Add(name);
}
}
private WeakReference weaknames;
private void Form1_Load(object sender, EventArgs e)
{
XNames objTemp = new XNames();
weaknames = new WeakReference(objTemp);
objTemp = null;
}
private void btnApply_Click(object sender, EventArgs e)
{
objs.Add(new ZClass());
if (weaknames.Target == null)
{
lblNames.Items.Clear();
}
}
List<ZClass> objs = new List<ZClass>();
}
internal class ZClass
{
public long[] array = new long[7500];
}
5. 弱引用的内部机制
弱引用通过短弱引用表和长弱引用表进行跟踪。这些表最初是空的,每个短弱引用表的条目都是对堆上托管对象的引用。当垃圾回收发生时,短弱引用表中没有稳定根的对象可能会被回收,对应的表项会被置为
null
。长弱引用表中的对象在终结时也会被跟踪,允许弱引用对象重新激活。
6. WeakReference 类的重要成员
| 成员名称 | 描述 |
|---|---|
WeakReference (object)
| 单参数构造函数,初始化对目标对象的弱引用。 |
WeakReference (object, Boolean)
|
双参数构造函数,初始化对目标对象的弱引用。如果
trackResurrection
为
true
,则在终结时也跟踪对象。
|
IsAlive
| 属性,用于判断目标对象是否已被回收。 |
Target
| 属性,获取或设置被引用的对象。 |
7. 关键终结对象
在某些情况下,正常的终结器和清理代码可能无法执行,导致资源泄漏等问题。.NET Framework 2.0 引入了关键终结对象,确保关键终结器的执行。关键终结器在垃圾回收操作中,会在正常终结器之后执行。关键终结对象派生自
CriticalFinalizeObject
,其终结器在受限执行区域(CER)中执行。
8. 受限执行区域(CER)
受限执行区域是一段保证能执行完毕的代码区域,即使发生异步异常也不会中断。在该区域内,开发者需要避免一些操作,例如:
- 装箱操作
- 不安全代码
- 锁定操作
- 序列化操作
- 调用不可信代码
- 可能引发异步异常的操作
以下是一个 CER 区域的示例代码:
[ReliabilityContract(Consistency.WillNotCorruptState,
Cer.Success)]
class ZClass {
void MethodA() {
RuntimeHelpers.PrepareConstrainedRegions();
try {
}
finally {
// 受限执行区域
}
}
}
9. 安全句柄
CriticalFinalizerObject
的部分实现位于
System.Runtime.InteropServices
命名空间中。
CriticalHandle
、
SafeHandle
等类派生自
CriticalFinalizerObject
,适用于大多数情况。这些类是内核句柄的安全包装,确保句柄能正确关闭。
以下是一个
PipeHandle
类的示例代码,它是管道句柄的安全包装:
public sealed class PipeHandle :
SafeHandleMinusOneIsInvalid
{
private PipeHandle()
: base(true)
{
}
[ReliabilityContract(Consistency.WillNotCorruptState,
Cer.Success)]
protected override bool ReleaseHandle()
{
return CloseHandle(handle);
}
[DllImport("kernel32.dll")]
extern public static bool CreatePipe(
out PipeHandle hReadPipe,
out PipeHandle hWritePipe,
IntPtr securityAttributes,
int nSize);
[ReliabilityContract(Consistency.WillNotCorruptState,
Cer.Success)]
[DllImport("kernel32.dll")]
public static extern bool CloseHandle(IntPtr handle);
}
public class AnonymousPipe
{
public AnonymousPipe()
{
PipeHandle.CreatePipe(out readHandle, out writeHandle,
IntPtr.Zero, 10);
MessageBox.Show((readHandle.DangerousGetHandle())
.ToInt32().ToString());
MessageBox.Show((writeHandle.DangerousGetHandle())
.ToInt32().ToString());
}
private PipeHandle readHandle = null;
private PipeHandle writeHandle = null;
}
10. 非托管资源管理
托管代码通常依赖于非托管资源,这些资源一般通过托管包装器访问。例如,
MyDevice
是一个模拟硬件设备的非托管应用程序,
DeviceWrapper
是其托管包装器。以下是
MyDevice
类的代码:
public sealed class MyDevice
{
static private int count = 0;
public MyDevice()
{
obj = new MyDeviceLib.DeviceClass();
++count;
}
private MyDeviceLib.DeviceClass obj;
public void Open()
{
obj.OpenDevice();
}
public void Close()
{
obj.CloseDevice();
}
public void Start()
{
obj.StartCommunicating();
}
public void Stop()
{
obj.StopCommunicating();
}
~MyDevice()
{
// 释放资源
--count;
}
}
11. 内存压力
非托管资源包装器可能会掩盖对象在内存方面的真实成本,导致内存不足异常。.NET 2.0 引入了内存压力机制,通过
GC.AddMemoryPressure
和
GC.RemoveMemoryPressure
方法来估计非托管内存的使用情况,从而提前触发垃圾回收,释放非托管资源。
以下是更新后的
MyDevice
类代码,加入了内存压力估计:
public MyDevice()
{
GC.AddMemoryPressure(40000);
obj = new MyDeviceLib.DeviceClass();
++count;
}
~MyDevice()
{
GC.RemoveMemoryPressure(40000);
// 释放资源
--count;
}
12. 句柄管理
内核资源是有限的,当资源耗尽时可能会导致应用程序挂起或崩溃。
HandleCollector
类用于管理内核资源的句柄,它可以管理任何有限可用性的资源。该类的构造函数有三个参数,分别是名称、初始阈值和最大阈值。当资源可用性超过阈值时,会触发垃圾回收,回收包含非托管资源的包装器,恢复本地资源。
以下是更新后的
MyDevice
类代码,加入了
HandleCollector
:
public sealed class MyDevice
{
static private HandleCollector track = new HandleCollector("devices", 3, 5);
static private int count = 0;
public MyDevice()
{
GC.AddMemoryPressure(40000);
track.Add();
obj = new MyDeviceLib.DeviceClass();
++count;
MessageBox.Show("Device count: " + count.ToString());
}
private MyDeviceLib.DeviceClass obj;
public void Open()
{
obj.OpenDevice();
}
public void Close()
{
obj.CloseDevice();
}
public void Start()
{
obj.StartCommunicating();
}
public void Stop()
{
obj.StopCommunicating();
}
~MyDevice()
{
GC.RemoveMemoryPressure(40000);
track.Remove();
// 释放资源
--count;
}
}
13. 总结
通过弱引用、关键终结对象、安全句柄和资源管理等技术,可以更好地管理 .NET 应用程序的内存和资源,避免内存泄漏和资源耗尽等问题。在实际开发中,需要根据具体情况选择合适的技术,确保应用程序的稳定性和性能。
以下是一个简单的流程图,展示了弱引用对象的使用流程:
graph TD;
A[创建强引用对象] --> B[创建弱引用对象];
B --> C[将强引用置为 null];
C --> D[使用弱引用对象];
D --> E{Target 是否为 null};
E -- 是 --> F[重新加载对象];
E -- 否 --> G[使用对象];
通过以上的介绍和示例代码,希望能帮助你更好地理解 .NET 内存管理的相关知识。在实际开发中,合理运用这些技术可以提高应用程序的性能和稳定性。
.NET 内存管理:弱引用、关键终结对象与资源管理(续)
14. 弱引用的适用场景
弱引用虽然有其独特的优势,但并非适用于所有场景。它不适用于包含难以重新水化信息的对象。当对象的信息是从持久化源(如文件或数据库)读取时,使用弱引用更为合适,因为可以轻松地重新读取文件或再次请求数据。
以下是一个简单的决策流程图,帮助判断是否适合使用弱引用:
graph TD;
A[对象信息是否难以重新水化] --> B{是};
B -- 是 --> C[不适合使用弱引用];
B -- 否 --> D[信息是否来自持久化源];
D --> E{是};
E -- 是 --> F[适合使用弱引用];
E -- 否 --> G[需进一步评估];
15. 关键终结对象的使用场景
关键终结对象主要用于确保关键终结器的执行,避免因某些情况(如应用程序域卸载)导致资源泄漏。在一些对资源管理要求较高的环境中,如 Microsoft SQL Server 2005 等托管 CLR 的环境,关键终结对象尤为重要。
使用关键终结对象的步骤如下:
1. 派生自
CriticalFinalizeObject
类。
2. 在终结器中编写关键清理代码。
3. 使用
RuntimeHelpers.PrepareConstrainedRegions
方法将清理代码放在受限执行区域(CER)中。
16. 安全句柄的使用优势
安全句柄(如
CriticalHandle
、
SafeHandle
等)是内核句柄的安全包装,它们确保内核句柄能正确关闭,避免资源泄漏。这些类实现了引用计数或其他机制,使得资源管理更加安全可靠。
安全句柄的使用步骤如下:
1. 派生自相应的安全句柄类(如
SafeHandleMinusOneIsInvalid
)。
2. 重写
ReleaseHandle
方法,在其中编写释放句柄的代码,并添加可靠性合约。
3. 在构造函数中初始化句柄。
17. 非托管资源管理的最佳实践
在管理非托管资源时,为了避免内存泄漏和资源耗尽问题,需要遵循以下最佳实践:
-
使用
Dispose
模式
:实现
IDisposable
接口,在
Dispose
方法中释放非托管资源。
-
估计内存压力
:使用
GC.AddMemoryPressure
和
GC.RemoveMemoryPressure
方法估计非托管内存的使用情况。
-
管理句柄
:使用
HandleCollector
类管理有限可用性的资源句柄。
以下是一个使用
Dispose
模式的示例代码:
class YClass : IDisposable
{
private bool disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// 释放托管资源
}
// 释放非托管资源
disposed = true;
}
}
~YClass()
{
Dispose(false);
}
}
18. 内存管理的性能优化
合理的内存管理可以提高应用程序的性能。以下是一些内存管理的性能优化建议:
-
减少对象创建
:尽量重用对象,避免频繁创建和销毁对象。
-
及时释放资源
:在对象不再使用时,及时调用
Dispose
方法释放资源。
-
使用弱引用
:对于一些不常用的对象,使用弱引用可以减少内存占用。
19. 总结与回顾
在 .NET 开发中,内存管理是一个至关重要的方面。通过使用弱引用、关键终结对象、安全句柄和合理的资源管理技术,可以有效地避免内存泄漏和资源耗尽问题,提高应用程序的稳定性和性能。
下面通过一个表格来回顾本文介绍的主要技术及其作用:
| 技术名称 | 作用 |
| — | — |
| 弱引用 | 减少内存占用,适用于从持久化源读取信息的对象 |
| 关键终结对象 | 确保关键终结器的执行,避免资源泄漏 |
| 安全句柄 | 安全包装内核句柄,确保句柄正确关闭 |
| 非托管资源管理 | 避免非托管资源泄漏,通过
Dispose
模式、内存压力估计和句柄管理实现 |
| 内存压力机制 | 提前触发垃圾回收,释放非托管资源 |
| 句柄管理(
HandleCollector
) | 管理有限可用性的资源句柄 |
希望通过本文的介绍,你能对 .NET 内存管理有更深入的理解,并在实际开发中合理运用这些技术,提升应用程序的质量。
graph LR;
A[弱引用] --> B[内存管理];
C[关键终结对象] --> B;
D[安全句柄] --> B;
E[非托管资源管理] --> B;
F[内存压力机制] --> B;
G[句柄管理] --> B;
B --> H[提高应用程序性能和稳定性];
在实际开发过程中,要根据具体的业务需求和场景,灵活运用这些技术,不断优化内存管理策略,以实现最佳的性能和稳定性。
57

被折叠的 条评论
为什么被折叠?



