本篇文章给大家谈谈强制GC是怎么玩的10种,以及清理和垃圾收集对应的知识点,文章可能有点长,但是希望大家可以阅读完,增长自己的知识,最重要的是希望对各位有所帮助,可以解决了您的问题,不要忘了收藏本站喔。
某些对象需要显式拆卸代码来释放资源,例如打开的文件、锁、操作系统句柄和非托管对象。在.NET术语中,这称为,它通过IDisposable接口提供支持。未使用的对象占用的托管内存也必须在某个时候回收;此函数称为,由CLR执行。
处置与垃圾收集的不同之处在于,处置通常是明确发起的;垃圾回收是完全自动的。换句话说,程序员负责释放文件句柄、锁和操作系统资源等事项,而CLR负责释放内存。
本章讨论处置和垃圾回收,还介绍了C#终结器以及它们为处置提供备份的模式。最后,我们讨论了垃圾回收器和其他内存管理选项的复杂性。
可识别、释放和关闭.NET为需要拆卸方法的类型定义了一个特殊接口:
publicinterfaceIDisposablen{nvoidDispose();n}
C#的using语句提供了一个语法快捷方式,用于在实现IDisposable的对象上调用Dispose使用一个try/finally块:
using(FileStreamfs=newFileStream("myFile.txt",FileMode.Open))n{n//...Writetothefile...n}
编译器将其转换为以下内容:
FileStreamfs=newFileStream("myFile.txt",FileMode.Open);ntryn{n//...Writetothefile...n}nfinallyn{nif(fs!=null)((IDisposable)fs).Dispose();n}
finally块可确保即使引发异常或代码提前退出块,也会调用Dispose方法。
同样,以下语法可确保在fs超出范围时立即进行处置:
usingFileStreamfs=newFileStream("myFile.txt",FileMode.Open);nn//...Writetothefile...
在简单方案中,编写自己的一次性类型只需实现IDisposable并编写Dispose方法:
sealedclassDemo:IDisposablen{npublicvoidDispose()n{n//Performcleanup/tear-down.n...n}n}注意
此模式在简单情况下效果很好,适用于密封类。在中,我们描述了一种更复杂的模式,它可以为忘记调用Dispose的使用者提供备份。对于未密封的类型,从一开始就有充分的理由遵循后一种模式-否则,如果子类型想要添加此类功能本身,则会变得非常混乱。
标准处置语义.NET在其处置逻辑中遵循一组事实上的规则。这些规则不会以任何方式硬连接到.NET或C#语言;它们的目的是为消费者定义一致的协议。他们在这里:
对象被处置后,它就无法赎回了。它不能被重新激活,调用它的方法或属性(Dispose除外)会抛出ObjectDisposedException。重复调用对象的Dispose方法不会导致错误。如果一次性对象x“拥有”一次性对象y,的Dispose方法会自动调用的Dispose方法,除非另有说明。这些规则在编写自己的类型时也很有帮助,尽管它们不是必需的。没有什么能阻止你写一个“Undispose”的方法,也许除了你可能会从同事那里得到的抨击!
根据规则3,容器对象会自动释放其子对象。一个很好的例子是Windows窗体容器控件,如窗体或面板。容器可以承载许多子控件,但你不会显式释放每个子控件;关闭或处置父控件或窗体将处理整个批次。另一个例子是当您将文件流包装在DeflateStream中时。释放DeflateStream也会释放FileStream—除非您在构造函数中另有说明。
关闭和停止某些类型除了定义“释放”之外,还定义了一个名为“关闭”的方法。.NETBCL在Close方法的语义上并不完全一致,尽管在几乎所有情况下,它都是以下任一情况:
功能上与“处置”相同“释放”的功能后者的一个例子是IDb连接:关闭的连接可以重新打开;a不能释放d连接。另一个例子是使用显示对话框激活的Windows表单:关闭隐藏它;释放释放其资源。
一些类定义了一个Stop方法(例如,计时器或HttpListener)。Stop方法可以释放非托管资源,如处置,但与处置不同,它允许重新启动。
何时处置要遵循的安全规则(在几乎所有情况下)是“如果有疑问,请处置”。包装非托管资源句柄的对象几乎总是需要处置才能释放句柄。示例包括文件或网络流、网络套接字、Windows窗体控件、GDI+笔、画笔和位图。相反,如果类型是一次性的,则它通常会(但并非总是)直接或间接引用非托管句柄。这是因为非托管句柄提供了通往操作系统资源、网络连接和数据库锁的“外部世界”的网关,如果对象被不当放弃,对象可能会在自身外部造成麻烦。
但是,有三种情况释放:
当您不“拥有”对象时,例如,通过静态字段或属性获取对象时当对象的Dispose方法执行您不希望的操作时当对象的Dispose方法在是不必要的,并且释放该对象会增加程序的复杂性时第一类很少见。主要情况在System.Drawing命名空间中:通过(如Brushes.Blue)获取的GDI+对象绝不能释放,因为在应用程序的整个生命周期中使用相同的实例。但是,通过构造函数获得的实例(例如新的SolidBrush)应该被释放,通过静态(例如Font.FromHdc)获得的实例也被释放。
第二类更常见。和System.Data命名空间中有一些很好的例子:
类型
处置功能
何时不处置
内存流
防止进一步的I/O
当您以后需要读/写流时
StreamReader,
刷新读取器/写入器并关闭基础流
当您想要保持基础流处于打开状态时(完成后,您必须在StreamWriter上调用Flush)
IDb连接
释放数据库连接并清除连接字符串
如果需要重新打开它,则应调用“关闭”而不是“处置”
DbContext(EFCore)
防止进一步使用
当您可能延迟计算连接到该上下文的查询时
MemoryStream的Dispose方法只禁用对象;它不执行任何关键清理,因为内存流不保存非托管句柄或其他此类资源。
第三类包括StringReader和StringWriter等类。这些类型在其基类的胁迫下是一次性的,而不是通过真正需要执行必要的清理。如果您碰巧完全在一种方法中实例化和处理此类对象,则将其包装在using块中几乎不会增加不便。但是,如果对象更持久,则跟踪何时不再使用,以便您可以处理它会增加不必要的复杂性。在这种情况下,您可以简单地忽略对象处置。
注意忽略处置有时会产生性能成本(请参阅)。
清理处置中的油田通常,不需要在其Dispose方法中清除对象的字段。但是,最好取消订阅对象在其生存期内在内部订阅的事件(有关示例,请参阅)。取消订阅此类事件可防止收到不需要的事件,并防止无意中使对象在垃圾回收器(GC)眼中保持活动状态。
注意Dispose方法本身不会导致释放(托管)内存—这只能在垃圾回收中发生。
还值得设置一个字段来指示对象已释放,以便在使用者稍后尝试调用对象上的成员时可以抛出ObjectDisposedException。一个好的模式是为此使用可公开读取的自动属性:
publicboolIsDisposed{get;privateset;}
尽管在技术上没有必要,但在Dispose方法中清除对象自己的事件处理程序(通过将它们设置为null)也是很好的。这消除了在处置期间或之后触发这些事件的可能性。
有时,对象包含高价值机密,例如加密密钥。在这些情况下,在处置期间从字段中清除此类数据是有意义的(以避免在稍后将内存释放到操作系统时被计算机上的其他进程发现)。系统中的对称算法类。Security.Cryptography通过在保存加密密钥的字节数组上调用Array.Clear来实现这一点。
匿名处置有时,实现IDisposable而无需编写类很有用。例如,假设您要在类上公开挂起和恢复事件处理的方法:
classFoon{nint_suspendCount;nnpublicvoidSuspendEvents()=>_suspendCount++;npublicvoidResumeEvents()=>_suspendCount--;nnvoidFireSomeEvent()n{nif(_suspendCount==0)n...firesomeevent...n}n...n}
这样的API使用起来很笨拙。消费者必须记得调用恢复事件.为了健壮,它们必须在finally块中执行此操作(以防抛出异常):
varfoo=newFoo();nfoo.SuspendEvents();ntryn{n...dostuff...//Becauseanexceptioncouldbethrownheren}nfinallyn{nfoo.ResumeEvents();//...wemustcallthisinafinallyblockn}
更好的模式是取消ResumeEvents并让SuspendEvents返回一个IDisposable.然后,使用者可以执行以下操作:
using(foo.SuspendEvents())n{n...dostuff...n}
问题在于,这会将工作推给必须实现SuspendEvents方法的人。即使努力减少空格,我们最终也会得到这种额外的混乱:
publicIDisposableSuspendEvents()n{n_suspendCount++;nreturnnewSuspendToken(this);n}nnclassSuspendToken:IDisposablen{nFoo_foo;npublicSuspendToken(Foofoo)=>_foo=foo;npublicvoidDispose()n{nif(_foo!=null)_foo._suspendCount--;n_foo=null;//Preventagainstconsumerdisposingtwicen}n}
模式解决了这个问题。使用以下可重用类
publicclassDisposable:IDisposablen{npublicstaticDisposableCreate(ActiononDispose)n=>newDisposable(onDispose);nnAction_onDispose;nDisposable(ActiononDispose)=>_onDispose=onDispose;nnpublicvoidDispose()n{n_onDispose?.Invoke();//Executedisposalactionifnon-null.n_onDispose=null;//Ensureitcan’texecuteasecondtime.n}n}
我们可以将暂停事件方法简化为以下内容:
publicIDisposableSuspendEvents()n{n_suspendCount++;nreturnDisposable.Create(()=>_suspendCount--);n}自动垃圾回收
无论对象是否需要自定义拆卸逻辑的Dispose方法,在某些时候都必须释放它在堆上占用的内存。CLR通过自动GC完全自动处理它的这一侧。您永远不会自己释放托管内存。例如,请考虑以下方法:
publicvoidTest()n{nbyte[]myArray=newbyte[1000];n...n}
执行Test时,将在内存堆上分配一个可容纳1,000字节的数组。该数组由存储在局部变量堆栈上的变量myArray引用。当方法退出时,这个局部变量myArray会弹出范围,这意味着没有任何东西可以引用内存堆上的数组。然后,孤立数组就有资格在垃圾回收中回收。
注意在禁用优化的调试模式下,局部变量引用的对象的生存期将扩展到代码块的末尾,以便于调试。否则,它将在不再使用的最早时间点有资格进行收集。
垃圾回收不会在对象孤立后立即发生。就像街上的垃圾收集一样,它会定期发生,尽管(与街上的垃圾收集不同)没有固定的时间表。CLR根据许多因素来决定何时收集,例如可用内存、内存分配量和自上次收集以来的时间(GC会自行调整以针对应用程序的特定内存访问模式进行优化)。这意味着在对象被孤立和从内存中释放之间存在不确定的延迟。此延迟的范围可以从纳秒到几天不等。
注意GC不会在每次收集时收集所有垃圾。相反,内存管理器将对象划分为,并且GC收集新世代(最近分配的对象)的频率高于旧世代(长期生存的对象)。我们将在中更详细地讨论这一点。
垃圾回收和内存消耗GC尝试在进行垃圾回收所花费的时间与应用程序的内存消耗(工作集)之间取得平衡。因此,应用程序消耗的内存可能超过其需要的内存,尤其是在构造大型临时阵列时。
可以通过Windows任务管理器或资源监视器监视进程的内存消耗,也可以通过查询性能计数器以编程方式监视进程的内存消耗:
//ThesetypesareinSystem.Diagnostics:nstringprocName=Process.GetCurrentProcess().ProcessName;nusingPerformanceCounterpc=newPerformanceCountern("Process","PrivateBytes",procName);nConsole.WriteLine(pc.NextValue());
这将查询专用工作集,该提供了程序内存消耗的最佳总体指示。具体而言,它排除了CLR在内部解除分配的内存,并且愿意在另一个进程需要时撤销到操作系统的内存。
根是使对象保持生命的东西。如果某个对象未被根直接或间接引用,则该对象将有资格进行垃圾回收。
根是以下之一:
执行方法(或其调用堆栈中的任何方法)中的局部变量或参数静态变量队列上的一个对象,用于存储准备完成的对象(请参阅下一节)代码不可能在已删除的对象中执行,因此如果存在执行(实例)方法的可能性,则必须以某种方式以这些方式之一引用其对象。
请注意,一组循环相互引用的对象在没有根引用的情况下被视为死对象(参见)。换句话说,无法通过根对象的箭头(引用)访问的对象是的,因此需要收集。
终结者在对象从内存中释放之前,其将运行(如果有)。终结器像构造函数一样声明,但它以?符号为前缀:
classTestn{n?Test()n{n//Finalizerlogic...n}n}
(尽管在声明中类似于构造函数,但终结器不能声明为公共或静态,不能有参数,也不能调用基类。
终结器是可能的,因为垃圾回收在不同的阶段工作。首先,GC识别适合删除的未使用对象。没有终结器的那些将立即删除。那些具有待处理(未运行)终结器的终结器将保持活动状态(暂时)并被放入特殊队列中。
此时,垃圾回收已完成,程序将继续执行。然后,启动并开始与程序并行运行,从该特殊队列中选取对象并运行其终结方法。在每个对象的终结器运行之前,它仍然非常活跃-该队列充当根对象。在取消排队并执行终结器后,该对象将成为孤立对象,并将在下一个集合中删除(对于该对象的)。
终结器可能很有用,但它们带有一些附带条件:
终结器减慢了内存的分配和收集速度(GC需要跟踪哪些终结器已运行)。终结器延长了对象和任何对象的生存期(它们都必须等待下一辆垃圾车进行实际删除)。无法预测一组对象的终结器将以什么顺序调用。您对何时调用对象的终结器的控制有限。如果终结器中的代码阻塞,则无法完成其他对象。如果应用程序无法完全卸载,则可以完全绕过终结器。总之,终结者有点像律师——尽管在某些情况下你确实需要它们,但一般来说,除非绝对必要,否则你不想使用它们。如果您确实使用它们,则需要100%确定您了解它们为您做什么。
以下是实现终结器的一些准则:
确保终结器快速执行。永远不要在你的终结器中阻塞(参见中的)。不要引用其他可完成的对象。不要抛出异常。注意CLR可以调用对象的终结器,即使在构造期间引发异常也是如此。出于这个原因,在编写终结器时,不要假设字段已正确初始化。
从终结器调用释放一种流行的模式是让终结器调用Dispose。当清理不紧急时,这是有道理的,并且通过调用Dispose来加速清理更像是一种优化而不是必要。
注意请记住,使用此模式,您将内存释放与资源释放耦合-这两个事情可能具有不同的兴趣(除非资源本身是内存)。您还会增加完成线程的负担。
此模式还可以作为使用者只是忘记调用Dispose的情况的备份。但是,最好记录故障,以便修复错误。
有一个实现此目的的标准模式,如下所示:
classTest:IDisposablen{npublicvoidDispose()//NOTvirtualn{nDispose(true);nGC.SuppressFinalize(this);//Preventfinalizerfromrunning.n}nnprotectedvirtualvoidDispose(booldisposing)n{nif(disposing)n{n//CallDispose()onotherobjectsownedbythisinstance.n//Youcanreferenceotherfinalizableobjectshere.n//...n}nn//Releaseunmanagedresourcesownedby(just)thisobject.n//...n}nn~Test()=>Dispose(false);n}
释放已重载以接受布尔处置标志。无参数版本声明为虚拟版本,而只是使用true调用增强版本。
增强版包含实际处置逻辑,受保护且;这为子类添加自己的处置逻辑提供了一个安全点。释放标志意味着它从Dispose方法“正确”调用,而不是从终结器以“最后手段模式”调用。这个想法是,当设置为false时,此方法通常不应该使用终结器引用其他对象(因为这些对象本身可能已经完成,因此处于不可预测的状态)。这排除了很多!以下是当释放为false时,Dispose方法仍然可以在最后手段模式下执行的几个任务:
释放对操作系统资源的任何(可能通过对Win32API的调用获得)删除在施工时创建的临时文件为了使它健壮,任何能够引发异常的代码都应该包装在try/catch块中,理想情况下,异常被记录下来。任何日志记录都应尽可能简单和可靠。
请注意,我们调用GC。在无参数Dispose方法中抑制终结-这可以防止终结器在GC稍后赶上它时运行。从技术上讲,这是不必要的,因为Dispose方法必须容忍重复调用。但是,这样做可以提高性能,因为它允许在单个周期内对对象(及其引用的对象)进行垃圾回收。
复活假设终结器修改了一个活的对象,使其引用回垂死的对象。当下一次垃圾回收发生时(对于对象的生成),CLR会将先前消亡的对象视为不再是孤立对象,因此它将逃避垃圾回收。这是一个高级场景,称为。
为了说明这一点,假设我们要编写一个管理临时文件的类。当该类的实例被垃圾回收时,我们希望终结器删除临时文件。听起来很简单:
publicclassTempFileRefn{npublicreadonlystringFilePath;npublicTempFileRef(stringfilePath){FilePath=filePath;}nn~TempFileRef(){File.Delete(FilePath);}n}
不幸的是,这有一个错误:File.Delete可能会引发异常(可能是由于缺少权限,或者文件正在使用中或已被删除)。这样的异常会关闭整个应用程序(以及阻止其他终结器运行)。我们可以简单地用一个空的捕获块“吞下”异常,但那时我们永远不会知道出了什么问题。调用一些精心设计的错误报告API也是不可取的,因为它会给终结器线程带来负担,从而阻碍其他对象的垃圾回收。我们希望将最终确定操作限制为简单、可靠和快速的操作。
更好的选择是将失败记录到静态集合中,如下所示:
publicclassTempFileRefn{nstaticinternalreadonlyConcurrentQueue<TempFileRef>FailedDeletionsn=newConcurrentQueue<TempFileRef>();nnpublicreadonlystringFilePath;npublicExceptionDeletionError{get;privateset;}nnpublicTempFileRef(stringfilePath){FilePath=filePath;}nn~TempFileRef()n{ntry{File.Delete(FilePath);}ncatch(Exceptionex)n{nDeletionError=ex;nFailedDeletions.Enqueue(this);//Resurrectionn}n}n}
将对象排队到静态FailedDeletes集合会为对象提供另一个裁判,确保它保持活动状态,直到对象最终取消排队。
注意ConcurrentQueue<T>是Queue<T的线程安全版本>在System.Collections.Concurrent中定义(参见)。使用线程安全集合有几个原因。首先,CLR保留在多个线程上并行执行终结器的权利。这意味着在访问共享状态(如静态集合)时,我们必须考虑同时完成两个对象的可能性。其次,在某些时候,我们将希望从FailedDeletes中取消项目排队,以便我们可以对它们做一些事情。这也必须以线程安全的方式完成,因为它可能会在终结器同时对另一个对象进行排队时发生。
气相色谱。重新注册以完成复活对象的终结器不会再次运行,除非您调用GC。重新注册完成.
在下面的示例中,我们尝试删除终结器中的临时文件(如上一个示例所示)。但是,如果删除失败,我们将重新注册该对象,以便在下一次垃圾回收中重试:
publicclassTempFileRefn{npublicreadonlystringFilePath;nint_deleteAttempt;nnpublicTempFileRef(stringfilePath){FilePath=filePath;}nn~TempFileRef()n{ntry{File.Delete(FilePath);}ncatchn{nif(_deleteAttempt++<3)GC.ReRegisterForFinalize(this);n}n}n}
在第三次尝试失败后,我们的终结器将静默地放弃尝试删除文件。我们可以通过将它与前面的示例相结合来增强这一点—换句话说,在第三次失败后将其添加到FailedDeletes队列中。
警告请注意,在终结器方法中只调用一次ReRegisterForFinalize。如果您调用它两次,该对象将被重新注册两次,并且必须再进行两次最终确定!
GC的工作原理标准CLR使用分代标记和紧凑GC,该GC对存储在托管堆上的对象执行自动内存管理。GC被视为跟踪GC,因为它不会干扰对对象的每次访问,而是间歇性地唤醒并存储在托管堆上的对象的图形,以确定哪些对象可以被视为垃圾并因此被收集。
GC在执行内存分配(通过new关键字)时启动垃圾回收,无论是在分配了特定的内存阈值之后,还是在其他时间减少应用程序的内存占用量。此过程也可以通过调用System.GC.Collect手动启动。在垃圾回收期间,所有线程都可以冻结(下一节将对此进行详细介绍)。
GC从其根对象引用开始,遍历对象图,将其接触的所有对象标记为可访问。完成此过程后,所有未标记的对象都被视为未使用,并受到垃圾回收。
没有终结器的未使用对象将立即丢弃;GC完成后,具有终结器的未使用对象将排队等待在终结器线程上进行处理。然后,这些对象有资格在下一个GC中收集对象生成(除非复活)。
然后将剩余的“活动”对象移动到堆的开头(压缩),从而为更多对象释放空间。这种压缩有两个目的:它可以防止内存碎片,并允许GC在分配新对象时采用非常简单的策略,即始终在堆末尾分配内存。这可以防止维护可用内存段列表的潜在耗时任务。
如果在垃圾回收后没有足够的空间为新对象分配内存,并且操作系统无法授予更多内存,则会引发内存不足异常。
注意您可以通过调用GC来获取有关托管堆当前状态的信息。GetGCMemoryInfo().从.NET5开始,此方法已得到增强,可返回与性能相关的数据。
优化技术GC结合了各种优化技术来减少垃圾收集时间。
世代集合最重要的优化是GC是分代的。这利用了这样一个事实,即尽管许多对象被快速分配和丢弃,但某些对象是长期存在的,因此不需要在每次收集期间进行跟踪。
基本上,GC将托管堆分为三代。刚刚分配的对象在中,在一个收集周期中幸存下来的对象在中;所有其他对象都在中。Gen0和Gen1被称为短暂()世代。
CLR使Gen0部分保持相对较小(典型大小为几百KB到几MB)。当Gen0部分填满时,GC会发起Gen0集合,这种情况相对频繁地发生。GC对Gen1应用类似的内存阈值(充当Gen2的缓冲区),因此Gen1集合也相对快速和频繁。但是,包含Gen2的完整集合需要更长的时间,因此很少发生。显示了完整集合的效果。
为了给出一些非常粗略的数字,Gen0集合可能需要不到一毫秒的时间,这不足以在典型的应用程序中引起注意。但是,在具有大型对象图的程序上,完整集合可能需要长达100毫秒的时间。这些数字取决于许多因素,因此可能会有很大差异-特别是在Gen2的情况下,其大小是的(与Gen0和Gen1不同)。
结果是,短期对象在使用GC时非常有效。用以下方法创建的StringBuilder几乎肯定会收集在快速Gen0中:
stringFoo()n{nvarsb1=newStringBuilder("test");nsb1.Append("...");nvarsb2=newStringBuilder("test");nsb2.Append(sb1.ToString());nreturnsb2.ToString();n}大型对象堆
GC对大于特定阈值(当前为85,000字节)的对象使用称为堆(LOH)的单独堆。这样可以防止压缩大型对象的成本,并防止过多的Gen0集合—如果没有LOH,分配一系列16MB的对象可能会在每次分配后触发Gen0集合。
默认情况下,LOH不受压缩的影响,因为在垃圾回收期间移动大块内存将非常昂贵。这有两个后果:
分配可能会更慢,因为GC不能总是简单地在堆的末尾分配对象-它还必须在中间查找间隙,这需要维护可用内存块的链接列表。1LOH会受到的影响。这意味着释放对象可能会在LOH中创建一个孔,以后可能难以填充。例如,86,000字节对象留下的孔只能由85,000字节到86,000字节之间的对象填充(除非与另一个孔相邻)。如果预计会出现碎片问题,可以指示GC在下一个集合中压缩LOH,如下所示:
GCSettings.LargeObjectHeapCompactionMode=nGCLargeObjectHeapCompactionMode.CompactOnce;
如果程序经常分配大型数组,另一种解决方法是使用。NET的阵列池API(请参阅)。
LOH也是非代际的:所有对象都被视为Gen2。
工作站与服务器集合.NET提供两种垃圾回收模式:和。是默认的;可以通过将以下内容添加到应用程序的文件来切换到:
<PropertyGroup>n<ServerGarbageCollection>true</ServerGarbageCollection>n</PropertyGroup>
生成项目后,此设置将写入应用程序的.文件,CLR将读取该文件:
"runtimeOptions":{n"configProperties":{n"System.GC.Server":truen...
启用服务器收集后,CLR将为每个核心分配一个单独的堆和GC。这加快了收集速度,但会消耗额外的内存和CPU资源(因为每个内核都需要自己的线程)。如果计算机在启用服务器收集的情况下运行许多其他进程,这可能会导致CPU超额订阅,这对工作站尤其有害,因为它会使整个操作系统感觉无响应。
服务器集合仅在多核系统上可用:在单核设备(或单核虚拟机)上,将忽略该设置。
背景收集在工作站和服务器模式下,CLR默认启用。可以通过将以下内容添加到应用程序的文件来禁用它:
<PropertyGroup>n<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>n</PropertyGroup>
生成时,此设置将写入应用程序的.文件:
"runtimeOptions":{n"configProperties":{n"System.GC.Concurrent":false,n...
GC必须在集合期间冻结(阻止)执行线程一段时间。后台收集可最大程度地减少这些延迟时间,从而使应用程序的响应速度更快。这是以消耗更多CPU和内存为代价的。因此,通过禁用后台收集,您可以完成以下操作:
略微降低CPU和内存使用率增加垃圾回收时的暂停(或)后台集合的工作原理是允许应用程序代码与Gen2集合并行运行。(Gen0和Gen1集合被认为足够快,因此它们不会从这种并行性中受益。
后台集合是以前称为并发集合的改进版本:它消除了一个限制,即如果在Gen0集合运行时Gen2部分填满,并发集合将不再。这使得不断分配内存的应用程序能够更快地响应。
气相色谱通知如果禁用后台收集,则可以要求GC在发生完全(阻止)收集之前通知您。这适用于服务器场配置:其理念是您将请求转移到集合之前的另一台服务器。然后,立即启动集合并等待它完成,然后再将请求重新路由回该服务器。
要开始通知,请调用GC。注册完整GCNotification.然后,启动第一个调用GC的另一个线程(参见)。等待完整GCApproach.当此方法返回指示某个集合已接近的GCNotificationStatus时,您可以将请求重新路由到其他服务器并强制手动收集(请参阅下一节)。然后调用GC。WaitForFullGCComplete:当此方法返回时,收集完成,您可以再次接受请求。然后重复整个循环。
强制垃圾回收您可以随时通过调用GC手动强制垃圾回收。收集。调用GC。没有参数的收集会激发完整的集合。如果传入整数值,则仅收集该值的代,因此GC。Collect(0)只执行快速的Gen0收集。
通常,通过允许GC决定何时收集来获得最佳性能:强制收集可能会不必要地将Gen0对象提升到Gen1(以及将Gen1对象提升到Gen2)来损害性能。它还可能破坏GC的自我调整能力,即GC动态每一代的阈值,以便在应用程序执行时最大限度地提高性能。
但是,也有例外。最常见的干预情况是应用程序进入睡眠状态一段时间:一个很好的例子是执行日常活动的Windows服务(也许是检查更新)。此类应用程序可能使用System.Timers.Timer每24小时启动一次活动。完成活动后,24小时内不会执行进一步的代码,这意味着在此期间不会进行内存分配,因此GC没有机会激活。无论服务在执行其活动时消耗了什么内存,它都会在接下来的24小时内继续消耗-即使对象图为空也是如此!解决方案是调用GC。在日常活动完成后立即收集。
若要确保收集对象被终结器延迟,请执行调用WaitForPendingFinalizers并重新收集的额外步骤:
GC.Collect();nGC.WaitForPendingFinalizers();nGC.Collect();
这通常是在循环中完成的:运行终结器的行为可以释放更多本身具有终结器的对象。
调用GC的另一种情况。收集是指测试具有类。
在运行时调整垃圾回收静态GCSettings.LatencyMode属性确定GC如何平衡延迟与整体效率。将其从默认值“交互式”更改为“低延迟”或“持续低延迟”指示CLR支持更快(但更频繁)的集合。如果您的应用程序需要非常快速地响应实时事件,这将非常有用。将模式更改为“批处理”可以最大化吞吐量,但代价是响应能力可能较差,。
如果在.文件中禁用后台收集,则不支持持续低延迟。
还可以通过调用GC来告知CLR暂时挂起垃圾回收。TryStartNoGCRegion,并使用GC恢复它。结束编号.
内存压力运行时根据多种因素(包括计算机上的总内存负载)决定何时启动集合。如果程序分配非托管内存(),运行时将对其内存使用情况产生不切实际的乐观感知,因为CLR只知道托管内存。可以通过指示CLR已分配指定数量的非托管内存来缓解此问题;您可以通过调用GC来执行此操作。添加内存压力.若要撤消此操作(释放非托管内存时),请调用GC。删除内存压力.
阵列池如果应用程序频繁实例化数组,则可以通过避免大部分垃圾回收开销。数组池是在.NETCore3中引入的,它的工作原理是“租用”一个数组,稍后返回到池中以供重用。
若要分配数组,请在System.Buffers命名空间中的ArrayPool类上调用Rent方法,指示所需的数组大小:
int[]pooledArray=ArrayPool<int>.Shared.Rent(100);//100bytes
这将从全局共享阵列池中分配一个(至少)100字节的数组。池管理器可能会为您提供一个大于您请求的数组(通常,它以2的幂分配)。
完成数组后,调用Return:这会将数组释放到池中,允许再次租用相同的数组:
ArrayPool<int>.Shared.Return(pooledArray);
您可以选择传入一个布尔值,指示池管理器在将数组返回到池之前清除数组。
警告数组池的一个限制是,没有什么能阻止你在返回数组后继续(非法)使用数组,因此您需要仔细编码以避免这种情况。请记住,您不仅可以破坏自己的代码,还可以破坏使用数组池的其他API,例如ASP.NETCore。
您可以创建自定义池并从中租用,而不是使用共享阵列池。这可以防止破坏其他API的风险,但会增加总体内存使用量(因为它减少了重用的机会):
varmyPool=ArrayPool<int>.Create();nint[]array=myPool.Rent(100);n...托管内存泄漏
在非托管语言(如C++)中,您必须记住在不再需要对象时手动释放内存;否则,将导致。在托管世界中,由于CLR的自动垃圾回收系统,这种错误是不可能的。
尽管如此,大型和复杂的.NET应用程序可能会表现出具有相同最终结果的相同综合征的温和形式:应用程序在其生存期内消耗越来越多的内存,直到最终必须重新启动。好消息是,托管内存泄漏通常更容易诊断和预防。
托管内存泄漏是由未使用的对象由于未使用或忘记的引用而保持活动状态引起的。一个常见的候选项是事件处理程序,它们保存对目标对象的引用(除非目标是静态方法)。例如,考虑以下类:
classHostn{npubliceventEventHandlerClick;n}nnclassClientn{nHost_host;npublicClient(Hosthost)n{n_host=host;n_host.Click+=HostClicked;n}nnvoidHostClicked(objectsender,EventArgse){...}n}
以下测试类包含一个实例化1,000个客户端的方法:
classTestn{nstaticHost_host=newHost();nnpublicstaticvoidCreateClients()n{nClient[]clients=Enumerable.Range(0,1000)n.Select(i=>newClient(_host))n.ToArray();nn//Dosomethingwithclients...n}n}
您可能期望在CreateClient完成执行后,1,000个客户端对象将有资格进行收集。遗憾的是,每个客户端都有另一个引用:其Click事件现在引用每个客户端实例的_host对象。如果Click事件未触发,或者HostClicked方法未执行任何操作来吸引注意,则可能会忽略这一点。
解决此问题的一种方法是使客户端实现IDisposable,并在Dispose方法中取消挂钩事件处理程序:
publicvoidDispose(){_host.Click-=HostClicked;}
然后,客户端的使用者在完成实例处理后释放实例:
Array.ForEach(clients,c=>c.Dispose());注意
在中,我们描述了此问题的另一种解决方案,该解决方案在倾向于不使用一次性对象的环境中非常有用(例如WindowsPresentationFoundation[WPF])。事实上,WPF提供了一个名为WeakEventManager的类,该类使用使用弱引用的模式。
定时器被遗忘的计时器也会导致内存泄漏(我们将在第中讨论计时器)。有两种不同的方案,具体取决于计时器的类型。让我们首先看一下System.Timers命名空间中的计时器。在下面的示例中,Foo类(实例化时)每秒调用一次tmr_Elapsed方法:
usingSystem.Timers;nnclassFoon{nTimer_timer;nnFoo()n{n_timer=newSystem.Timers.Timer{Interval=1000};n_timer.Elapsed+=tmr_Elapsed;n_timer.Start();n}nnvoidtmr_Elapsed(objectsender,ElapsedEventArgse){...}n}
不幸的是,Foo的实例永远不能被垃圾回收!问题在于运行时本身保留对活动计时器的引用,以便它可以触发其已用事件;因此:
运行时将使_timer保持活动状态。_timer将通过tmr_Elapsed事件处理程序使Foo实例保持活动状态。当您意识到计时器实现时,解决方案是显而易见的IDisposable.释放计时器会停止它,并确保运行时不再引用该对象:
classFoo:IDisposablen{n...npublicvoidDispose(){_timer.Dispose();}n}注意
一个好的准则是,如果类中的任何字段被分配了实现IDisposable的对象,则自己实现IDisposable。
WPF和Windows窗体计时器的行为方式与刚才讨论的内容相同。
但是,System.Threading命名空间中的计时器是特殊的。.NET不保存对活动线程计时器的引用;而是直接引用回调委托。这意味着,如果您忘记释放线程计时器,终结器可能会触发,该终结器将自动停止并释放计时器:
staticvoidMain()n{nvartmr=newSystem.Threading.Timer(TimerTick,null,1000,1000);nGC.Collect();nSystem.Threading.Thread.Sleep(10000);//Wait10secondsn}nnstaticvoidTimerTick(objectnotUsed){Console.WriteLine("tick");}
如果此示例在“发布”模式下编译(禁用调试并启用优化),则计时器将在有机会触发一次之前被收集并完成!同样,我们可以通过在完成后处理计时器来解决此问题:
using(vartmr=newSystem.Threading.Timer(TimerTick,null,1000,1000))n{nGC.Collect();nSystem.Threading.Thread.Sleep(10000);//Wait10secondsn}
对tmr的隐式调用。在使用块的末尾释放可确保tmr变量被“使用”,因此在块结束之前不会被GC视为死机。具有讽刺意味的是,这种对Dispose的调用实际上使对象存活!
诊断内存泄漏避免托管内存泄漏的最简单方法是在编写应用程序时主动监视内存消耗。您可以按如下方式获取程序对象的当前内存(true参数告诉GC首先执行收集):
longmemoryUsed=GC.GetTotalMemory(true);
如果您正在实践测试驱动开发,一种可能性是使用单元测试来断言内存已按预期回收。如果此类断言失败,则只需检查最近所做的更改。
如果您已经有一个具有托管内存泄漏的大型应用程序,工具可以帮助找到它。还有更友好的图形工具,如Microsoft的CLRProfiler,SciTech的MemoryProfiler和RedGate的ANTSMemory。
CLR还公开了许多事件计数器来帮助进行资源监视。
弱引用有时,保留对GC“不可见”的对象的引用很有用,以使对象保持活动状态。这称为,由System.WeakReference类实现。
要使用弱引用,请使用目标对象构造它:
varsb=newStringBuilder("thisisatest");nvarweak=newWeakReference(sb);nConsole.WriteLine(weak.Target);//Thisisatest
如果目标被一个或多个弱引用引用,则GC将认为该目标符合收集条件。收集目标时,弱引用的目标属性将为null:
varweak=GetWeakRef();nGC.Collect();nConsole.WriteLine(weak.Target);//(nothing)nnWeakReferenceGetWeakRef()=>nnewWeakReference(newStringBuilder("weak"));
若要防止在测试目标为null和使用目标之间收集目标,请将目标分配给局部变量:
varsb=(StringBuilder)weak.Target;nif(sb!=null){/*Dosomethingwithsb*/}
将目标分配给局部变量后,它具有强根,因此在使用该变量时无法收集。
以下类使用弱引用来跟踪已实例化的所有Widget对象,而不会阻止收集这些对象:
classWidgetn{nstaticList<WeakReference>_allWidgets=newList<WeakReference>();nnpublicreadonlystringName;nnpublicWidget(stringname)n{nName=name;n_allWidgets.Add(newWeakReference(this));n}nnpublicstaticvoidListAllWidgets()n{nforeach(WeakReferenceweakin_allWidgets)n{nWidgetw=(Widget)weak.Target;nif(w!=null)Console.WriteLine(w.Name);n}n}n}
这种系统的唯一附带条件是静态列表将随着时间的推移而增长,积累具有空目标的弱引用。因此,您需要实施一些清理策略。
弱引用和缓存弱引用的一个用途是缓存大型对象图。这允许短暂缓存内存密集型数据,而不会导致过多的内存消耗:
_weakCache=newWeakReference(...);//_weakCacheisafieldn...nvarcache=_weakCache.Target;nif(cache==null){/*Re-createcache&assignitto_weakCache*/}
此策略在实践中只能略微有效,因为您几乎无法控制GC何时触发以及它选择收集哪一代。特别是,如果您的缓存保留在Gen0中,则可以在几微秒内收集它(请记住,GC不会仅在内存不足时才收集-它会在正常内存条件下定期收集)。因此,至少应该使用两级缓存,从保存强引用开始,随着时间的推移,这些引用会转换为弱引用。
弱引用和事件我们之前看到了事件如何导致托管内存泄漏。最简单的解决方案是避免在这种情况下订阅,或者实现Dispose方法来取消订阅。弱引用提供了另一种解决方案。
假设一个委托只包含对其目标的弱引用。这样的代表不会让目标保持活力——除非这些目标有独立的裁判。当然,这不会阻止发射代表击中未引用的目标-在目标有资格收集和GC赶上它之间的时间内。若要使此类解决方案有效,代码在该方案中必须可靠。假设是这种情况,您可以实现弱类,如下所示:
publicclassWeakDelegate<TDelegate>whereTDelegate:classn{nclassMethodTargetn{npublicreadonlyWeakReferenceReference;npublicreadonlyMethodInfoMethod;nnpublicMethodTarget(Delegated)n{n//d.Targetwillbenullforstaticmethodtargets:nif(d.Target!=null)Reference=newWeakReference(d.Target);nMethod=d.Method;n}n}nnList<MethodTarget>_targets=newList<MethodTarget>();nnpublicWeakDelegate()n{nif(!typeof(TDelegate).IsSubclassOf(typeof(Delegate)))nthrownewInvalidOperationExceptionn("TDelegatemustbeadelegatetype");n}nnpublicvoidCombine(TDelegatetarget)n{nif(target==null)return;nnforeach(Delegatedin(targetasDelegate).GetInvocationList())n_targets.Add(newMethodTarget(d));n}nnpublicvoidRemove(TDelegatetarget)n{nif(target==null)return;nforeach(Delegatedin(targetasDelegate).GetInvocationList())n{nMethodTargetmt=_targets.Find(w=>nEquals(d.Target,w.Reference?.Target)&&nEquals(d.Method.MethodHandle,w.Method.MethodHandle));nnif(mt!=null)_targets.Remove(mt);n}n}nnpublicTDelegateTargetn{ngetn{nDelegatecombinedTarget=null;nnforeach(MethodTargetmtin_targets.ToArray())n{nWeakReferencewr=mt.Reference;nn//Statictarget||aliveinstancetargetnif(wr==null||wr.Target!=null)n{nvarnewDelegate=Delegate.CreateDelegate(ntypeof(TDelegate),wr?.Target,mt.Method);ncombinedTarget=Delegate.Combine(combinedTarget,newDelegate);n}nelsen_targets.Remove(mt);n}nnreturncombinedTargetasTDelegate;n}nsetn{n_targets.Clear();nCombine(value);n}n}n}
此代码演示了C#和CLR中的几个有趣的点。首先,请注意,我们检查TDelegate是构造函数中的委托类型。这是因为C#中的—以下类型约束是非法的,因为C#认为是不支持约束的特殊类型:
...whereTDelegate:Delegate//Compilerdoesn’tallowthis
相反,我们必须选择一个类约束并在中执行运行时检查。
在合并和删除方法中,我们通过as运算符而不是更常见的强制转换运算符执行从到Delegate的引用转换。这是因为C#不允许使用此类参数的强制转换运算符,因为转换和之间可能存在歧义。
然后,我们调用GetInvocationList,因为这些方法可能是使用多播委托(具有多个方法收件人的委托)调用的。
在Target属性中,我们构建了一个多播委托,该委托组合了目标处于活动状态的弱引用引用引用的所有委托,从列表中删除剩余的(死)引用,以防止_targets列表无休止地增长。(我们可以通过在Combine方法中执行相同的操作来改进我们的类;另一个改进是添加线程安全的锁[参见中的)。我们还允许代表完全没有弱参考;这些表示其目标是静态方法的委托。
下面演示如何在实现事件时使用此委托:
publicclassFoon{nWeakDelegate<EventHandler>_click=newWeakDelegate<EventHandler>();nnpubliceventEventHandlerClickn{nadd{_click.Combine(value);}remove{_click.Remove(value);}n}nnprotectedvirtualvoidOnClick(EventArgse)n=>_click.Target?.Invoke(this,e);n}
文章到此结束,如果本次分享的强制GC是怎么玩的10种和清理和垃圾收集的问题解决了您的问题,那么我们由衷的感到高兴!