谈一谈.net析构函数对垃圾回收的影响

这里说析构函数,其实并不准确,应该叫Finalize函数,Finalize函数形式上和c++的析构函数很像 ,都是(~ClassName)的形式,但是功能上完全不一样。析构函数编译成il语言后会变成一个Finalize的函数,他是重写的object的Finalize虚函数,标题上用析构函数,主要是我认为很多人不知道Finalize函数。
写一个类型解释下可能会更通俗易懂一点:

    public class Test
    {
        ~Test() { }  //这个就是Finalize函数
        private byte[] b = new byte[10000];
    }

最近看了一些代码,有不少用Finalize函数的。特别是ef数据仓库中,情况如下:

public class DbRepostoty
{
    private Context context;
    public DbRepostoty(Context context)
    {
        this.context = context;
    }
    ~DbRepostoty()
    {
        context.Dispose();
    }
}
public class Context : DbContext
{
}

看上去很高大上,但是这样写到底好不好呢?好不好我们最后再去评论,先看一看下面这个简单的例子:

    public class WithFinalize
    {
        ~WithFinalize() { }
        private byte[] b = new byte[10000];
    }
    public class WithoutFinalize
    {
        private byte[] b = new byte[10000];
    }
    class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("测试1无Finalize函数:");
            Test<WithoutFinalize>();
            Console.WriteLine(Environment.NewLine+ "测试2有Finalize函数:");
            Test<WithFinalize>();

            Console.ReadKey();
        }
        public static void Test<T>() where T : new()
        {
            GC.Collect();
            Thread.Sleep(10);
            Console.WriteLine("初始内存:" + GC.GetTotalMemory(false));
            var list = new List<T>();
            for (int i = 0; i < 10; i++) list.Add(new T());
            Console.WriteLine("分配之后:" + GC.GetTotalMemory(false));
            GC.Collect();
            Thread.Sleep(10);
            Console.WriteLine("一次回收:" + GC.GetTotalMemory(false));
            GC.Collect();
            Thread.Sleep(10);
            Console.WriteLine("二次回收:" + GC.GetTotalMemory(false));
        }
    }

这段代码有三个类一个是我们需要运行的主程序,另外两个 WhitFinalize 和WhitoutFinalize则是我们要测试的类型,这两个类一个加了Finalize函数,一个未加,其余的完全一样。主程序则分别要测试这两个类型在垃圾回收的时的表现,我们先测试的没有加Finalize函数的类型,在测试的加了类型。 一共四个数值,分别是初始时的内存, new了10个测试类型之后的内存(测试类型大约需要10k的内存空间,10个也就是大约100k),垃圾回收一次之后的内存,垃圾回收二次之后的内存,我们看下具体的运行情况:

测试1无Finalize函数:
初始内存:96224
分配之后:196464
一次回收:97036
二次回收:97036

测试2有Finalize函数:
初始内存:97056
分配之后:197296
一次回收:197396
二次回收:97156

从运行情况来看两次测试的初始化内存都大约97k左右,new了10个测试对象之后都增长了大约100k,和预期的一样,但是第一次垃圾回收之后测试1(没有Finalize函数)回收了100k左右的内存,而测试2(有Finalize函数)则基本上没有回收掉内存,却等到了第二次垃圾回收 回收了100k内存。不禁会想,这又是为什呢?

这得从垃圾回收的一些原理说起,东西比较多,我们说的简单一下。垃圾回收的时候会从根遍历所有引用的对象,然后遍历到了就做好标记,代表有用,没遍历到的就会是为垃圾,但是在这些垃圾中有一些对象定义了Finalize函数,于是就把这些有Finalize的对象从垃圾堆里拉了回来,其余的垃圾则回收掉,而这些死而复活的对象则和那些本来就不是垃圾对象都幸存了下来,并一并升级为下一代对象,垃圾回收结束之后 clr会用一个较高优先级的线程来调用这些死而复活对象的Finalize方法,直到下次垃圾回收他们才被回收掉。这也是我们看到测试2第二次垃圾回收才被回收掉的原因,我们在这里讲的都是一些粗略的东西,内部实现还要复杂。

我们看到我在代码里用到了很多Thread.Sleep(10); 这是什么原因呢?这就的注意下我上一段的一句话“垃圾回收结束之后 clr会用一个较高优先级的线程来调用这些死而复活对象的Finalize方法”,Finalize方法的调用和我们的前台代码是并发进行的,而且我们前台代码比较简单,如果不暂停一下的话很可能不少对象的Finalize方法还没执行完,我们就调用了下一次的垃圾回收(GC.Collect())。影响结果的准确性。

还有我们之前提到了代的概念,这里也简单说一下代,垃圾回收时对象一共有三代 :0,1,2。每一代都有自己的内存预算,空间不足的时候会调用垃圾回收。为了提高性能都是按代回收,第0代超预算之后就回收第0代的对象,而存活下来的对象就提升为第1代,依次类推,而往往经过多次0代的垃圾回收才能回收一次第1代。

我们代码中的GC.Collect();没有参数,意思是回收所有代的对象,我们可以把GC.Collect()换成GC.Collect(0);意思是回收第0代的对象,然后运行程序:

        public static void Test<T>() where T : new()
        {
            GC.Collect();
            Thread.Sleep(10);
            Console.WriteLine("初始内存:" + GC.GetTotalMemory(false));
            var list = new List<T>();
            for (int i = 0; i < 10; i++) list.Add(new T());
            Console.WriteLine("分配之后:" + GC.GetTotalMemory(false));
            GC.Collect(0);
            Thread.Sleep(10);
            Console.WriteLine("一次回收:" + GC.GetTotalMemory(false));
            GC.Collect(0);
            Thread.Sleep(10);
            Console.WriteLine("二次回收:" + GC.GetTotalMemory(false));
        }
测试1无Finalize函数:
初始内存:96224
分配之后:196464
一次回收:97056
二次回收:97036

测试2有Finalize函数:
初始内存:97056
分配之后:197296
一次回收:197396
二次回收:197396

我们看到测试2中在第二次垃圾回收之后(对第0代)内存依旧没有回收掉,而这种情况更接近于实际。

从上面的小例子中我们了解到Finalize方法对性能和内存都有不好的影响,那为什么要存在这个方法呢?这里我们说一下要使用Finalize的两个情况:

第一个情况就是对象含有一个本机资源,比如一个句柄,这样可以在Finalize方法释放这个句柄,就能消除忘记释放句柄造成的本机资源浪费。

第二种情况就是在这个对象被回收之前需要做一些必须要做的是事情,比如FileStream这个类,需要在回收之前把缓冲区的东西写入到文件内。

我们在回过头开看一看之前提到的数据仓库的类,这个类第一没有占用任何本机资源,第二在被回收之前也没有必须要做的事情,写一个Finalize方法并调用 context.Dispose(); 只能增加性能开销,影响垃圾回收效果。我们可以用反编译软件看一下DbContext这个基类,他都没有Finalize方法,又何必再画蛇添足呢?

希望觉得对自己有帮助的朋友给我点个赞(●‘?‘●)

时间: 2024-10-12 14:46:59

谈一谈.net析构函数对垃圾回收的影响的相关文章

(一)Python入门-6面向对象编程:04__del__方法(析构函数)和垃圾回收机制-__call__方法和可调用对象

一:__del__方法(析构函数)和垃圾回收机制 __del__方法称为“析构方法”,用于实现对象被销毁时所需的操作.比如:释放对象 占用的资源,例如:打开的文件资源.网络连接等. Python实现自动的垃圾回收,当对象没有被引用时(引用计数为 0),由垃圾回收器 调用__del__方法. 我们也可以通过del 语句删除对象,从而保证调用__del__方法. 系统会自动提供__del__方法,一般不需要自定义析构方法. [操作] #析构函数 class Person: def __del__(s

浅谈Chrome V8引擎中的垃圾回收机制

垃圾回收器 JavaScript的垃圾回收器 JavaScript使用垃圾回收机制来自动管理内存.垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带来的内存泄露问题.但使用了垃圾回收即意味着程序员将无法掌控内存.ECMAScript没有暴露任何垃圾回收器的接口.我们无法强迫其进 行垃圾回收,更无法干预内存管理 内存管理问题 在浏览器中,Chrome V8引擎实例的生命周期不会很长(谁没事一个页面开着几天几个月不关),而且运行在用户的机器上.如果

JAVA局部变量对垃圾回收的影响

结论 局部变量表中的变量是很重要的垃圾回收根节点,被局部变量表中变量直接活着间接引用的对象都不会被回收. 实验 看如下代码,使用JVM的-XX:+PrintGC参数运行下面代码(在main函数中分别执行localVarGcN的每一个函数) package com.winwill.jvm.basic; /** * @author qifuguang * @date 15/4/21 20:44 */ public class GcTest { private static final int SIZ

C#析构函数与垃圾回收

析构函数基本语法 C# class Car { ~ Car() // destructor { // cleanup statements... } } 析构函数说明 不能在结构中定义析构函数.只能对类使用析构函数. 一个类只能有一个析构函数. 无法继承或重载析构函数. 无法调用析构函数.它们是被自动调用的. 析构函数既没有修饰符,也没有参数. 析构函数跟构造函数相反 构造函数可控制执行 析构函数不可控制 托管资源和非托管资源 托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的

finalize()方法对垃圾回收的影响

概述 Java中提供了一个类似C++析构函数的机制: finalize()方法,该函数允许子类重载,用于在对象被回收是释放资源. 但是一般情况下,尽量不要使用finalize函数进行资源的释放,原因主要有一下几点: finalize函数调用时,有可能导致对象复活. finalize函数执行的时间没有保障,他完全由GC线程决定,正常情况下,若不发生gc,则finalize一直都没有机会被执行. 一个坏的finalize函数会严重影响gc的性能. 实验 下面通过一个实验来看看: package co

JVM基础(5)-垃圾回收机制

一.对象引用的类型 Java 中的垃圾回收一般是在 Java 堆中进行,因为堆中几乎存放了 Java 中所有的对象实例.谈到 Java 堆中的垃圾回收,自然要谈到引用.在 JDK1.2 之前,Java 中的引用定义很很纯粹:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用.但在 JDK1.2 之后,Java 对引用的概念进行了扩充,将其分为强引用(Strong Reference).软引用(Soft Reference).弱引用(Weak

降低Java垃圾回收开销的5条建议

这篇文章是转载于网上的一篇文章,看了之后感觉不错,受益良多,所以在此分享给大家. 原文: 保持GC低开销的窍门有哪些? 随着一再拖延而即将发布的 Java9,G1("Garbage First")垃圾回收器将被成为 HotSpot 虚拟机默认的垃圾回收器.从 serial 垃圾回收器 到CMS 收集器, JVM 见证了许多 GC 实现,而 G1 将成为其下一代垃圾回收器. 随着垃圾收集器的发展,每一代 GC 与其上一代相比,都带来了巨大的进步和改善.parallel GC 与 seri

管理Java垃圾回收的五个建议

[编者按]本文作者是Niv Steingarten,是Takipi 的联合创始人,热衷于编写优雅简洁的代码. 作者通过对垃圾收集器的介绍和梳理,在管理垃圾回收方面提出了五个建议.减少收集器开销.帮助大家进一步提升项目性能.本文系国内 ITOM 管理平台 OneAPM project师编译整理. 保持GC低开销最有用的建议是什么? 早有消息声称Java 9即将公布,但现在却一再推迟.当中比較值得关注的是G1("Garbage-First")垃圾收集器将成为HotSpot JVM的默认收集

Unity优化方向——优化Unity游戏中的垃圾回收

介绍 当我们的游戏运行时,它使用内存来存储数据.当不再需要该数据时,存储该数据的内存将被释放,以便可以重用.垃圾是用来存储数据但不再使用的内存的术语.垃圾回收是该内存再次可用以进行重用的进程的名称. Unity使用垃圾回收作为管理内存的一部分.如果垃圾回收发生得太频繁或者有太多工作要做,我们的游戏可能会表现不佳,这意味着垃圾回收是导致性能问题的常见原因. 在本文中,我们将了解垃圾回收如何工作的,什么时候发生垃圾回收,以及如何有效地使用内存,从而最小化垃圾回收对游戏的影响. 诊断垃圾回收的问题 垃