前言
简单讲述一下垃圾回收,我们能做的一些控制。
正文
强制回收
- class Program
- {
- static void Main()
- {
- var str = new StringBuilder();
- var x = "";
- for (int i = 0; i < 500; i++)
- {
- x += "xxxxxxxxsadasdasdsadsaewqeqczxcxzgsfaswqeqwrqwewqeasdasqweqwrqsdasddas";
- }
- for (int i = 0; i < 10000; i++)
- {
- str.Append(x);
- }
-
- var str2 = str.ToString();
- GC.Collect(2, GCCollectionMode.Forced);
-
- // 等待用户按 Enter 键
- Console.ReadLine();
- }
- }
复制代码 当我们执行完GC.Collect之后,我们发现str2不存在了,被回收了。
这里之所以参数是2,是因为str2是大对象,那么天生就在第二代。
可在进程中调用几个方法来监视垃圾会回收器。具体地说,GC类提供了以下静态方法,可调用它们查看某一代发生了多少次垃圾回收,或者托管堆中的对象当前使用了多少内存。- Int32 CollectionCount(Int32 generation);
- Int64 GetTotalMemory(Boolean forceFullCollection);
复制代码 我们如何去监控垃圾回收呢?
对于.net 而言我们会有自己的工具之类的。
还有一个很出色的工具可分析内存和应用程序的性能,它的名字是 PerfView。 该工具能收集 “Windows 事件跟踪”(Event Tracing for Windows, ETW)日志并处理它们。获取该工具最好的办法是网上搜索 PerfView 。最后还应该考虑一下 SOS Debugging Extension(SOS.dll),它对于内存问题和其他 CLR 问题的调试颇有帮助。对于内存有关的行动, SOS Debugging Extension 允许检查进程中为托管堆分配了多少内存,显示在终结队列中登记终结的所有对象,显示每个 AppDomain 或整个进程的 GCHandle 表中的记录项,并显示是什么根保持对象在堆中存活。
这样可以通过外部事件的方式暴露出来。
通用,我们的内存资源被托管了,那么我们的本机资源我们如何清理呢?
例如,System.IO.FileStream 类型需要打开一个文件(本机资源)并保存文件的句柄。然后,类型的Read 和 Write 方法用句柄操作文件。类似地,System.Threading.Mutex 类型要打开一个 Windows 互斥体内核对象(本机资源)并保存其句柄,并在调用 Mutex 的方法时使用该句柄。
包含本机资源的类型被 GC 时, GC 会回收对象在托管堆中使用的内存。但这样会造成本机资源(GC 对它一无所知)的泄露,这当然是不允许的。所以,CLR 提供了称为终结(finalization)的机制,允许对象在被判定为垃圾之后,但在对象内存被回收之前执行一些代码。任何包装了本机资源(文件、网络连接、套接字、互斥体)的类型都支持终结。CLR 判定一个对象不可达时,对象将终结它自己,释放它包装的本机资源。之后,GC 会从托管堆回收对象。
终极基类 System.Object 定义了受保护的虚方法 Finalize。垃圾回收器判定对象是垃圾后,会调用对象的 Finalize 方法(如果重写)。Microsoft 的 C# 团队认为 Finalize 在编程语言中需要特殊语法(类似于 C# 要求用特殊语法定义构造器)。因此,C# 要求在类名前添加 ~符号来定义 Finalize 方法,如下例所示:- internal sealed class SomeType {
- // 这是一个 Finalize 方法
- ~SomeType() {
- // 这里的代码会进入 Finalize 方法
- }
- }
复制代码 编译上述代码,用 ILDasm.exe 检查得到的程序集,会发现 C# 编译器实际是在模块的元数据中生成了名为 Finalize 的 protected override 方法。查看 Finalize 的 IL,会发现方法主体的代码被放到一个 try 块中,在 finally 块中则放入了一个 base.Finalize 调用。
被视为垃圾的对象在垃圾回收完毕后才调用 Finalize 方法,所以这些对象的内存不是马上被回收的,因为 Finalize 方法可能要执行访问字段的代码。可终结对象在回收时必须存活,造成它被提升到另一代,使对象活的比正常时间长。这增大了内存耗用,所以应尽可能避免终结。更糟的是,可终结对象被提升时,其字段引用的所有对象也会被提升,因为它们也必须继续存活。所以,要尽量避免为引用类型的字段定义可终结对象。
另外要注意,Finalize 方法的执行时间是控制不了的。应用程序请求更多内存时才可能发生 GC,而只有 GC 完成后才运行 Finalize。另外,CLR 不保证多个 Finalize 方法的调用顺序。所以,在 Finalize 方法中不要访问定义了Finalize方法的其他类型的对象;那些对象可能已经终结了。但可以安全地访问值类型的实例,或者访问没有定义 Finalize 方法的引用类型的对象。调用静态方法也要当心,这些方法可能在内部访问已终结的对象,导致静态方法的行为变得无法预测。
CLR 用一个特殊的、高优先级的专用线程调用 Finalize 方法来避免死锁①。如果 Finalize 方法阻塞(例如进入死循环,或等待一个永远不发出信号的对象),该特殊线程就调用不了任何更多的 Finalize 方法。这是非常坏的情况,因为应用程序永远回收不了可终结对象占用的内存————只要应用程序运行就会一直泄露内存。如果 Finalize 方法抛出未处理的异常,则进程终止,没办法捕捉该异常。
创建封装了本机资源的托管类型时,应该先从 System.Runtime.InteropServices.SafeHandle 这个特殊基类派生出一个类。该类的形式如下(我在方法中添加了注释,指明它们做的事情):- public abstract class SafeHandle : CriticalFinalizerObject, IDisposable {
- // 这是本机资源的句柄
- protected IntPtr handle;
- protected SafeHandle(IntPtr invalidHandleValue, Boolean ownsHandle) {
- this.handle = invalidHandleValue;
- // 如果 ownsHandle 为 true,那么这个从 SafeHandle 派生的对象被回收时,
- // 本机资源会被关闭
- }
- protected void SetHandle(IntPtr handle) {
- this.handle = handle;
- }
- // 可调用 Dispose 显式释放资源
- // 这是 IDisposable 接口的 Dispose 方法
- public void Dispose() { Dispose(true); }
- // 默认的 Dispose 实现(如下所示)正是我们希望的。强烈建议不要重写这个方法
- protected virtual void Dispose(Boolean disposing) {
- // 这个默认实现会忽略 disposing 参数:
- // 如果资源已经释放,那么返回;
- // 如果 ownsHandle 为 false, 那么返回;
- // 设置一个标志来指明该资源已经释放;
- // 调用虚方法 ReleaseHandle;
- // 调用 GC.SuppressFinalize(this)方法来阻止调用 Finalize 方法;
- // 如果 ReleaseHandle 返回 true,那么返回;
- // 如果走到这一步,就激活 releaseHandleFailed 托管调试助手(MDA)。
- }
- // 默认的 Finalize 实现(如下所示)正是我们希望的。强烈建议不要重写这个方法
- ~SafeHandle() { Dispose(false); }
- // 派生类要重写这个方法以实现释放资源的代码
- protected abstract Boolean ReleaseHandle();
- public void SetHandleAsInvalid()
- {
- // 设置标志来指出这个资源已经释放
- // 调用 GC.SuppressFinalize(this) 方法来阻止调用 Finalize 方法
- }
- public Boolean IsClosed
- {
- get
- {
- // 返回指出资源是否释放的一个标志
- }
- }
- public abstract Boolean IsInvalid
- {
- // 派生类要重写这个属性
- // 如果句柄的值不代表资源(通常意味着句柄为 0 或 -1),实现应返回 true
- get;
- }
- // 以下三个方法涉及安全性和引用计数,本节最后会讨论它们
- public void DangerousAddRef(ref Boolean success) { ... }
- public IntPtr DangerousGetHandle() { ... }
- public void DangerousRelease() { ... }
- }
复制代码 举一个简单点的例子:- using System;
- class ResourceWrapper : IDisposable
- {
- private IntPtr handle;
- private bool disposed = false;
- public ResourceWrapper()
- {
- handle = SomeNativeLibrary.OpenResource();
- }
- public void DoSomething()
- {
- if (disposed)
- throw new ObjectDisposedException("ResourceWrapper");
- // Use the resource
- }
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
- protected virtual void Dispose(bool disposing)
- {
- if (!disposed)
- {
- if (disposing)
- {
- // Release managed resources
- }
- // Release unmanaged resources
- SomeNativeLibrary.CloseResource(handle);
- handle = IntPtr.Zero;
- disposed = true;
- }
- }
- ~ResourceWrapper()
- {
- Dispose(false);
- }
- }
- class Program
- {
- static void Main()
- {
- using (ResourceWrapper resource = new ResourceWrapper())
- {
- resource.DoSomething();
- }
- }
- }
复制代码 可以看下这个disposing:- if (disposing)
- {
- // Release managed resources
- }
复制代码 Release managed resources 是什么呢? 其实就是封装好的文件流啥的,这个时候我们可以手动释放掉。- 释放托管资源通常包括清理对象持有的其他托管对象或资源,例如关闭文件流、释放数据库连接、清理缓存等。这些资源由.NET框架管理,不需要手动释放内存,但需要确保在不再需要时正确释放资源以避免内存泄漏。
- 在C#中,托管资源由.NET的垃圾回收器自动管理。垃圾回收器会跟踪对象的引用情况,并在对象不再被引用时自动释放其占用的内存。这种自动内存管理机制减少了内存泄漏的风险,简化了开发人员的工作,因此不需要手动释放托管资源。
复制代码 然后呢,在终结器里面呢,我们不能去这么做。
为什么呢? 这是因为终结器里面可能这个对象都不在了,去调用这个对象可能产生意想不到的结构。
然后看到dispose 里面,我们看到了这个:GC.SuppressFinalize(this);
为什么我们要抑制终结器呢?
我们在调用dispose的时候呢,如果我们后面不用终结器的话,那么是很好的,为什么呢?
因为前面说过,终结器是在垃圾回收之后执行的,也就是说终结器对象不会立即消失,并且和其引用存留到下一代,这对垃圾回收不好。
如果我们能关掉,那么就已经抑制了,那么就不会这么消耗性能。
那么我们怎么使用呢?- using System;
- using System.IO;
- public static class Program {
- public static void Main() {
- // 创建要写入临时文件的字节
- Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 };
- // 创建临时文件
- FileStream fs = new FileStream("Temp.dat", FileMode.Create);
- // 将字节写入临时文件
- fs.Write(bytesToWrite, 0, bytesToWrite.Length);
- // 删除临时文件
- File.Delete("Temp.dat"); // 抛出 IOException 异常
- }
- }
复制代码 这里我们学过操作系统的话,那么这个时候大多数时候会出现问题的。
因为fs的时候已经拿到了句柄,然后File.Delete("Temp.dat")再去删除的话,那么会出现问题。
在我们学完垃圾回收后,那么是有可能可以成果的。
为什么呢? 因为垃圾回收机制会回收fs的句柄,然后正好File 就可以删除了。
这种概率比较好。- using System;
- using System.IO;
- public static class Program {
- public static void Main() {
- // 创建要写入临时文件的字节
- Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 };
- // 创建临时文件
- FileStream fs = new FileStream("Temp.dat", FileMode.Create);
- // 将字节写入临时文件
- fs.Write(bytesToWrite, 0, bytesToWrite.Length);
- // 写入结束后显式关闭文件
- fs.Dispose();
- // 删除临时文件
- File.Delete("Temp.dat"); // 总能正常工作
- }
- }
复制代码 这个时候我们可以手动释放了,就能正常工作。
这似乎也可以。
我一直有一个想法,为啥不交给我们去释放呢?而要交给垃圾回收呢。- using System;
- using System.IO;
- public static class Program {
- public static void Main() {
- // 创建要写入临时文件的字节
- Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 };
- // 创建临时文件
- FileStream fs = new FileStream("Temp.dat", FileMode.Create);
- // 将字节写入临时文件
- fs.Write(bytesToWrite, 0, bytesToWrite.Length);
- // 写入结束后显式关闭文件
- fs.Dispose();
- // 关闭文件后继续写入
- fs.Write(bytesToWrite, 0, bytesToWrite.Length); // 抛出 ObjectDisposedException
- // 删除临时文件
- File.Delete("Temp.dat"); // 总能正常工作
- }
- }
复制代码 这里在调用第二个fs.Write,那么会抛出异常,也就是说ObjectDisposedException,就是说被disposed了。
这里告诉我们一个事情,那就是很多时候我们不知道我们啥时候要disposed,因为对象可能相互引用。
除非是知道了,以后再也不会使用这个资源对象了,才可以disposed。
我们一般释放的时候这样写:- using System;
- using System.IO;
- public static class Program {
- public static void Main() {
- // 创建要写入临时文件的字节
- Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 };
- // 创建临时文件
- FileStream fs = new FileStream("Temp.dat", FileMode.Create);
- try {
- // 将字节写入临时文件
- fs.Write(bytesToWrite, 0, bytesToWrite.Length);
- }
- finally {
- // 写入字节后显式关闭文件
- if (fs != null) fs.Dispose();
- }
- // 删除临时文件
- File.Delete("Temp.dat"); // 总能正常工作
- }
- }
复制代码 然后我们可以这样写:- using System;
- using System.IO;
- public static class Program {
- public static void Main() {
- // 创建要写入临时文件的字节
- Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 };
- // 创建临时文件
- using (FileStream fs = new FileStream("Temp.dat", FileMode.Create)) {
- // 将字节写入临时文件
- fs.Write(bytesToWrite, 0, bytesToWrite.Length);
- }
- // 删除临时文件
- File.Delete("Temp.dat"); // 总能正常工作
- }
- }
复制代码 一个容易忽略的问题:
奇怪的依赖:- FileStream fs = new FileStream("DataFile.dat", FileMode.Create);
- StreamWriter sw = new StreamWriter(fs);
- sw.Write("Hi there");
- // 不要忘记写下面这个 Dispose 调用
- sw.Dispose();
- // 注意:调用 StreamWriter.Dispose 会关闭 FileStream
- // FileStream 对象无需显式关闭
复制代码 当sw调用Dispose,会将内存的数据刷入到磁盘,同时Dispose会调用FileStream的Dispose。
也就是说和资源一起刷完。
那么能不能不调用Dispose呢? 让终结器自己去刷呢?
是不能的,因为可能出现一个问题,因为FileStream和StreamWriter不是同一个对象。
那么StreamWriter使用终结器的时候呢,可能FileStream被终结了,那么刷新数据的时候呢,就会报错。
那么怎么办呢?那只能去调用这个StreamWriter去解决问题。
不然就可能丢失。
那么GC 为本机还提供了什么其他功能呢?
GC 没有提供让我们手动去控制啥时候回收呢?
也是有的。
比如内存达到多少的时候。- public static void AddMemoryPressure(Int64 bytesAllocated);
- public static void RemoveMemoryPressure(Int64 bytesAllocated);
复制代码 那有没有句柄数量达到一定的时候呢?
也是有的。- public sealed class HandleCollector {
- public HandleCollector(String name, Int32 initialThreshold);
- public HandleCollector(String name, Int32 initialThreshold, Int32 maximumThreshold);
- public void Add();
- public void Remove();
- public Int32 Count { get; }
- public Int32 InitialThreshold { get; }
- public Int32 MaximumThreshold { get; }
- public String Name { get; }
- }
复制代码 其实他们的原理,还是调用GC.Collect,只是多一个监控而已。
那么我们重新来看一下终结器的原理:
首先呢,gc会像往常一样进行回收,还记得我们的gc怎么回收的吗?
这个时候e,i,j 都是垃圾了。
然后又一个问题,那就是终结器列表中,有这几个,所以不能回收。
这个时候e,i,j的引用要标记为可以复活,也就是说继续到下一代。
并且把e,i,j 移动到freachable列表中去。
然后将会有一个独立的线程,去执行终结器,然后从freachable列表中移除。
下一次执行垃圾回收的时候呢,因为终结列表中没有,所以可以直接清除掉。
这个也不一定是下一次,因为升代了,所以呢,可以几次gc后才回收。
结
那么我们还可以控制gc的对象的回收,我们下下章就行解释。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |