【.NET深呼吸】清理对象引用,有一个问题容易被忽略

大家知道,托管代码一个重要的特点是自动管理内存,即我们常说的垃圾回收机制,那些高大上的理论我就不重复了,有兴趣的朋友可以翻书。我这个有个毛病——不喜欢很严肃地去说一些理论的东西,所以我不多介绍了。

一般而言,当代码执行超出某个变量的有效范围后,或者不再引用某个对象实例时,该实例会发生析构,垃圾回收器很可能就要清理门户了,当然也可能不是马上清理,也许会过一会儿再清理。

对于一些要自定义进行清理操作的类,我们会采取以下方案:

1、写上析构函数,在析构函数中清理。

2、实现IDisposable接口,并实现Dispose方法,在方法中编写自定义清理代码。当该类型被实例化后,最后不再使用时会调用Dispose方法清理,如果顺利清理,最后还会调用类型的析构函数。通常,如何实现了IDisposable接口,就不必再写上析构函数了。如果希望Dispose方法被自动调用,可以在实例化对象的代码包装在using语句块中,当执行完using块时会自动调用Dispose方法。

可能有人笑了,老周,你太逗了,这些基础知识谁不知道?当然,我说上面那些内容是为了绕个小圈子,以便进入主题。于是,我产生了一个疑问:是不是存在某些情景下,可能导致对象实例不会被回收呢?就算你调用了Dispose方法,就算你把变量设为null来解除引用,就算你调用GC类的方法来回收……

经过老周测试,还真有这种情况,而且很多朋友都很有可能会忽略,甚至在意识认知上误认为对象实例已经被回收,而实际上是没有回收的。

我简单说一下这种情形:

比如有一个静态类(静态类的成员必是静态的)A,里面有静态事件。随后在其他类的实例中处理A类的静态事件,并且处理事件的方法就位于这个实例对象上……

不急,我们还是看真实的例子吧。假如我定义了一个静态类MyChecker,它里面有个静态事件CheckEvent。

    public static class MyChecker
    {
        #region 静态事件
        public static event EventHandler CheckEvent;
        #endregion

        public static void CallEvent()
        {
            if (CheckEvent != null)
            {
                CheckEvent(new object(), EventArgs.Empty);
            }
        }
    }

只要CallEvent方法被调用,CheckEvent事件会被引发。

然后,定义另一个类SampleClass,并在该类中处理刚才MyChecker中的静态事件。

    class SampleClass:IDisposable
    {
        public SampleClass()
        {
            MyChecker.CheckEvent += MyChecker_CheckEvent;
        }

        void MyChecker_CheckEvent(object sender, EventArgs e)
        {
            new Form2().Show();
        }

        ~SampleClass()
        {
            System.Diagnostics.Debug.WriteLine("\n看,析构函数调用了。\n");
        }

        public void Dispose()
        {
            //……
        }
    }

在类的构造函数中,附加CheckEvent事件的处理,处理方法名为MyChecker_CheckEvent。

可能大家已经发现,老周写的SampleClass类有点恐怖气息,既实现了Dispose方法,怎么又写了析构函数,我这里写上析构函数是为了验证类的实例是否真的被清理,如果实例真的被回收,那么Debug类会在“输出”窗口中输出提示,如果没有提示输出,说明类的实例还霸占着内存。

接下来测试一下。

            SampleClass sc = new SampleClass();
            await Task.Delay(10 * 1000);
            sc.Dispose();
            sc = null;
            GC.Collect();

实例化SampleClass后,然后Delay会暂停10秒,10秒钟过后会调用Dispose方法,并设置变量为null引用,我害怕不能及时清理,连GC.Collect方法也用上了。

而在等待这10秒期间,可以调用静态类的CallEvent方法来引发静态事件CheckEvent。

MyChecker.CallEvent();

按照一般理解,在10秒钟后,SampleClass实例应该被清理,并且在“输出”窗口会输出提示。

好,现在试一下。

……

实验结果表明,输出 窗口中连鸭毛都没有输出,这说明10秒钟后,SampleClass实例根本没有发生析构。于是又出问题了,这是怎么回事?SampleClass实例不是不存在引用了吗,怎么不发生析构?

其实我们忽略了一点:静态事件CheckEvent还跟SampleClass实例的方法绑定着呢,实质上,虽然将变量设为null,可是SampleClass实例中的MyChecker_CheckEvent方法还被静态类中的静态事件引用着,所以不会被回收。不知道你明白了没。

这个问题很多朋友在实际开发中都会忽略,还得意地以为Sample实例真的被回收了,实际上实例不会被回收,除非程序结束。因为MyChecker是静态类,不基于实例。如果MyChecker不是静态类,那么当MyChecker的实例释放后,SampleClass实例就可以被释放了。

那么,如何解决呢?很简单,只要在SampleClass类的Dispose方法中解除静态事件与方法的绑定即可,这样的话,静态事件就不再引用实例中的方法成员了,此时实例就可以发生析构了。

        public void Dispose()
        {
            MyChecker.CheckEvent -= MyChecker_CheckEvent;
        }

这个例子研究,告诉我们:在类实例中处理静态事件时一定要小心

本示例的源码下载:http://files.cnblogs.com/files/tcjiaan/refsample.zip

好了,今天就扯到这里吧。

时间: 2024-08-07 04:30:21

【.NET深呼吸】清理对象引用,有一个问题容易被忽略的相关文章

复制一个空洞文件且忽略掉其空洞内容

首先说一下什么叫做空洞文件!比如说,下面这段代码: 1 #include<stdio.h> 2 #include<string.h> 3 #include<stdlib.h> 4 #include<errno.h> 5 #include<sys/types.h> 6 #include<sys/stat.h> 7 #include<fcntl.h> 8 #include<unistd.h> 9 10 #defin

80锁屏清理进程(重点)

锁屏清理进程是一个比较有用的功能,可以为用户节省很多的电量,锁屏清理进程也比定时清理进程优点多.说下思路: 判断用户是否锁屏需要注册一个广播接收者去监听,当然这个广播接收者放在服务里面比较好,试想如果放在Activity里面,当Activity执行onDestory()的时候,广播接收者就没了.而且这个广播接受者必须是代码注册. package com.ustc.mobilemanager.service; import java.util.List; import android.app.Ac

Mac垃圾清理工具使用注意点

Mac上手动清理很难实现,因此很多用户都在使用Mac垃圾清理工具cleanmymac,为了让广大用户更好的清理mac,今天小编和大家聊聊使用cleanmymac中的注意点. 1.CleanMyMac是一款mac清理软件,它杀不了病毒.这里我们所说的是这款软件的主要功能,并不是说Mac OS X 就没有病毒,只是一款软件专注于一个领域或者说方向,才能做得更好,而且真正意义上的病毒是不会那么容易就被清除了的,所以与其浪费精力在开发一个极难维护的功能上,还不如优化其它的功能,真正做到接近完美. 2.接

清理代码的阅读笔记

深度解析:清理烂代码 2015-10-05 PHP开发者 (点击上方公众号,可快速关注) 英文:Niklas Frykholm 作者:伯乐在线 - 唐小娟 网址:http://blog.jobbole.com/28672/ 猜猜看怎么了!你正”继承“(接收)了一堆混乱的旧代码.恭喜你!现在都是你的了.混乱的代码可能来自任何地方.中间件,网络,可能来自你自己的公司. 你知道在一个角落里有一个家伙,没有人过去管他在做什么.猜猜看他一直在做什么?辛辛苦苦写出了代码,却是一堆烂代码. 你还记得这个模块是

深度解析:清理烂代码(转)

本文由 伯乐在线 - 唐小娟 翻译.未经许可,禁止转载!英文出处:Niklas Frykholm.欢迎加入翻译小组. 猜猜看怎么了!你正”继承“(接收)了一堆混乱的旧代码.恭喜你!现在都是你的了.混乱的代码可能来自任何地方.中间件,网络,可能来自你自己的公司. 你知道在一个角落里有一个家伙,没有人过去管他在做什么.猜猜看他一直在做什么?辛辛苦苦写出了代码,却是一堆烂代码. 你还记得这个模块是一个家伙几年前写的,在他离开公司之前.这个模块已经有20个不同的人加过补丁,进行过代码修复,而且他们也并不

Mac OS X Mavericks如何清理安装

在Mac上,OS X Mavericks操作系统可能还是不少人的选择.如果你想在mac上重新安装一个全新的OS X操作系统,那么最好就是要将mac完全清理干净没有任何垃圾. 图一:OS X操作系统 在安装OS X Mavericks操作系统前,我们还需要做两件事,那就是清理mac垃圾,然后备份新的mac,最后就可以重新安装了. 步骤1:清理mac 清理mac可以节省大量备份的时间,而且清理mac方法也是非常简单的,使用cleanmymac3简单的步骤就可以实现mac的完全清理,并且只要花费可能1

深度解析:清理烂代码

摘要: 烂代码不一定是问题,只要它们没有出错,没有人会对它嗤之以鼻.但不幸的是,它们没被发现的概率太小了.错误会被发现.需要新的功能,新系统发布了.现在你不得不面对这堆恐怖的代码,试着去清理它们. 猜猜看怎么了!你正”继承“(接收)了一堆混乱的旧代码.恭喜你!现在都是你的了.混乱的代码可能来自任何地方.中间件,网络,可能来自你自己的公司. 你知道在一个角落里有一个家伙,没有人过去管他在做什么.猜猜看他一直在做什么?辛辛苦苦写出了代码,却是一堆烂代码. 你还记得这个模块是一个家伙几年前写的,在他离

如何清理Docker占用的磁盘空间?

摘要:用了Docker,好处挺多的,但是有一个不大不小的问题,它会一不小心占用太多磁盘,这就意味着我们必须及时清理. 作为一个有信仰的技术公司,我们Fundebug的后台采用了酷炫的全Docker化架构,所有服务,包括数据库都运行在Docker里面.这样做当然不是为了炫技,看得清楚的好处还是不少的: 所有服务器的配置都非常简单,只安装了Docker,这样新增服务器的时候要简单很多. 可以非常方便地在服务器之间移动各种服务,下载Docker镜像就可以运行,不需要手动配置运行环境. 开发/测试环境与

linux shell 脚本 历史文件清理脚本,按天,按月,清理前N天的历史文件,删除指定大小历史文件,历史文件归档清理

不知道大家那有没有要清理的这个事情.需要清理目录历史文件.可能后续也会有很多其他地方需要清理历史文件,可能会用到. 我这两天空闲写了个脚本,清理比较方便,有要进行清理的大量历史文件的话可以用. 脚本用到的命令只有linux才有,像solaris等就不支持,所以只能在linux上运行.如果是nas存储的话,可以挂载到一个linux主机上跑脚本清理. 另外,脚本查找文件用的是ls,但是ls也有最大文件的限度(大概10w以内),如果每天文件很多,每天都是10w+的,运行可能提示文件数过多无法ls. 还