浅谈C#托管程序中的资源释放问题

C# 释放内存空间的方法 当不再需要对象时释放内存空间对于防止内存泄漏和提高应用程序性能至关重要。C# 提供了多种方法来释放内存空间。 阅读详情

终于开始动手写这篇文章了,有个网友催了我好几次,今天终于可以静下心来完成它。

 

便于对文章的开展,需要先明确两个概念。

第一个就是很多人用.Net写程序,会谈到托管这个概念。那么.Net所指的资源托管到底是什么意思,是相对于所有资源,还是只限于某一方面资源?很多人对此不是很了解,其实.Net所指的托管只是针对内存这一个方面,并不是对于所有的资源;因此对于Stream,数据库的连接,GDI+的相关对象,还有Com对象等等,这些资源并不是受到.Net管理而统称为非托管资源。而对于内存的释放和回收,系统提供了GC-Garbage Collector,而至于其他资源则需要手动进行释放。

 

那么第二个概念就是什么是垃圾,通过我以前的文章,会了解到.Net类型分为两大类,一个就是值类型,另一个就是引用类型。前者是分配在栈上,并不需要GC回收;后者是分配在堆上,因此它的内存释放和回收需要通过GC来完成。GC的全称为“Garbage Collector”,顾名思义就是垃圾回收器,那么只有被称为垃圾的对象才能被GC回收。也就是说,一个引用类型对象所占用的内存需要被GC回收,需要先成为垃圾。那么.Net如何判定一个引用类型对象是垃圾呢,.Net的判断很简单,只要判定此对象或者其包含的子对象没有任何引用是有效的,那么系统就认为它是垃圾。

 

明确了这两个基本概念,接下来说说GC的运作方式以及其的功能。内存的释放和回收需要伴随着程序的运行,因此系统为GC安排了独立的线程。那么GC的工作大致是,查询内存中对象是否成为垃圾,然后对垃圾进行释放和回收。那么对于GC对于内存回收采取了一定的优先算法进行轮循回收内存资源。其次,对于内存中的垃圾分为两种,一种是需要调用对象的析构函数,另一种是不需要调用的。GC对于前者的回收需要通过两步完成,第一步是调用对象的析构函数,第二步是回收内存,但是要注意这两步不是在GC一次轮循完成,即需要两次轮循;相对于后者,则只是回收内存而已。

 

很明显得知,对于某个具体的资源,无法确切知道,对象析构函数什么时候被调用,以及GC什么时候会去释放和回收它所占用的内存。那么对于从C、C++之类语言转换过来的程序员来说,这里需要转变观念。

 

那么对于程序资源来说,我们应该做些什么,以及如何去做,才能使程序效率最高,同时占用资源能尽快的释放。前面也说了,资源分为两种,托管的内存资源,这是不需要我们操心的,系统已经为我们进行管理了;那么对于非托管的资源,这里再重申一下,就是Stream,数据库的连接,GDI+的相关对象,还有Com对象等等这些资源,需要我们手动去释放。

 

如何去释放,应该把这些操作放到哪里比较好呢。.Net提供了三种方法,也是最常见的三种,大致如下:

1.  析构函数;

2.  继承IDisposable接口,实现Dispose方法;

3.  提供Close方法。

 

经过前面的介绍,可以知道析构函数只能被GC来调用的,那么无法确定它什么时候被调用,因此用它作为资源的释放并不是很合理,因为资源释放不及时;但是为了防止资源泄漏,毕竟它会被GC调用,因此析构函数可以作为一个补救方法。而Close与Dispose这两种方法的区别在于,调用完了对象的Close方法后,此对象有可能被重新进行使用;而Dispose方法来说,此对象所占有的资源需要被标记为无用了,也就是此对象被销毁了,不能再被使用。例如,常见SqlConnection这个类,当调用完Close方法后,可以通过Open重新打开数据库连接,当彻底不用这个对象了就可以调用Dispose方法来标记此对象无用,等待GC回收。明白了这两种方法的意思后,大家在往自己的类中添加的接口时候,不要歪曲了这两者意思。

 

接下来说说这三个函数的调用时机,我用几个试验结果来进行说明,可能会使大家的印象更深。

首先是这三种方法的实现,大致如下:

    /// <summary>

    /// The class to show three disposal function

    /// </summary>

    public class DisposeClass:IDisposable

    {

        public void Close()

        {

            Debug.WriteLine( "Close called!" );

        }

 

        ~DisposeClass()

        {

            Debug.WriteLine( "Destructor called!" );

        }

 

        #region IDisposable Members

 

        public void Dispose()

        {

            // TODO:  Add DisposeClass.Dispose implementation

            Debug.WriteLine( "Dispose called!" );

        }

 

        #endregion

    }

 

对于Close来说不属于真正意义上的释放,除了注意它需要显示被调用外,我在此对它不多说了。而对于析构函数而言,不是在对象离开作用域后立刻被执行,只有在关闭进程或者调用GC.Collect方法的时候才被调用,参看如下的代码运行结果。

        private void Create()

        {

            DisposeClass myClass = new DisposeClass();

        }

 

        private void CallGC()

        {

            GC.Collect();

        }

 

        // Show destructor

        Create();

        Debug.WriteLine( "After created!" );

        CallGC();

 

运行的结果为:

After created!

Destructor called!

 

显然在出了Create函数外,myClass对象的析构函数没有被立刻调用,而是等显示调用GC.Collect才被调用。

 

对于Dispose来说,也需要显示的调用,但是对于继承了IDisposable的类型对象可以使用using这个关键字,这样对象的Dispose方法在出了using范围后会被自动调用。例如:

    using( DisposeClass myClass = new DisposeClass() )

    {

        //other operation here

    }

 

如上运行的结果如下:

Dispose called!

 

那么对于如上DisposeClass类型的Dispose实现来说,事实上GC还需要调用对象的析构函数,按照前面的GC流程来说,GC对于需要调用析构函数的对象来说,至少经过两个步骤,即首先调用对象的析构函数,其次回收内存。也就是说,按照上面所写的Dispose函数,虽说被执行了,但是GC还是需要执行析构函数,那么一个完整的Dispose函数,应该通过调用GC.SuppressFinalize(this )来告诉GC,让它不用再调用对象的析构函数中。那么改写后的DisposeClass如下:

    /// <summary>

    /// The class to show three disposal function

    /// </summary>

    public class DisposeClass:IDisposable

    {

        public void Close()

        {

            Debug.WriteLine( "Close called!" );

        }

 

        ~DisposeClass()

        {

            Debug.WriteLine( "Destructor called!" );

        }

 

        #region IDisposable Members

 

        public void Dispose()

        {

            // TODO:  Add DisposeClass.Dispose implementation

            Debug.WriteLine( "Dispose called!" );

            GC.SuppressFinalize( this );

        }

 

        #endregion

    }

 

通过如下的代码进行测试。

        private void Run()

        {

            using( DisposeClass myClass = new DisposeClass() )

            {

                //other operation here

            }

        }

 

        private void CallGC()

        {

            GC.Collect();

        }

 

        // Show destructor

        Run();

        Debug.WriteLine( "After Run!" );

        CallGC();

 

运行的结果如下:

Dispose called!

After Run!

 

显然对象的析构函数没有被调用。通过如上的实验以及文字说明,大家会得到如下的一个对比表格。

 

析构函数

Dispose方法

Close方法

意义

销毁对象

销毁对象

关闭对象资源

调用方式

不能被显示调用,会被GC调用

需要显示调用

或者通过using语句

需要显示调用

调用时机

不确定

确定,在显示调用或者离开using程序块

确定,在显示调用时

 

那么在定义一个类型的时候,是否一定要给出这三个函数地实现呢。

 

我的建议大致如下。

1. 提供析构函数,避免资源未被释放,主要是指非内存资源;

2. 对于Dispose和Close方法来说,需要看所定义的类型所使用的资源(参看前面所说),而决定是否去定义这两个函数;

3. 在实现Dispose方法的时候,一定要加上“GC.SuppressFinalize( this )”语句,避免再让GC调用对象的析构函数。

 

C#程序所使用的内存是受托管的,但不意味着滥用,好地编程习惯有利于提高代码的质量以及程序的运行效率。

C#内存管理-栈堆/回收器托管/非托管资源释放/指针的应用 1.栈内存-从上往下增长,释放时从下部的末尾出栈 .net 5中栈好像也是从下往上增长了,释放时从上部顶端出栈。 栈类型数据,整体是从进程空间中的栈内存资源的最大地址处开始分配栈内存的,栈指针总是指向已用地址更小的内存地址上,下一个为空闲内存地址。当变量出了作用域时候,栈内存就会释放,栈指针上移到非空闲地址的小地址上,若再进来变量就会覆盖之前不用了的地址内存区域。      栈内存分配是对整 阅读详情

相关推荐

C# 快速释放内存的大数组

本文告诉大家如何使用 Marshal 做出可以快速释放内存的大数组。 最近在做 3D ,需要不断申请一段大内存数组,然后就释放他,但是 C# 对于大内存不是立刻释放,所以就存在一定的性能问题。 在博客园看到了一位大神使用 Marshal 做出快速申请的大数组,于是我就学他的方法来弄一个。本文告诉大家这个类是如何使用

lindexi 1万+

C# 内存管理

C# 内存管理,预防和清理内存

lvxingzhe3的博客 5364

文档多模态识别工具对比:MinerU、PaddleOCR、Marker

本文对比了三款文档多模态识别工具:MinerU、PaddleOCR和Marker。MinerU专注于中文文档的端到端解析,支持复杂元素提取和结构化输出,适合中文论文和教材解析。PaddleOCR是通用OCR工具包,支持多语言和全流程任务,适用于发票、车牌等图像OCR。Marker则注重多语言支持和轻量化部署,适合英文论文和技术文档的快速转换。三者在核心技术、中文优化、表格处理、输出格式、部署复杂度和适用语言等方面各有优劣。选型建议根据具体需求:MinerU适合高精度中文解析,PaddleOCR适合定制化OC

朴拙科技的博客 4743

【C#】内存的使用和释放

【C#】内存的使用和释放

wangnaisheng的专栏 2300

晶圆测试(wafer sort)全流程解析:从探针卡到良率统计的实战指南

本文详细解析了晶圆测试(wafer sort)的全流程,从探针卡选型到良率统计的实战指南。涵盖了12英寸先进制程产线的关键技术,包括探针卡系统、测试程序开发、产线自动化集成及数据分析,特别聚焦多站点并行测试的时序优化和接触阻抗异常排查,帮助工程师提升测试效率与准确性。

weixin_29279153的博客 731

BBR算法原理与实现深度解析

BBR算法通过重新定义拥塞控制的基本范式,为现代网络应用提供了突破性的解决方案。随着QUIC协议的普及和智能网卡技术的发展,BBR类算法将在边缘计算、云游戏、工业物联网等领域发挥更重要的作用。

程序猿老樊的博客 2417

浅谈C#托管程序中的资源释放问题 来源:愚翁

终于开始动手写这篇文章了,有个网友催了我好几次,而我要么因为手头有事,要么就是被其他思路给叉开,以至这篇文章拖了好久还没开始写,今天终于可以静下心来完成它。 用了.Net工具来写程序的人,不难发现它有个好处,就是使用的内存不用释放,尤其在使用C#或者VB.Net来写程序,因为程序所占用的内存都是受系统托管的,因此内存的释放不需要程序员去操心。 很多人从C语言或者C++等等语言转型过

之语的技术资料专栏 1020

中移物联ML307C模组OPENCPU笔记2

本文介绍了如何编译和运行一个DEMO程序,以官方SDK为例。

weixin_43700831的博客 1883
上一篇: 多线程断点续传研究之一
下一篇: 《Effective C#》Item 8:确保“0”在值类型中是有效的
Knight94
博客等级 码龄25年 1013粉丝 87原创
评论 27
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值