关于C#的垃圾回收机制,Finalize和Dispose的区别(自认为很清晰了,有疑问的评论)
来到个新地方,新学习C#,前面看到C#的垃圾回收,Finalize和Dispose时,总是一知半解,迷迷糊糊。这次好了,前面连续两次面试问到这个问题,脑子里不是很清晰,加上用英文来表达,更是雪上加霜的感觉。
回来,好好看了相关资料,从网上看,总没有人能说的很清晰,往往很深奥的样子,拿了本<C# language 7.0>,这样中英文结合看,总算清晰了。
现在主要是找工作,没有时间写详细,先就说个重点:
1. 垃圾回收器GC负责托管对象的回收。GC通过从应用根对象开始(如全局变量,静态变量等)去访问对象到来判断对象是否能回收,如果无法到达对象,则对像可以被回收。
而不是通过引用计数法来判断对象是否需要被回收,因为引用计数无法解决一个循环引用的两个对象的回收,他们的引用计数都为1,但实际这两个对象是整个应用程序中的孤岛对象,
从应用程序根对象开始追踪是无法访问到的,应该被回收。(后续详细解释)
2. 垃圾回收很耗性能,一般只在分配新对象发现空间不够用时才触发回收。也就是说,何时回收对用户来说是不可控,未知的。
具体回收时采用的算法是分步回收,即分代回收的方法。其基于统计原理,新产生的对象总是有最大的可能性不被使用而需要回收,比如新调用函数中的新分配的局部对象;而经过几次回收周期中存活下来的对象,则更不可能被回收掉,如全局变量等。
GC初始化默认每个对象都是需要回收的,回收触发时,通过检测从根对象开始是否能访问到来判断对象是否真的可以回收。GC将对象标签为3代,分别是0,1,和2代对象。0代是还没有经过一次回收检测的对象(没检测过,默认都是可以回收的),1代表示经过一次回收检测后存活下来(即不能回收)的对象,而2代表示经过2次以上检测仍存活的对象。
GC检测依照顺序,分别检测0代,1代和2代。但是如果检测到0代,回收部分对象后,发现空间已经足够新对象使用,回收检测立刻停止。如果仍然不够,才会继续检测1代对象,直至2代对象。通过这样的方法,避免垃圾回收机制产生作用时的性能消耗。大部分情况下,0代检测就能释放足够空间应付新对象的分配,这样后续的垃圾回收过程就可以避免。
从回收过程也可以看出,一次回收过程中,哪个对象能被回收,也是不可控,未知的。
3.垃圾回收器对堆托管对象可以回收,但对非托管对象就需要使用用户手动回收。非托管对象包括文件句柄,socket,数据库连接等。这类对象除了本身是托管外,其内部还包含对操作系统资源申请的对象,这类对象需要使用者主动释放。如文件打开后,需要关闭。
非托管对象的释放,C#提供了两种方式。一种是使用Finalize(),一种是使用Dispose().
3.1 Finalize接口是Object的虚保护函数,C#所有的对象都继承自Object,所以都天然可以重写该接口。但C#编译器在此处做了限制,外部用户只能改写析构函数,编译器编译析构函数时,自动会加上Finalize()的调用。因此加上析构函数,就等于重写了Finalize接口。析构函数是垃圾回收器负责调用的,那就意味着Finalize的调用,也是GC负责调用。考虑到上面GC回收对象的机制的特点,我们可以得出Finalize是不确定何时会调用的,虽然肯定的是,其最后始终会被调用。
Finalize被GC调用的具体过程是:新对象产生时,CLR会监测到Finalize有重写,会将对象建立一个指针引用放入一个Finalization Queue;GC发生作用时,如果该对象被检测出可以回收,则会先将其从Finalization Queue的引用队列移到另外一个叫freachable的引用队列,等待下个回收周期时,在对象空间被释放前,该队列的里对象的Finalize接口被保证调用。因此Finalize比较耗时,至少要两个回收周期才能回收。
Dispose()来自于接口IDispose,所以只要继承IDispose接口,我们就可以重写Dispose,在里面来释放非托管对象。与Finalize不同的是,Dispose接口的调用,需要使用者自己确定何时调用,因此该调用是明确见效的。使用具有Dispose接口的对象时,一般是采用catch,finally 的形式,在finally调用dispose,这样确保不管出现什么情况dispose可以被调用到。C#利用using关键字简化了该种调用方式。(using db = new DbMyContext()){ ... }这样离开这个using语句块后,CLI会自动调用using新生成对象的Dispose接口。
3.2 这两者方式各有优缺点。Finalize可以由GC确保最终会调用,非托管对象可以被释放,用户可以不用操心,但又不确定何时会释放。Dispose可以明确立刻释放,但又是不可靠的,因为使用者(程序员自己)可能会忘记显式的调用Dispose接口。
基于此,最好的办法是利用两者的优点,一起使用。就是说在这两个地方都来释放,确保非托管资源一定释放。因为Fialize的调用是垃圾回收器处理,消耗性能和时间。一旦确定已经调用了Dispose以后,后续的Finalize是可以不用调用的,GC.SuppressFinalize(this)可以完成该功能来,停止Finalize的使用。
基于此,微软给出了一种推荐的写法:
public class MyRefObj : IDispose{
private bool disposed = false; // used to identify whether the Dispose() has been called
//disposing is used to indicate whethet to explicitly call the managed objects' dispose that you expect to manage intentionally
void CleanUp(bool disposing){
if (!this.disposed)
{
if (disposing){
... // Clean up the managed resource explicitly, like call the managed objects' Dispose
}
... // Clean up the unmanaged resource
}
disposed = true;
}
public void Dispose()
{
CleanUp(true);
// Now suppress finalization.
GC.SuppressFinalize(this);
}
~ MyRefObj()
{
CleanUp(false);
}
}
说明,析构函数中的CleanUP参数不能为true,因为CleanUp里面会对一些托管资源调用Dispose接口,而托管资源何时被回收是不确定的,因此这些调用的行为是不确定的,所以不能调用。
基本要点都说完了,后面在补充细节说明。
1. C#中的Object,Class和reference的关系
Object是Class的一个实例,而Reference是一个指针。学过C/C++的很好理解,实际其内容是一个内存地址,该内存地址里存放的是实际的对象。
C#中所有的类的实例化都是通过引用实现,实际是使用new 来实例化一个类。如下举例说明:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** GC Basics *****");
// Create a new Car object on the managed heap. We are returned a reference to this object ("refToMyCar").
Car refToMyCar = new Car("Zippy", );
// The C# dot operator (.) is used to invoke members on the object using our reference variable.
Console.WriteLine(refToMyCar.ToString());
Console.ReadLine();
}
}
其中变量refToMyCar实际就是一个引用,一个指针,指向实际的实例对象Car("Zippy", 50)
在C#中,引用refToMyCar和实例对象Car("Zippy", 50)是存放于不同的内存块中,分别叫做堆栈区和托管内存堆区;

程序的堆栈区是一个临时数据区,存放函数调用的对象,局部变量等,函数调用完存放于堆栈区的对象的生命期就结束了,每个线程都有固定大小的堆栈。
而堆是一个全局内存区,所有分配在Managed Heap的对象,需要管理何时释放。在C/C++语言中,这部分数据需要显示的delete和new配套使用,而在C#中,会被.NET CLR来管理,就是说你在托管堆中只管生成新的实例,而不用管该实例占用空间的释放,这由CLR自动管理,实际就是垃圾回收器干的事情。
2. 垃圾回收器(GC)
GC是怎样知道一个对象不在使用了呢,这涉及到后面描述的具体算法,简而言之就是GC发现,无法从代码根应用到达的对象。可以以一个不太严谨的解释来理解,就是该分配在堆区的
对象,已经没有外部引用了。如下一段代码:
static void MakeACar()
{
// If myCar is the only reference to the Car object, it *may* be destroyed when this
method returns.
Car myCar = new Car();
}
Car的对象在函数外,没有可以引用到,就意味着该对象可以被回收了。
3.
C#有两类对象,托管和非托管对象。这个托管指的是对象被谁管理,在.net框架里,就是被CLR托管。非托管对象,指的是一些操作系统提供的资源,比如文件句柄,Socket,数据库连接等。
这些资源对象的使用,需要像操作系统申请并且使用完后,及时的归还。比如C#的FileStream类,我们使用时有如下一段代码:
FileInfo finfo = new FileInfo(FilePath);
//以打开或者写入的形式创建文件流
using (FileStream fs = finfo.OpenWrite())
...{
//根据上面创建的文件流创建写数据流
StreamWriter sw = new StreamWriter(fs, System.Text.Encoding.Default);
//把新的内容写到创建的HTML页面中
sw.WriteLine(strhtml);
sw.Flush();
sw.Close();
}
其中,finfo, fs, sw三个分别为FileInfo,FileStream,StreamWriter的对象。
关于C#的垃圾回收机制,Finalize和Dispose的区别(自认为很清晰了,有疑问的评论)的更多相关文章
- java: system.gc()和垃圾回收机制finalize
System.gc()和垃圾回收机制前的收尾方法:finalize(收尾机制) 程序退出时,为每个对象调用一次finalize方法,垃圾回收前的收尾方法 System.gc() 垃圾回收方法 clas ...
- Android_对android虚拟机的理解,包括内存管理机制垃圾回收机制。dalvik和art区别
虚拟机很小,空间很小,谈谈移动设备的虚拟机的大小限制 16M ,谈谈加载图片的时候怎么处理大图片的,outmemoryExceptionBitmapFactory.option 垃圾回收,没有引用的对 ...
- .net垃圾回收机制编程调试试验
1. 什么是CLR GC? 它是一个基于引用跟踪和代的垃圾回收器. 从本质上,它为系统中所有活跃对象都实现了一种引用跟踪模式,如果一个对象没有任何引用指向它,那么这个对象就被认为是垃圾对象,并且可以被 ...
- 垃圾回收机制GC知识再总结兼谈如何用好GC
一.为什么需要GC 应用程序对资源操作,通常简单分为以下几个步骤: 1.为对应的资源分配内存 2.初始化内存 3.使用资源 4.清理资源 5.释放内存 应用程序对资源(内存使用)管理的方式,常见的一般 ...
- JAVA的垃圾回收机制
1. 垃圾回收的意义 在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象:而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾.JVM的 ...
- JVM的生命周期、体系结构、内存管理和垃圾回收机制
一.JVM的生命周期 JVM实例:一个独立运行的java程序,是进程级别 JVM执行引擎:用户运行程序的线程,是JVM实例的一部分 JVM实例的诞生 当启动一个java程序时.一个JVM实例就诞生了, ...
- java JVM垃圾回收机制
Java语言出来之前,大家都在拼命的写C或者C++的程序,而此时存在一个很大的矛盾,C++等语言创建对象要不断的去开辟空间,不用的时候有需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都 ...
- 【转】【C#】C# 垃圾回收机制
摘要:今天我们漫谈C#中的垃圾回收机制,本文将从垃圾回收机制的原理讲起,希望对大家有所帮助. GC的前世与今生 虽然本文是以.NET作为目标来讲述GC,但是GC的概念并非才诞生不久.早在1958年,由 ...
- Java高级之虚拟机垃圾回收机制
博客出自:http://blog.csdn.net/liuxian13183,转载注明出处! All Rights Reserved ! 区别于C语言手动回收,Java自动执行垃圾回收,但为了执行高效 ...
随机推荐
- Unity Shader着色器优化
https://mp.weixin.qq.com/s?__biz=MzU5MjQ1NTEwOA==&mid=2247493518&idx=1&sn=c51b92e9300bcf ...
- springboot jpa mongodb 多条件分页查询
public Page<Recorded> getRecordeds(Integer page, Integer size, Recorded recorded) { if (page&l ...
- spark sql 导出数据
如果用户希望在spark sql 中,执行某个sql 后,将其结果集保存到本地,并且指定csv 或者 json 格式,在 beeline 中,实现起来很麻烦.通常的做法是将其create table ...
- Gradle安装配置
1.构建工具的简单介绍在代码世界中有三大构建工具,ant.Maven和Gradle. 现在的状况是maven和gradle并存,gradle使用的越来越广泛. Maven使用基于XML的配置,Grad ...
- Vue双向绑定实现原理demo
一.index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> ...
- 利用git reflog找回错误的重置
在开发中经常需要reset分支,如果在reset前没有记住分支指向的提交ID,想要重置回原来的提交恐怕大多数开发者是重新拉取远程版本库,再rebase分支.但如果连不上远程版本库或没有远程版本怎么办呢 ...
- 什么是obj文件?
百度百科: 程序编译时生成的中间代码文件.目标文件,一般是程序编译后的二进制文件,再通过链接器(LINK.EXE)和资源文件链接就成可执行文件了.OBJ只给出了程序的相对地址,而可执行文件是绝对地址. ...
- JS——变量和函数的预解析、匿名函数、函数传参、return
JS解析过程分为两个阶段:编译阶段.执行阶段.在编译阶段会将函数function的声明和定义都提前,而将变量var的声明提前,并将var定义的变量赋值为undefined. 匿名函数: window. ...
- Java操作Excel之POI简单例子
/** * 利用POI操作Excel表单 * * 需要jar包: * HSSF针对03及以前版本,即.xls后缀 * |---poi-3.16.jar * XSSF针对07及以后版本,即xlsx后缀 ...
- mybatis + log4j2 问题 java.lang.NoClassDefFoundError: org/apache/logging/log4j/spi/AbstractLoggerWrapper
root cause java.lang.NoClassDefFoundError: org/apache/logging/log4j/spi/AbstractLoggerWrapper 网上资料比较 ...