C#中的资源回收
资源回收
https://blog.csdn.net/Blues1021/article/details/49046907
https://www.cnblogs.com/luminji/archive/2011/03/29/1997812.html
对象的生存期
在计算机程序中,资源的生存期代表了内存空间中该资源存在的时间。在 .NET Framework 中,托管资源的生存期由垃圾回收器(Garbage Collector,GC)负责管理。
托管资源和非托管资源
托管资源是由 CLR 负责分配和回收内存的资源。CLR 为托管资源提供垃圾回收器,以便在应用程序结束使用这些资源时自动清理它们。与托管资源对应的是非托管资源,例如文件句柄、数据库连接、COM 对象等,它们需要开发人员手动释放。
托管资源 | 描述 |
---|---|
类型 | 值类型、引用类型 |
非托管资源 | 描述 | .NET中与之相关的托管类 |
---|---|---|
GDI+相关的设备 | 调用外部 Win32 API 函数从操作系统获得的 | Graphics |
COM对象 | 由非托管代码、C++代码或其他不是由.NET编写的语言创建的DLL | 由.NET框架编写的DLL |
句柄 | 句柄是操作系统用于跟踪已打开文件的唯一标识符 | |
网络连接 | ||
数据库连接 | ||
被指针指向的对象 | 需要避免 |
托管资源
托管类型的资源会有GC工具像自动释放
值类型
引用类型
- 当托管资源没有被引用时才会被GC
- 若委托资源A不被引用,但委托资源A内部的委托资源a被单独引用了,那么A被GC,a不会被GC
资源释放的示例
程序运行
示例代码
Form4 form4;
private void button2_Click(object sender, EventArgs e)
{
Form3 form3 = new Form3();
from4 = new Form4();
form4.Controls.Add(form3.button);
}
//垃圾回收,该动作一般是自动触发
private void btnGC_Click(object sender, EventArgs e)
{
GC.Collect();
}
非托管资源
GC工具箱无法自动释放非托管资源,需要手动释放
句柄
句柄是操作系统用于跟踪已打开资源的唯一标识,句柄是由操作系统控制的,不被.NET的垃圾回收器管理
private void ExecuteShowMessage(object parameter)
{
var mat = Cv2.ImRead(@"E:\Picture\6-1.bmp");
Bitmap bitmap = BitmapConverter.ToBitmap(mat);
IntPtr hBitmap = IntPtr.Zero;
try
{
hBitmap = bitmap.GetHbitmap();
BitmapSource bitmapSource = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
hBitmap,
IntPtr.Zero,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
ImageSource= bitmapSource;
}
finally
{
if (hBitmap != IntPtr.Zero)
{
DeleteObject(hBitmap);
}
GC.Collect();
}
}
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeleteObject(IntPtr hObject);
IDispose
当对象被回收后,它的 Finalize() 方法会被调用。在该方法中,我们通常进行一些资源清理的工作,比如关闭文件、释放非托管资源等。需要特别注意的是,Finalize() 方法的执行是由垃圾回收器触发的,因此无法精准控制其执行时间,也无法确保其在何时执行完成。
为了避免这种不确定性,.NET 还提供了 IDisposable 接口和 using 块。使用 IDisposable 接口和 using 块可以在对象不再需要时手动释放资源,并确保资源及时得到释放。与自动触发的垃圾回收机制不同,IDisposable 接口和 using 块是由程序员主动触发的,因此可以在释放资源时进行精确控制。
使用using代码块可以明确作用域,防止出现非托管资源因为作用域过大,导致一直无法释放的情况
FileStream fileStream = new FileStream(@"F:\ttt.txt", FileMode.OpenOrCreate);
FileStream f = new FileStream(@"F:\ttt.txt", FileMode.OpenOrCreate);
//抛异常System.IO.IOException:“文件“F:\ttt.txt”正由另一进程使用,因此该进程无法访问此文件。”
FileStream fileStream = new FileStream(@"F:\ttt.txt", FileMode.OpenOrCreate);
fileStream.Dispose();//通过Dispose手动释放资源,就不会报错
FileStream f = new FileStream(@"F:\ttt.txt", FileMode.OpenOrCreate);
Dispose中的代码实现
析构函数
通过析构函数释放非托管资源
不使用析构函数
public class M
{
/// <summary>
/// 析构函数(终结器),当该对象被GC时会自动调用
/// 为了防止GC工具箱不知如何处理,对象内存在托管资源释放
/// </summary>
~M()
{
//保证GC时可以释放掉内存中的PDF
pdfViewer.Document.Dispose();
}
PdfViewer pdfViewer;
public void Build()
{
pdfViewer = new PdfViewer();
pdfViewer.Document = PdfDocument.Load(@"F:\Desktop\领域驱动设计模式、原理与实践.pdf");
}
}
public partial class Form1 : Form
{
private void button1_Click(object sender, EventArgs e)
{
//方法执行完成后,m对象被GC时,内存里的PDF并不会被自动释放
M m = new M();
m.Build();
}
}
析构函数函数的意义
对于非托管资源需要手动释放,但是在实际编程过程中,往往会忘记手动释放(或者是不方便手动释放),因此在需要在析构函数中加入释放的代码。当GC时可以自动释放
public class FileStream : Stream
{
private SafeFileHandle _handle;
/// <summary>释放由 <see cref="T:System.IO.FileStream" /> 占用的非托管资源,还可以另外再释放托管资源。</summary>
/// <param name="disposing">若要释放托管资源和非托管资源,则为 <see langword="true" />;若仅释放非托管资源,则为 <see langword="false" />。</param>
[SecuritySafeCritical]
protected override void Dispose(bool disposing)
{
try
{
if (this._handle == null || this._handle.IsClosed || this._writePos <= 0)
return;
this.FlushWrite(!disposing);
}
finally
{
if (this._handle != null && !this._handle.IsClosed)
this._handle.Dispose();
this._canRead = false;
this._canWrite = false;
this._canSeek = false;
base.Dispose(disposing);
}
}
}
/// <summary>表示操作系统句柄的包装类。 必须继承此类。</summary>
[SecurityCritical]
[__DynamicallyInvokable]
[SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
public abstract class SafeHandle : CriticalFinalizerObject, IDisposable
{
/// <summary>释放 <see cref="T:System.Runtime.InteropServices.SafeHandle" /> 类所使用的非托管资源,指定是否执行常规释放操作。</summary>
/// <param name="disposing">如进行常规释放操作,则为 <see langword="true" />;如终结句柄,则为 <see langword="false" />。</param>
[SecurityCritical]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[__DynamicallyInvokable]
protected virtual void Dispose(bool disposing)
{
if (disposing)
this.InternalDispose();
else
this.InternalFinalize();
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[MethodImpl(MethodImplOptions.InternalCall)]
private extern void InternalDispose();
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[MethodImpl(MethodImplOptions.InternalCall)]
private extern void InternalFinalize();
}
public abstract class Stream : MarshalByRefObject, IDisposable
{
/// <summary>关闭当前流并释放与之关联的所有资源(如套接字和文件句柄)。 不直接调用此方法,而应确保流得以正确释放。</summary>
public virtual void Close()
{
this.Dispose(true);
GC.SuppressFinalize((object) this);
}
/// <summary>释放由 <see cref="T:System.IO.Stream" /> 使用的所有资源。</summary>
[__DynamicallyInvokable]
public void Dispose()
{
this.Close();
}
}
COM对象
COM对象可能是托管资源也可能是非托管资源,这取决于它们是如何创建和使用的。
- 如果COM对象是由托管代码创建的,并由托管代码持有它们的引用并负责释放它们,那么它们就是托管资源。例如,使用C#或VB.NET编写的代码可以通过InteropServices名称空间来创建COM对象,然后CLR会管理它们的分配和释放。
- 但是,如果COM对象是由非托管代码、C++代码或其他不是由.NET编写的语言创建的,那么它们就是非托管资源。在这种情况下,我们需要手动跟踪它们的生命周期,并在使用完毕后显式地释放它们的资源。
//如果调用下面这段代码就属于调用非托管资源
[DllImport("gdi32.dll", EntryPoint = "GetDeviceCaps", SetLastError = true)]
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
当创建一个对象时,它会被分配到内存中。对象的生存期开始于该对象被创建(或被实例化)的时刻,结束于该对象被垃圾回收器回收的时刻。在生存期内,对象可以被引用和操作,一旦其生存期结束,该对象占用的内存空间会被释放,以便后续的内存分配使用。
需要注意的是,对象的生存期并不直接受到程序员的控制,而是由垃圾回收器根据程序的需要自动管理。程序员可以通过手动释放对象占用的内存空间以及控制对对象的引用,影响对象的生存期。
对象的作用域
对象的作用域表示了该对象的访问范围。在 C# 中,对象的作用域由其声明位置和访问修饰符决定。
对象的作用域可以分为以下几种:
局部作用域(Local Scope):局部作用域指在方法内部声明的对象,只能在声明的方法内部访问,一旦方法执行完毕,则对象将无法访问。
类作用域(Class Scope):类成员(如字段、属性、方法)所定义的对象的作用域为整个类,在类中任意地方都可以访问该对象。
包含作用域(Enclosing Scope):如果一个对象在嵌套的作用域内定义,则其作用域包含在最靠近的包含作用域内。这使得对象可以在所有包含的作用域内访问。
需要注意的是,对象在其作用域外部仍然存在时(例如保存在成员变量或静态变量中),其引用仍然存在,其生存期也不会结束。因此,在操作对象时需要考虑其作用域和引用关系,以避免内存泄漏和性能问题。
对象销毁的条件
在 .NET 中,当一个对象不再被应用程序使用时,它就会被垃圾回收器(Garbage Collector,简称 GC)标记为垃圾对象,并最终被回收。垃圾回收器负责回收所有未被引用的对象,从而释放相应的内存。
- 当一个对象被标记为垃圾时,不会立即被回收,而是会等待垃圾回收器的处理。垃圾回收器会在必要的时候(比如可用内存不足时)对堆中的对象进行清理,并回收被标记为垃圾的对象占用的内存。
在进行垃圾回收的过程中,垃圾回收器会:
- 标记所有被引用的对象;
- 标记所有被引用对象的引用对象;
- 标记得到的所有对象之外的所有对象为垃圾对象;
- 释放被标记为垃圾对象占用的内存。