当前位置: 首页 > 科技观察

Dotnet垃圾回收解析

时间:2023-03-14 00:13:30 科技观察

本文转载自微信公众号《老王加》,作者老王加。转载本文请联系老王Plus公众号。在说垃圾回收之前,先说两个概念:托管代码是CLR管理的代码非托管代码是操作系统直接执行的代码在C++早期,内存的分配和释放都是我们手动处理的,并且在通用语言CLR中,多了一个垃圾收集器GC作为自动内存管理器来完成同样的工作。从此以后,对于开发者来说,我们不需要使用显式的代码来进行内存管理了。这样做的好处是显而易见的:消除了大量与内存相关的错误,比如没有释放对象导致的内存泄漏,或者试图访问已经释放的对象的内存等。为了防止在没有提供的情况下重印原文网址,这里是原文链接:https://abc.com1.托管资源的回收和管理上面提到垃圾回收GC是Dotnet中的一个自动内存管理器。一种用于清理和回收堆内存未引用部分的机制。通常CLR会在这些情况下启动垃圾回收:当需要为堆上的新对象分配内存,但没有足够的可用内存时;当对象被强制Dispose时;托管堆上分配的对象内存超过阈值(这个阈值会动态调整);调用GC.Collect方法。这些内容是基础。很好理解,面试的时候有话要说。看不懂也没关系,不影响做出好的程序。如果能记住下面的内容,对程序开发会有很大的帮助。在Dotnet的垃圾回收机制中,回收器会自我优化,适用于各种场景。不过,我们还是可以根据运行环境来设置垃圾回收的类型。Dotnet的CLR提供了以下两种垃圾回收:WorkstationgarbagecollectionServergarbagecollection这两种回收机制有一定的区别。工作站回收主要是为客户端应用设计的,也是程序默认的回收机制。垃圾回收进程运行在触发垃圾回收的用户线程上,并使用相同的优先级。这样,优点是不会被挂起或延迟,缺点是需要和其他线程竞争CPU时间。当运行环境只有一个CPU时,系统会自动采用工作站模式,不管你怎么设置。服务器回收针对的是高吞吐量的服务器应用。回收进程运行在专用的高优先级线程上,默认多线程运行,效率更高。缺点是会占用较多的资源,并且因为线程的干扰和它们之间的上下文切换会影响整体的性能。因此,选择什么样的回收机制需要仔细分析。通常对于常见的应用程序,工作站回收就可以了。如果是服务端API服务,需要选择服务端回收。而如果需要在服务器端启动多个实例进行处理,比如在总线上保存数据,最好回收工作站。设置垃圾回收方式,开发时可以在xxx.csproj文件中添加:mode,当然去掉这一行,默认也是工作站模式。对于生产环境已经上线的应用,也可以修改回收方式。在程序目录下找到xxx.runtimeconfig.json文件,在里面添加:"configProperties":{"System.GC.Server":true}这两个配置的关系是:如果在开发的时候在.csproj中添加了ServerGarbageCollection,即在发布时会自动将System.GC.Server添加到.runtimeconfig.json中。2.非托管资源的回收与管理上面提到的回收机制是针对托管资源的。对于非托管资源,GC不会主动回收。要回收非托管资源,只能手动编写代码,显式释放。一般来说,程序中使用的操作系统、网络或数据库连接等资源文件都是非托管资源,需要手动清理。清理非托管资源有两种方式:使用终结器Finalize,通过GC回收手动处理Dispose2.1。使用finalizerFinalizeFinalize是System.Object的一个虚方法,这个方法是在GC回收对象的内存之前通过垃圾返还调用的。我们可以覆盖这个方法来释放非托管资源。多说几句:看来MS对这部分有点犹豫,所以这里的规则一直介于两者之间。C#对析构函数支持并不严格。System.Object支持重写Object.Finalize方法,但是它创建的类不支持。重写会报错,但只能通过重写析构函数来实现,编译器将代码包装在try块Function的析构函数中或者重写Finalize,通过finally调用Object.Finalize来实现。使用终止符的缺点也很明显。当GC检测到需要回收对象时,它会在一段不确定的时间后调用终结器。这种不确定性非常烦人,我们很难预测对象何时会真正被释放。Finalize虽然看起来是手动清除非托管资源,但实际上是由垃圾收集器完成的。它最大的作用是确保非托管资源必须被释放。2.2手动处理手动处理Dispose最重要的原因是需要的时候立即释放,而不是让垃圾收集器无限期延迟后释放。手动释放,主要工作是提供一个IDisposable.Dispose的实现,实现非托管资源的确定性释放。这样当你需要释放的时候,调用Dispose方法,非托管资源就会立即释放。手动处理很容易实现。框架提供了一个接口System.IDisposable:publicinterfaceIDisposable{voidDispose();}他只包含一个方法Dispose。使用时需要实现该方法,以便在使用后及时释放非托管资源。同时Dispose方法也提供了GC.SuppressFinalize方法告诉GC该对象已经被手动处理,不再需要调用finalizer。publicvoidDispose(){GC.SuppressFinalize(this);}这样可以提前回收对象的内存。在某些情况下,可能无法调用IDisposable.Dispose方法释放非托管资源,但场景确实需要确定性释放。这时候可能通过重写Object.Finalize来实现:publicclassMyClass{~MyClass(){//TODO:Releaseunmanagedresources}}有点奇怪是不是?其实这就是我上面说的MS犹豫的地方。如果直接重写Object.Finalize,像这样:publicclassMyClass{protectedoverridevoidFinalize(){//TODO:releaseunmanagedresources}}会报错Donotoverrideobject.Finalize.Instead,provideadestructor.,他的正确做法写它是析构函数。上面提到的内容,做成例程模板,会是这样的:}//TODO:释放非托管资源(非托管对象)并替换终结器//TODO:将大字段设置为nulldisposedValue=true;}}~MyClass(){Dispose(disposing:false);}publicvoidDispose(){Dispose(disposing:true);GC.SuppressFinalize(this);}}看到这个建议保存上面的例程模板.这是最完整的版本,网上能找到的大部分都是简化版。事实上,我们经常使用的许多类都实现了IDisposable接口。例如,任何可以使用using调用的类都实现了IDisposable接口。另外还有一些类把Dispose改成了别的名字,比如IO中的Close方法,就是一个Dispose。另外,如果对象实现了IDisposable接口,而我们直接new了这个对象,那么在使用结束之后,我们需要对这个对象进行Dispose。因为设计者既然选择了Dispose,那么最后调用Dispose是正确的。3.总结最后做一个简单的总结。垃圾收集模式选择:如果应用程序可分配的资源少或可竞争的资源少,则使用工作站模式,否则,使用服务器模式。在回收方面,托管资源丢给GC自动处理,非托管资源需要人工处理:其中:Finalize是标记非托管资源可以回收,然后由GC进行回收工作。直接调用Dispose并立即回收。