C#中十分不安全的java 垃圾回收收,你注意到了吗

开始说垃圾回收之前先说说托管资源和非托管资源。
先来说说是什么托管资源和非托管资源:托管资源一般是指被CLR控制的内存资源,这些资源的管理可以由CLR来控制,例如程序中分配的对象,作用域内的变量等。这部分的资源是可以被垃圾回收器自动回收的;非托管资源自然就是不被CLR控制的资源,垃圾回收器无法对其实现自动回收了。
那么如何及时的清理使用完毕的资源呢?
实现Dispose方法
Finalize方法和析构函数
通过GC类进行强制回收
清理对象使用的资源就让对象实现Dispose方法,Dispose方法释放它拥有的所有资源。它还应该通过调用其父类型的 Dispose 方法应该释放它拥有的所有资源并同样也调用其父类型的 Dispose 方法,从而在整个基类型层次结构中传播此模式。若要确保始终正确地清理资源,Dispose 方法应该可以被多次调用而不引发任何异常。
&!--&br /&&br /&Code highlighting produced by Actipro CodeHighlighter (freeware)&br /&http://www.CodeHighlighter.com/&br /&&br /&--&1 Font font2 = new Font("Arial", 10.0f);2 using (font2)3 {4
// use font25 }
&!--&br /&&br /&Code highlighting produced by Actipro CodeHighlighter (freeware)&br /&http://www.CodeHighlighter.com/&br /&&br /&--&using (Font font3 = new Font("Arial", 10.0f),font4 = new Font("Arial", 10.0f))
// Use font3 and font4.}
浏览: 40564 次
来自: 福州
你试试看在代码2中 img前面加个空格。
(window.slotbydup=window.slotbydup || []).push({
id: '4773203',
container: s,
size: '200,200',
display: 'inlay-fix'1.托管资源的回收
  我们都知道C#托管资源的回收由GC全权负责控制,可是什么时候GC会回收垃圾呢?一般出现以下情况会回收垃圾:手动调用GC.Collect()强制回收;第0代对象内存已满;应用程序域被卸载时,CLR会回收所有资源;windows报告内存不足。其中作为开发人员,我们应该尽量少使用Collect方法,除非已经很明确的知道内存中有很多大对象需要被回收,否则随意使用Collect方法会导致扰乱GC正常的工作方式,从而扰乱了应用程序的内存使用。现在来看一个完整的托管垃圾回收过程,如下图所示。从这个过程可以看到分代回收的优点:经过几次垃圾回收后全局对象等生存时间较长的对象将被放到第二代,由于第0代空间很小因此可以快速遍历寻找未被引用的对象,而正好第0代总是有生存时间较短的新对象加入,最终的效果就是提高了性能。在图中还提到了根的概念,每个应用程序都有由JIT编译器和CLR运行时维护的一组指向托管堆中内存的根指针,主要包括全局变量、静态变量、局部变量、寄存器指针。当遍历代中堆的对象时,一个根将作为一个入口点,我们说的对象被引用指的是在根上有指向对象的指针。如下图a指向objA,objA、objB、objC形成一个环,此时如果a离开了作用域而被释放,objB和objC也会被标记为不可达,GC只会标记一次之后不会再标记。
  当GC把不可达的对象标记为不可达时,并不会就开始释放资源,而是会检查Finalization队列中是否有指针指向不可达对象,如果有这样的对象则会将Finilazation队列中的指针移到Freachable队列中。Freachable队列中一旦有指针则会触发指向的对象去执行Finalize方法,之后再从队列中删除这个指针,被指向的对象就可以由GC回收了。这样设计也是为了彻底的释放资源,当new一个含有Finalize方法的对象时,就会在Finalization队列中添加指向这个对象的指针。一般我们使用Finalize方法是为了释放非托管资源,所以为了彻底释放资源我们要保证非托管资源也被正确释放了。
&垃圾回收就是不断地循环上述的步骤,当第二代内存也满了则会对第二代内存来一次垃圾回收,不过这种情况发生的概率很低。另外在程序运行过程中这3代的内存大小并不是不变的,而是会根据实际情况动态改变的。现在假设执行完了一次垃圾回收,将第0代和第1代的不可达对象清理了。可以想象在堆中存在着许多碎片,连续的代内存变得不再连续。因此GC会线程挂起接着来一次内存压缩,在本例中是将第0代和第1代的剩余对象转移到第2代中,从这个过程中可以发现并不是简单的复制对象就可以了,因为根中的指针所指向的对象已被转移。所以此时GC需要修改应用程序的根指针和发生对象引用的指针,让这些指针指向新的对象内存地址。如果这里有一个对象是被非托管资源所指向的,由于GC无法去修改非托管资源的指针,因此这个对象将不会被转移。
2.非托管资源的回收
  在C#中当我们使用非托管资源比如文件操作、数据库链接、套接字时就使用了非托管资源,比如文件操作程序中我们会在操作非托管资源时使用using包起来或者最后调用Close()方法。使用Reflector工具可以看到加上using语句块其实就是在程序中最后添加了Close方法,Close方法则是调用了Dispose方法,之后又调用了SuppressFinalize方法让Finalize方法禁止调用。从Close方法的内部实现可以看出在C#中释放非托管资源的工作是交给Finalize与Dispose这2个方法了。Finalize方法是有我们程序员自己定义的一个方法来释放内存,调用时间是上面提到的GC垃圾回收前。Dispose方法是实现了IDispose接口中的Dispose方法,开发者直接调用来释放非托管资源。
  在VS中创建一个FileStream对象fs,会发现使用点是无法点出Finalize方法的,F12进去也没有看到这个方法。这是因为.NET对其做了规定,开发人员只能通过析构函数来实现,不能显示的进行调用。如下面代码所示,如果我在析构函数中不加上sw.Close()的话te.txt打开是什么都看不到的,因为这时非托管资源没有释放。也就是说这里析构函数没起作用,它只是一个声明告诉CLR这个对象的指针需要添加到Finalization中,因此我们需要在析构函数中手动添加Close方法去释放资源。再来看看IL代码,可以看到析构函数在IL中就是一个Finalize方法,方法里面又调用了父类的Finalize方法。从这可以看出Finalize方法的一个特点,子类Finalize方法中会调用父类的Finalize方法,这样递归调用可保证所有父类直到object的资源都被清理掉,不过这对性能也是一个很大的损失啊。现在可以总结Finalize的工作原理了,首先Object类有一个受保护的实现了的虚方法,.NET要求每一个释放非托管资源的类通过析构函数的方式重写这个方法,当然也可以不重写,如果没有重写则Finalization队列中不会添加这个这个对象的指针。如果添加了析构函数,则需要在析构函数中编写释放资源的代码,说到底Finalize方法需要我们程序员手动的释放非托管资源。而且它被调用的时机还不知道,只知道是在一个对象变为不可达后才会被调用,这样的话可能在下一个GC回收周期非托管资源才被释放或者代数的增加。另外Finalization和Freachable2个队列的维护以及GC开新线程去执行Finalize方法(包括父类的)都将带来性能的损耗。
public class MyClass
~MyClass()
//进行资源的清理
sw.Close();
public void Func()
FileStream fs = new FileStream("D:\\te.txt", FileMode.OpenOrCreate);
sw = new StreamWriter(fs);
string str="哈哈";
sw.Write(str);
从上面可以看出对于非托管资源的释放,Dispose方法是首选,只需我们手动的编写一条代码即可释放,控制权在程序员手中并且性能比Finalize要好。关于Dispose的工作模式可查看我的另一篇随笔cnblogs.com/fangyz/p/5293888.html,一般操作非托管资源的类都重写了Dispose方法,比如可以在VS中看到FileStream的Dispose方法。如果我们要在自定义类中重写Dispose方法,最后要加上base.Dispose(),这样可保证继承链上的父类资源也释放了资源。
&声明:本文原创发表于博客园,作者为,如有错误欢迎指出&。本文未经作者许可不许转载,否则视为侵权。
阅读(...) 评论()基于.NET平台的开发语言中,最让开发人员爽的一点就是垃圾回收处理机制,在编码过程中,终于可以解放你的双手来关注更重要的事情。很多的资料中在讲到.NET中的垃圾回收机制时都说&CLR在合适的时候进行垃圾回收&,但什么时候才是&合适的时候&?内存又是如何分配的?CLR是如何对内存进行回收的?这一章我们来讨论有关垃圾回收的相关内容。
第一节 垃圾回收机制
早期的C/C++开发中,一个对象的生命周期大概像这样:计算对象大小&&查找可用内存&&初始化对象&&使用对象&&摧毁对象。如果在上面的过程中,开发人员忘记了&摧毁对象&这一步骤,则很有可能导致内存泄露!这是一个非常可怕的事情!幸好,CLR的开发人员为我们解决了这一问题,在.NET Framework中引入了垃圾回收机制,使得开发人员不需要再过多地关注内存释放的问题,CLR会在合适的时候进行执行垃圾回收来释放不再使用的内存。这里就像一个邪恶的男人所说的话:给我一个女人,我能创造一个民族!其实一个新世界你都可以去创造,前提是要有一个足够大的星球内存来容纳你的子孙!CLR就是这么认为的。
在激活一个进程时,CLR会先保留一块连续的内存,在主线程启动过程中,可能会初始化一系列对象,CLR先计算对象大小及其开销所占用的字节数,接着会在连续的内存块中为这些对象分配内存,这些对象被配置在第0代内存,在构造第0代内存的时候会分配一个默认大小的内存,随着程序的运行,可能会初始化更多的对象,CLR发现第0代内存不能装载更多的新生对象,此时CLR会启动垃圾回收器对第0代内存进行回收,不再使用的对象所占用的内存会被释放,接着把0代对象提升为第1代,然后把新生对象配置在第0代内存区中。CLR使用了3个阶段的代,每次新分配的对象都会被配置在第0代内存中,最老的对象在第2代内存中,每次为新对象分配内存时,都可能会进行垃圾回收以释放内存,很显然CLR认为&内存永远也使用不完&,很显然CLR为我们自动管理了内存垃圾,很显然CLR的这个&认为&在我们开发人员看来是不成立的,我们从以下几个方面来解读垃圾回收机制。
第二节 内存分配
垃圾回收是对引用类型而言的。
CLR要求引用类型的对象从托管堆中分配内存的,值类型是从栈中分配内存。在C#中通常使用new操作符来创建一个对象,编译器将会在IL中生成newobj指令,执行一个newobj指令会有以下过程:(在前一节中我们已经知道,在一个进程启动时会先保留一个连续的内存块)先计算类型及其基类型的字段所需要的字节数A,再计算类型对象的指针和一个同步索引块共8或16个字节,到此总共需要(A+8或18)字节的内存,CLR会检查当前进程区是否有足够的内存来容纳(A+8或16)个字节的对象,如果有,则将新对象放其中,否则CLR进行垃圾回收,释放不再使用的内存来容纳新的对象,在整个进程的生命周期中,CLR会维护一个指针P,它一直指向当前进程所分配的最后一个对象内存的结尾处而不会跑出当前进程内存区边界,如图:
每次计算新的将要创建的对象所需要的字节数时,CLR都是通过P加上新的需要的字节数进行检查可用内存区,如果超出了地址末尾,则表示当前的托管堆已经被用完,准备进行垃圾回收了。由于进程拥有一个独立连续的内存区,所以CLR能保证创建的新对象基本上都是紧挨着放置的。
当托管堆的内存被用完,新生的对象无处放置时,CLR就要开始进行垃圾回收了,随着程序的持续运行,托管堆可能越来越大,如果要对整个托管堆进行垃圾回收(下面会讲到如何回收),势必会严重影响性能,因为有时可能仅仅需要数十个字节就能容纳新的对象,有时候可能要对可达的对象进行搬迁,为了小范围有目的性地进行垃圾回收,CLR使用了&代&概念来优化垃圾回收器,代是垃圾回收机制使用的一个逻辑技术,也是一种算法,它把托管堆中的内存分为3个代(截止到目前.NET Framework4.0有3个代:0、1、2)。
进程在初始化时,CLR为托管堆初始化为包含0个对象的一块内存区域,新添加到堆中的对象为第0代对象,CLR在初始化第0代内存区时会分配一个默认的配额,假设为512K,不同的.NET框架和版本,可能这个配额不相同。假设进程及其线程初始化完成后分配了4个对象,如下图:
这4个对象占据了512K的内存,程序继续运行,当再分配第5个对象Obj5的时候,发现第0代已无可用内存,此时CLR会启动垃圾回收器进行垃圾回收,假如上面的Obj3已经无效,此是Obj3的内存会被释放出来,接着搬迁Obj4对象到Obj3的位置(在Obj2的内存地址末尾处),存活下来的对象Obj1、Obj2和Obj4会被提升为第1代对象,第1代的内存区域根据程序运行的情况,CLR可能会为其分配20M(也可能是其他值)大小的内存区,第0代内存暂时为空,接着将Obj5分配到第0代内存区,如下:
程序继续运行,并又新分配了4个对象Obj6-Obj9,且此时Obj2和Obj5都不再使用,即为不可达对象,此时需要再创建一个新对象Obj10,但发现第0代的512K内存已经用完,所以CLR再一次启动垃圾回收器进行垃圾回收,这一次垃圾回收器会认为第0代的新对象生命周期短,所以先对第0代进行回收,并将存活对象提升到第1代中,垃圾回收器发现此时第1代中的对象远远小于20M,所以放弃对第1代的回收,程序继续运行,分配N多的新对象,当把第0代的对象提升到第1代,而第1代对象超20M时,则会对第1代的对象进行回收,第1代存活的对象被提升为第2代,第0代存活的对象被提升为第1代,如下图:
每一次垃圾回收的过程,垃圾回收器会根据实际使用情况自动调整第0、1、2代的默认配额大小,比如可能将第2代调整为200M,几分钟过后可能将其调整为120M,也有可能是1024M,程序继续运行,当对3个全部进行了垃圾回收且重新调整配额后,可用内存还不足以放置新对象,CLR就会抛出OutOfMemoryException异常,此时活神仙也无法施救了。原来CLR认为&内存永远也使用不完&也是有条件的啊!
第四节 垃圾回收过程
托管堆中的一个对象,当线程中有变量对其引用则为可达对象,否则为不可达对象。
在一次垃圾回收过程开始时,垃圾回收器会认为堆中的所有对象都是垃圾。
第一步是标记对象,垃圾回收器沿着线程栈上行检查所有根,静态字段、方法参数、活动中的局部变量以及寄存器指向的对象等都是根,当发现有根引用了托管堆中的对象A时,垃圾回收器会对此对象A进行标记,在标记A时,如果检测到对象A内又引用了另一个对象B,则也对B进行标记,对一个根检测完毕后会接着检测下一个根,执行同样的标记过程,代码中很有可能多个对象中引用了同一个对象C,垃圾回收器只要检测到对象C已经被标记过,则不再对对象C内所引用的对象进行检测,以防止无限循环标记。有标记的对象就是可达对象,未标记的对象就是不可达对象。
第二步是搬迁对象压缩堆,垃圾回收器遍历堆中的所有对象来寻找未标记的对象,因为未标记的对象是垃圾对象,可以进行回收,如果发现对象较小,则忽略,否则会先释放这些垃圾对象所占的内存,再把可达对象搬迁到这里以压缩堆,在搬迁可达对象之后,所有指向这些对象的变量将无效,接着垃圾回收器要重新遍历应用程序的所有根来修改它们的引用。在这个过程中如果各个线程正在执行,很可能导致变量引用到无效的对象地址,所以整个进程的正在执行托管代码的线程是被挂起的。
其实在垃圾回收器准备开始一次回收时,正在执行托管代码的所有线程都必须被挂起,挂起时,CLR会记录每个线程的指令指针以确定线程当前执行到哪里以便将来在垃圾回收结束后进行恢复。如果一个线程的指令指针恰好到达了一个安全点,则可以挂起该线程,否则CLR会尝试劫持该线程,如果还未到达安全点,则等待几百毫秒后CLR会尝试再一次劫持该线程,有可能经过多次尝试,最终挂起该线程,当当前进程的所有执行托管代码的线程都挂起后,垃圾回收器就可以开始工作了。(有关线程劫持可查找相关资料)。垃圾回收器回收完毕后,CLR恢复所有线程,程序继续运行。可见,垃圾回收对性能影响之巨大!
第五节 大对象
在创建新对象时,任何大于等于85000字节的对象都被认为是大对象,这些对象的内存是从大对象堆中分配的,大对象总是被认为是第2代对象,要尽量避免分配大对象来减少性能损伤,为了提高性能,垃圾回收器不对大对象进行搬迁压缩,只在回收第2代内存时进行回收。
第六节 手工进行回收
一般的情况下,CLR会智能地在必要的时候更行垃圾回收,但我们也可以在我们愿意的情况下手动启动垃圾回收器,System.GC类提供了重载版本的静态方法来启动垃圾回收器:
//对所有代进行垃圾回收。
GC.Collect();
//对指定的代进行垃圾回收。
GC.Collect(int generation);
//强制在 System.GCCollectionMode 值所指定的时间对零代到指定代进行垃圾回收。
GC.Collect(int generation, GCCollectionMode mode);
在上一节中我们已经知道,每一次垃圾回收过程都会导致性能损伤,所以我们尽量避免调用这3个方法进行垃圾回收,当然必要的时候也可以调用。
不仅仅以上谈到几种情况下会启动垃圾回收器,当CLR接到Windwos发出内存告急通知时也会启动垃圾回收、CLR卸载AppDomain时也会启动垃圾回收。
阅读(...) 评论()Access denied | www.bkjia.com used Cloudflare to restrict access
Please enable cookies.
What happened?
The owner of this website (www.bkjia.com) has banned your access based on your browser's signature (42fbb39d12a378da-ua98).

我要回帖

更多关于 java 垃圾回收 的文章

 

随机推荐