作为一位C++出身的C#程序员,我最初对垃圾收集(GC)抱有怀疑态度,怀疑它是否能够稳定高效的运作;而到了现在,我自己不得不说我已经逐渐习惯并依赖GC与我的程序“共同奔跑”了,对“delete”这个习惯于充当罪魁祸首的关键字也渐渐产生了陌生感。然而实践证明,我对GC的过分信赖却招致了很多意想不到的错误,这也激励了我对GC的运作机制作深入一步的了解。随后我开始翻书,查资料,终于对GC有了一个比较完整的理解(但远远算不上深入)。有人也许会说:“研究GC的内部机制有什么价值吗?我们是搞应用程序开发的,客户的机器可以达到很高的配置,内存资源不是问题。”这种说法明显是认为“垃圾收集=内存释放”了,其实在垃圾收集中,造成最多麻烦的往往不是内存量,而是在内存释放之外,GC暗地里为我们做的繁杂事务(例如非托管资源的清理和释放)。如果你对GC的基本运作还不了解,而又没有时间仔细阅读众多技术资料的话,那么我的这几篇文章或许对你能有一些帮助。

下面就从资源的分配和释放入手,先了解一下背景知识。

一. 托管资源的分配

CLR在运行时管理着一段内存地址空间(虚拟地址空间,在运行中会映射到物理内存地址中),分为“托管堆”和“栈”两部分,栈用于存储值类型数据,它会在方法执行结束后自动销毁其中引用的值类型变量,这一部分不属于垃圾收集的范围。托管堆用于引用类型的变量存储,是垃圾收集的关键阵地。

托管堆是一段连续的地址空间,其中所分配出去的空间呈现出类似数组形态的队列结构:

NextObjPtr是托管堆所维护的一个内存指针,指示下一个对象分配的内存起始地址,它会随着内存的分配而不断移动(当然也会随着内存垃圾回收而发生移动),永远指向下一个空闲的地址。

到了这里,我们不妨与C++比较一下内存分配机制的效率(对效率不感兴趣的大可以跳过:)),顺便让C++的朋友们打消一些对CLR分配内存效率的疑虑。在查找空闲内存空间时,CLR只需要在NextObjPtr处直接留出指定大小的空间提供给数据初始化,然后计算新的空闲地址并重置NextObjPtr指针即可。而在C/C++中,在分配内存之前先要遍历一遍内存占用的链表以查找合适大小的内存块,然后再修改此链表,这样也很容易产生内存碎块,使得内存分配性能下降。很明显,.NET的分配方式效率更高。但是这种效率是以GC的劳动为代价的。

二. 垃圾判定

要进行垃圾收集,首先要知道什么是垃圾。GC通过遍历应用程序中的“根”来寻找垃圾。我们可以认为根是一个指向引用类型对象内存地址的指针。如果一个对象没有了根,就是它不再被任何位置所引用,那么它就是垃圾的候选者了。

值得注意的一点是,对象可能在其生存期结束之前就被列入垃圾名单,甚至已经被GC所暗杀!那是因为对象可能在生存期的某一时刻已经不再被引用,如果在这个时候执行垃圾收集,那么这个不幸的对象极有可能已经被列为垃圾并被销毁(为什么说是“可能”呢?因为它不一定在GC的视力范围内。后面讲到“代龄”时会详细介绍相关细节)。

1publicstaticvoid Main()

2 {

3string sGarbage= "I'm here";

4

5//下面的代码没有再引用s,它已经成为垃圾对象---当然,这样的代码本身也是垃圾;

6//此时如果执行垃圾收集,则sGarbage可能已经魂归西天

7

8 Console.WriteLine("Main() is end");

9 }

三. 对象代龄

尽管GC总是在默默为我们劳动,但它毕竟是由人创造的,人会偷懒,它也会。为了减少每次的工作量,它总是希望能够减少工作的范围;它坚信,越晚创建的对象往往越短命,因此它会集中精力处理这一部分的内存区域,暂且搁置其他部分。GC引入“代龄”的概念来划分对象生存级别。

CLR初始化后的第一批被创建的对象被列为0代对象。CLR会为0代对象设定一个容量限制,当创建的对象大小超过这个设定的容量上限时,GC就会开始工作,工作的范围是0代对象所处的内存区域,然后开始搜寻垃圾对象,并释放内存。当GC工作结束后,幸存的对象将被列为第1代对象而保留在第1代对象的区域内。此后新创建的对象将被列为新的一批0代对象,直到0代的内存区域再次被填满,然后会针对0代对象区域进行新一轮的垃圾收集,之后这些0代对象又会列为第1代对象,并入第1代区域内。第1代区域起初也会被设上一个容量限制值,等到第1代对象大小超过了这个限制之后,GC就会扩大战场,对第1代区域也做一次垃圾收集,之后,又一次幸存下来的对象将会提升一个代龄,成为第2代对象。

可见,有一些对象虽然符合垃圾的所有条件,但它们如果是第1代(甚至是第2代老臣)对象,并且第1代的分配量还小于被设定的限制值时,这些垃圾对象就不会被GC发现,并且可以继续存活下去。

另外,GC还会在工作过程中汲取经验,根据应用程序的特点而自动调整每代对象区域的容量,从而可以更高效的工作。

应该了解的垃圾收集机制(二)

对于大多数应用而言,了解垃圾收集机制的主要动机并不是为了对内存“省吃俭用”,而是为了处理非托管资源的控制问题,这些问题往往跟内存的大小没有什么关系。例如对一个文件进行操作,该何时关闭文件,关闭文件时要注意什么问题,如果忘了关闭会带来什么后果?这些都是我们需要认真考虑的,无论你的内存有多大:)

对于这一类的操作,我们不能依赖GC帮我们做,因为它并不知道我们在释放时想干什么,它甚至不知道自己该干什么!我们不得不自己动手来编写处理代码。当然,微软已经为我们搭好了框架,就是这两个函数:Finalize和Dispose。它们也代表了非托管清理的两种方式:自动和手动。

一. Finalize

Finalize很像C++的析构函数,我们在代码中的实现形式为这与C++的析构函数在形式上完全一样,但它的调用过程却大不相同。

~ClassName() {//释放你的非托管资源}

比如类A中实现了Finalize函数,在A的一个对象a被创建时(准确的说应该是构造函数被调用之前),它的指针被插入到一个finalization链表中;在GC运行时,它将查找finalization链表中的对象指针,如果此时a已经是垃圾对象的话,它会被移入一个freachable队列中,最后GC会调用一个高优先级线程,这个线程专门负责遍历freachable队列并调用队列中所有对象的Finalize方法,至此,对象a中的非托管资源才得到了释放(当然前提是你正确实现了它的Finalize方法),而a所占用的内存资源则必需等到下一次GC才能得到释放,所以一个实现了Finalize方法的对象必需等两次GC才能被完全释放。

由于Finalize是由GC负责调用,所以可以说是一种自动的释放方式。但是这里面要注意两个问题:第一,由于无法确定GC何时会运作,因此可能很长的一段时间里对象的资源都没有得到释放,这对于一些关键资源而言是非常要命的。第二,由于负责调用Finalize的线程并不保证各个对象的Finalize的调用顺序,这可能会带来微妙的依赖性问题。如果你在对象a的Finalize中引用了对象b,而a和b两者都实现了Finalize,那么如果b的Finalize先被调用的话,随后在调用a的Finalize时就会出现问题,因为它引用了一个已经被释放的资源。因此,在Finalize方法中应该尽量避免引用其他实现了Finalize方法的对象。

可见,这种“自动”释放资源的方法并不能满足我们的需要,因为我们不能显示的调用它(只能由GC调用),而且会产生依赖型问题。我们需要更准确的控制资源的释放。

二. Dispose

Dispose是提供给我们显示调用的方法。由于对Dispose的实现很容易出现问题,所以在一些书籍上(如《Effective C#》和《Applied Microsoft.Net Framework Programming》)给出了一个特定的实现模式:

class DisposePattern :IDisposable

{

private System.IO.FileStream fs = new System.IO.FileStream("test.txt", System.IO.FileMode.Create);

~DisposePattern()

{

Dispose(false);

}

IDisposable Members#region IDisposable Members

public void Dispose()

{

//告诉GC不需要再调用Finalize方法,

//因为资源已经被显示清理

GC.SupdivssFinalize(this);

Dispose(true);

}

#endregion

protected virtual void Dispose(bool disposing)

{

//由于Dispose方法可能被多线程调用,

//所以加锁以确保线程安全

lock (this)

{

if (disposing)

{

//说明对象的Finalize方法并没有被执行,

//在这里可以安全的引用其他实现了Finalize方法的对象

}

if (fs != null)

{

fs.Dispose();

fs = null; //标识资源已经清理,避免多次释放

}

}

}

}

在注释中已经有了比较清楚的描述,另外还有一点需要说明:如果DisposePattern类是派生自基类B,而B是一个实现了Dispose的类,那么DisposePattern中只需要override基类B的带参的Dispose方法即可,而不需要重写无参的Dispose和Finalize方法,此时Dispose的实现为:

class DerivedClass : DisposePattern

{

protected override void Dispose(bool disposing)

{

lock (this)

{

try

{

//清理自己的非托管资源,

//实现模式与DisposePattern相同

}

finally

{

base.Dispose(disposing);

}

}

}

}

当然,如果DerivedClass本身没有什么资源需要清理,那么就不需要重写Dispose方法了,正如我们平时做的一些对话框,虽然都是继承于System.Windows.Forms.Form,但我们常常不需要去重写基类Form的Dispose方法,因为本身没有什么非托管的咚咚需要释放。

了解GC的脾性在很多时候是非常必要的,起码在出现资源泄漏问题的时候你不至于手足无措。我写过一个生成excel报表的控件,其中对excel对象的释放就让我忙活了一阵。如果你做过excel开发的话,可能也遇到过结束excel进程之类的问题,特别是包装成一个供别人调用的库时,何时释放excel对象以确保进程结束是一个关键问题。当然,GC的内部机制非常复杂,还有许多内容可挖,但了解所有细节的成本太高,只需了解基础,够用就好。

(转)C#的 GC工作原理基础的更多相关文章

  1. JVM 的 工作原理,层次结构 以及 GC工作原理

    JVM Java 虚拟机 Java 虚拟机(Java virtual machine,JVM)是运行 Java 程序必不可少的机制.JVM实现了Java语言最重要的特征:即平台无关性.原理:编译后的 ...

  2. 优秀Java程序员必须了解的GC工作原理

    一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率 ,才能提高整个应 ...

  3. 优秀Java程序员必须了解的GC工作原理(转)

    一个优秀的Java程序员必须了解GC(Garbage Collection 垃圾收集)的工作原理.如何优化GC的性能.如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统.实时系 ...

  4. GC入门指南(二)------GC工作原理

    本系列博客旨在帮助大家理解java垃圾收集器及其工作原理,这是系列的第二篇. java垃圾回收事实上是由一个能够进行自己主动内存管理的进程完毕的,这使得程序猿在写代码的时候不必过多考虑内存释放与回收的 ...

  5. Java GC工作原理以及Minor GC、Major GC、Full GC简单总结

    名词解释: GC:垃圾收集器 Minor GC:新生代GC,指发生在新生代的垃圾收集动作,所有的Minor GC都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个过程非 ...

  6. JVM和GC的工作原理

    转载于https://uestc-dpz.github.io JVM Java 虚拟机 Java 虚拟机(Java virtual machine,JVM)是运行 Java 程序必不可少的机制.JVM ...

  7. Hadoop基础-MapReduce的工作原理第一弹

    Hadoop基础-MapReduce的工作原理第一弹 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 在本篇博客中,我们将深入学习Hadoop中的MapReduce工作机制,这些知识 ...

  8. 【转】优秀的Java程序员必须了解GC的工作原理

    一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率 ,才能提高整个应 ...

  9. .Net Core 中GC的工作原理

    前言 .NET 中GC管理你服务的内存分配和释放,GC是运行公共语言运行时(CLR Common Language Runtime)中,GC可以帮助开发人员有效的分配内存和和释放内存,大多数情况下是不 ...

随机推荐

  1. mysql_pw 指令 数据库创建过程

    ------------------pw_db数据库创建过程各表创建指令-------------------------- create database pw_db; #创建一个数据库use pw ...

  2. 【PAT甲级】1066 Root of AVL Tree (25 分)(AVL树建树模板)

    题意: 输入一个正整数N(<=20),接着输入N个结点的值,依次插入一颗AVL树,输出最终根结点的值. AAAAAccepted code: #define HAVE_STRUCT_TIMESP ...

  3. mysqld: [ERROR] Found option without preceding group in config file D:\TONG\mysql-5.7.19-winx64\my.ini at line 1!

    my.ini文件编码不对,改为ANSI 貌似大意是说,配置不全 改编码为ANSI解决

  4. 简单bat脚本

    hwf.bat: set GAP_HOME=%~dp0\.. ::copy "%JAVA_HOME%\bin\javaw.exe" "%JAVA_HOME%\bin\HW ...

  5. 无法访问Eureka主页

    spring: application: name: 90foodmall-eureka server: port: 8000 eureka: client: #是否将自己注册到Eureka Serv ...

  6. Java中短路

    当使用逻辑运算符时,我们会遇到一种“短路”的现象.即一旦能够准确无误的确定整个表达式的值,就不再计算表达式余下的部分了.因此整个表达式靠后的部分有可能不被运算 /**短路 * @param args ...

  7. Java基础知识笔记第十章:输入输出流

    File类 文件的属性 目录 文件的创建与删除 运行可执行文件 文件字节输入流 文件字节输出流 文件字符输入输出流 缓冲流 随机流 数组流 数据流 对象流 序列化与对象克隆 使用Scanner解析文件 ...

  8. redhat 7.6 apache 服务简单安装-01

    rpm -qa | grep httpd         //该命令查看apache是否安装,下面图片是已安装,未安装不会显示任何内容 yum install   httpd   -y        ...

  9. 上篇: php 微信公众号 基于Thinkphp3.2框架开发

    说明:本教程是自己自学+自己的理解+扩展(包括学习过程中遇到的一些问题) 参考教程:麦子学院--李忠益--http://www.maiziedu.com/u/70409/ 微盟: http://www ...

  10. 139、Java内部类之使用this访问外部类属性

    01.代码如下: package TIANPAN; class Outer { // 外部类 private String msg = "Hello World !"; class ...