.net垃圾回收机制编程调试试验

1. 什么是CLR GC?

它是一个基于引用跟踪和代的垃圾回收器。

从本质上,它为系统中所有活跃对象都实现了一种引用跟踪模式,如果一个对象没有任何引用指向它,那么这个对象就被认为是垃圾对象,并且可以被回收。GC通过代的概念来跟踪对象的持续时间,活跃时间段的对象被归为0代,而活跃时间更长的被归为1代和2代。CLR认为大多数对象都是短暂活跃的,因此0代被收集的频率远远高于1代和2代。

看下GC中对象及其代龄分布:

在.net中,初始分配的小对象在0代上; 通过垃圾回收后,存活的有根对象将被移动到后一代上。

有根对象(引用对象)有哪些?

1.静态、全局对象,包括缓存和进程内Session

2.Com对象计数器

3.线程堆栈上的局部变量钉扣对象,

4.本地API调用,Remoting/Webservice调用

5.finalizer 队列里的对象

先来一个简单的实例程序,

 1 public class Student
 2     {
 3         public string Name { get; set; }
 4         public string Address { get; set; }
 5         public Student(string name, string address)
 6         {
 7             Name = name;
 8             Address = address;
 9         }
10     }
11     class Program
12     {
13         static void Main(string[] args)
14         {
15             Student wang = new Student("wang", "Beijing");
16             Student lee = new Student("lee", "Shanghai");
17
18             //GC.Collect();
19             Console.ReadLine();
20         }
21     }

Run Windbg, 看下:

!eeheap -gc

看下每一代在CLR 堆中的起始地址,输出关于GC的信息:

Number of GC Heaps: 1
generation 0 starts at 0x0000000002621030
generation 1 starts at 0x0000000002621018
generation 2 starts at 0x0000000002621000
ephemeral segment allocation context: none
         segment            begin         allocated             size
0000000002620000 0000000002621000  0000000002625fe8 0x0000000000004fe8(20456)
Large object heap starts at 0x0000000012621000
         segment            begin         allocated             size
0000000012620000 0000000012621000  0000000012627048 0x0000000000006048(24648)
Total Size            0xb030(45104)
------------------------------
GC Heap Size            0xb030(45104)

看红色字体,得知:

“第2代的起始地址是 0x0000000002621000,第0代的起始地址是 0x0000000002621030”。 这里暂时先记下,留着后面还会在看。

切换到主线程, 以便看当前程序堆栈,

~0s

关键时候到了,!clrstack -a,继续看:

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x3f70 (0)
>...省略无关内容...
51
52 00000000002ceef0 000007ff001701e8 System.IO.TextReader+SyncTextReader.ReadLine()
53     PARAMETERS:
54         this = 0x0000000002625a98
55
56 00000000002cef50 000007fee82dc6a2 Test.Program.Main(System.String[])
57     PARAMETERS:
58         args = 0x0000000002623598
59     LOCALS:
60         0x00000000002cef70 = 0x0000000002623658
61         0x00000000002cef78 = 0x0000000002623678

当程序实例化两个Student对象,执行完 Student lee = new Student("lee", "Shanghai") 后,
这里保存了2个对象,嘿嘿:

59     LOCALS:
60         0x00000000002cef70 = 0x0000000002623658
61         0x00000000002cef78 = 0x0000000002623678
看到了,第一个对象的地址是:0x0000000002623658, 而前面说,第0代的起始地址是 0x0000000002621030, 很显然,这两个对象被分配在了0代,且占用了0x20(32)个字节。

不相信,那我们来验明下正身,

0:000> .load C:\Symbols\sosex_64\sosex.dll
0:000> !gcgen 0x0000000002623658
GEN 0

呵呵,还要继续验身么,继续...

! do 0x0000000002623658,

0:000> !do 0x0000000002623658
Name: Test.Student
MethodTable: 000007ff00033538
EEClass: 000007ff001623a8
Size: 32(0x20) bytes
 (D:\Test\PInvoke\CPP\Test\bin\Debug\Test.exe)
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fee7577d90  4000001        8        System.String  0 instance 00000000026235b8 <Name>k__BackingField
000007fee7577d90  4000002       10        System.String  0 instance 00000000026235e0 <Address>k__BackingField

验到了, 就是Test.Student类的实例,Name和Address字段都看到了。

开个小差,看看这个实例的内容,

 !do 00000000026235b8,

0:000> !do 00000000026235b8
Name: System.String
MethodTable: 000007fee7577d90
EEClass: 000007fee717e560
Size: 34(0x22) bytes
 (C:\Windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: wang
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fee757f000  4000096        8         System.Int32  1 instance                5 m_arrayLength
000007fee757f000  4000097        c         System.Int32  1 instance                4 m_stringLength
000007fee75797d8  4000098       10          System.Char  1 instance               77 m_firstChar
000007fee7577d90  4000099       20        System.String  0   shared           static Empty
                                 >> Domain:Value  0000000000bcb1d0:0000000002621308 <<
000007fee7579688  400009a       28        System.Char[]  0   shared           static WhitespaceChars
                                 >> Domain:Value  0000000000bcb1d0:0000000002621a98 <<
0:000> !do 00000000026235e0
Name: System.String
MethodTable: 000007fee7577d90
EEClass: 000007fee717e560
Size: 40(0x28) bytes
 (C:\Windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: Beijing
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fee757f000  4000096        8         System.Int32  1 instance                8 m_arrayLength
000007fee757f000  4000097        c         System.Int32  1 instance                7 m_stringLength
000007fee75797d8  4000098       10          System.Char  1 instance               42 m_firstChar
000007fee7577d90  4000099       20        System.String  0   shared           static Empty
                                 >> Domain:Value  0000000000bcb1d0:0000000002621308 <<
000007fee7579688  400009a       28        System.Char[]  0   shared           static WhitespaceChars
                                 >> Domain:Value  0000000000bcb1d0:0000000002621a98 <<
0:000> !do 00000000026235b8 

果然, 是家住“beijing”的"wang" 同学!

通过这个事例,我们验证了,在.net中,初始分配的小对象在0代上。

那么假设,我们执行GC.Collect(),结果会怎样?

1  static void Main(string[] args)
2         {
3             Student wang = new Student("wang", "Beijing");
4             Student lee = new Student("lee", "Shanghai");
5
6             GC.Collect();
7             Console.ReadLine();
8         }

不一步步看了,给明结果吧:

000000000032eb50 000007fee82dc6a2 Test.Program.Main(System.String[])
    PARAMETERS:
        args = 0x00000000025d3598
    LOCALS:
        0x000000000032eb70 = 0x00000000025d3658
        0x000000000032eb78 = 0x00000000025d3678

0:000> !gcgen 0x00000000025d3658
GEN 1
0:000> !gcgen 0x00000000025d3678
GEN 1

2 Dispose,Finalization(终结器)

Dispose:用于处置那些占用非托管资源的对象。

Finalization(终结器): 这是CLR提供的一种机制,允许对象在GC回收其内存之前执行一些清理工作。

当客户端记得的时候使用IDisposable接口释放你的非受控资源,当客户端忘记的时候防护性地使用终结器(finalizer)。它与垃圾收集器(Garbage Collector)一起工作,确保只在必要的时候该对象才受到与终结器相关的性能影响。这是处理非受控资源的一条很好的途径,因此我们应该彻底地认识它。如果你的类使用了非内存资源,它就必须含有一个终结器。你不能依赖客户端总是C#调用Dispose()方法。因为当它们忘记这样做的时候,你就面临资源泄漏的问题。没有调用Dispose是它们的问题,但是你却有过失。

用于保证非内存资源被正确地释放的唯一途径是创建终结器。

调用Dispose()方法的实现(implementation)负责下面四个事务:

1.释放所有的非受控资源。

2.释放所有的受控资源(包括未解开事件)。

3.设置标志表明该对象已经被处理过了。你必须在自己的公共方法中检查这种状态标志并抛出ObjectDisposed异常(如果某个对象被处理过之后再次被调用的话)。

4.禁止终结操作(finalization)。调用GC.SuppressFinalize(this)来完成这种事务。

Finalization(终结器)原理:

应用程序创建一个新对象时,new操作符会从堆中分配内存。如果这个对象定义了Finalize方法,那么该类型的实例在构造器被调用之前,会将指向该对象的一个指针放到一个finalization list中。finalization list是由GC控制的一个内部数据结构。列表中的每一项都指向一个对象,在回收该对象的内存前,会调用他的Finalize方法。

GC开始时,假定某些对象(如B,C,D对象)被判定位垃圾后,GC会扫描finalization list 以查找指向前述对象(如B,C,D对象)的指针。若发现finalization list有指针指向前述对象(如B,C,D对象)。finalization list会移除指向前述对象(如B,C,D对象)的指针,并把指针追加到Freachable队列。

当垃圾回收器将对象的引用从finalization list移至freachable队列时,对象不再被视为垃圾,其内存不能被回收,即对象“复活”。

然后,GC开始compact可回收内存,特殊的高优先级CLR线程专门负责清空freachable队列,并调用finalize方法。

再次GC被调用时,会发现应用程序的根不再指向它,freachable队列也已经清空。所以,这些对象的内存会被直接回收。

整个过程中,实现Finalization(终结器)的对象需要至少执行两次垃圾回收才能释放其所占内存。(假设对象代龄被提升,则可能多次GC才回收其内存)。

规范的Dispose实现模式:

 1     public class ComplexCleanupBase : IDisposable
 2     {
 3         // some fields that require cleanup
 4         private SafeHandle handle;
 5
 6         private bool disposed = false; // to detect redundant calls
 7
 8         public ComplexCleanupBase()
 9         {
10             // allocate resources
11         }
12
13         protected virtual void Dispose(bool disposing)
14         {
15             if (!disposed)
16             {
17                 if (disposing)
18                 {
19                     if (handle != null)
20                         handle.Dispose();
21                     // dispose-only, i.e. non-finalizable logic
22                 }
23
24                 // shared cleanup logic
25                 disposed = true;
26             }
27         }
28
29         public void Dispose()
30         {
31             Dispose(true);
32             GC.SuppressFinalize(this);
33         }
34
35         ~ComplexCleanupBase()
36         {
37             Dispose(false);
38         }
39     }

了解上述后,来看个问题:

a. 数据库连接,文件连接假如不手动dispose(),资源会被回收么?

回答上面这个问题前,先做个试验,

 1  protected void Button1_Click(object sender, EventArgs e)
 2         {
 3             byte[] b = new byte[] { 1, 2, 3, 4, 5 };
 4             FileStream fs = new FileStream(@"d:\temp.dat", FileMode.Create);
 5             fs.Write(b, 0, b.Length);
 6
 7         }
 8
 9         protected void Button2_Click(object sender, EventArgs e)
10         {
11             GC.Collect();
12         }
13
14         protected void Button3_Click(object sender, EventArgs e)
15         {
16             File.Delete(@"d:\temp.dat");
17         }

Button1,Button2,Button3来回点几下,看看会有什么现象?

连续点击Button1或先点Button1后点Buttion3,第二次都会报错,说明文件连接没有被释放。但是如果点击Button1再点击Button2再点击Button1那么就不会报错了,因为Button2垃圾回收将文件连接关闭了。

这么说来,数据库连接,文件连接假如不手动dispose(),资源也会被GC回收,因为FileStream里的SaveFileHandle实现了Finalize,执行GC,其Finalize方法起了作用。

好强大的GC哦! 只不过,需要等待GC.collect()才能释放这些资源连接,但是呢这些资源开着开销很大很昂贵,所以推荐用完即手动dispose().

这样看规范的Dispose实现模式,能够确保.net资源即使被遗忘关闭,借助垃圾回收机制,也能顺利清理其资源。

好了,有了上面这些基础之后,再来看一个例子:

 1  public class Foo
 2     {
 3         Timer _timer;
 4
 5         public Foo()
 6         {
 7             _timer = new Timer(1000);
 8             _timer.Elapsed += _timer_Elapsed;
 9             _timer.Start();
10         }
11
12         void _timer_Elapsed(object sender, ElapsedEventArgs e)
13         {
14             Console.WriteLine("Tick");
15         }
16     }
17     class Program
18     {
19         static void Main(string[] args)
20         {
21             Foo foo = new Foo();
22             foo = null;
23             Console.ReadLine();
24         }
25     }

注:Timer类来自using System.Timers;
执行发现会出现一连串的 “Tick”。 这里foo=null并未起到作用,Timer资源并未关闭。这里我们不深究为何foo=null不起作用,实际上是因为.net编译做了优化处理,foo=null直接被编译器忽视了。

那怎么办才能做到万事顺利地关闭Timer?

Dispose模式登场,修改后的类如下:

 1   public class Foo : IDisposable
 2     {
 3         Timer _timer;
 4
 5         public Foo()
 6         {
 7             _timer = new Timer(1000);
 8             _timer.Elapsed += _timer_Elapsed;
 9             _timer.Start();
10         }
11
12         void _timer_Elapsed(object sender, ElapsedEventArgs e)
13         {
14             Console.WriteLine("Tick");
15         }
16
17         public void Dispose()
18         {
19             _timer.Dispose();
20         }
21         ~Foo()
22         {
23             Dispose();
24         }
25     }
26     class Program
27     {
28         static void Main(string[] args)
29         {
30             using (Foo foo = new Foo())
31             {
32                 System.Threading.Thread.Sleep(3000);
33             }
34             Console.ReadLine();
35         }
36     }
时间: 2024-11-07 11:17:56

.net垃圾回收机制编程调试试验的相关文章

【学习】012 垃圾回收机制算法分析

垃圾回收机制概述 Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理.由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”.垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存. ps:内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“

Java性能优化之JVM GC(垃圾回收机制)

Java的性能优化,整理出一篇文章,供以后温故知新. JVM GC(垃圾回收机制) 在学习Java GC 之前,我们需要记住一个单词:stop-the-world .它会在任何一种GC算法中发生.stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行.当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成.GC优化很多时候就是减少stop-the-world 的发生. JVM GC回收哪个区域内的垃圾? 需要注意的是,JV

JavaGC专家(1)—深入浅出Java垃圾回收机制

在学习GC之前,你首先应该记住一个单词:"stop-the-world".Stop-the-world会在任何一种GC算法中发生.Stop-the-world意味着 JVM 因为要执行GC而停止了应用程序的执行.当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态,直到GC任务完成.GC优化很多时候就是指减少Stop-the-world发生的时间. 按代的垃圾回收机制 在Java程序中不能显式地分配和注销内存.有些人把相关的对象设置为null或者调用Sy

CMS垃圾回收机制

详解CMS垃圾回收机制 原创不易,未经允许,不得转载~~~ 什么是CMS? Concurrent Mark Sweep. 看名字就知道,CMS是一款并发.使用标记-清除算法的gc. CMS是针对老年代进行回收的GC. CMS有什么用? CMS以获取最小停顿时间为目的. 在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景. CMS如何执行?  总体来说CMS的执行过程可以分为以下几个阶段: 3.1 初始标记(STW) 3.2 并发标记 3.3 并发预清理

Java 垃圾回收机制(早期版本)

Java 垃圾回收机制在我们普通理解来看,应该视为一种低优先级的后台进程来实现的,其实早期版本的Java虚拟机并非以这种方式实现的. 先从一种很简单的垃圾回收方式开始. 引用计数 引用计数是一种简单但是速度很慢的垃圾回收技术. 每个对象都含有要给引用计数器,当有引用连接至对象时,引用计数+1. 当引用离开作用域或者被置为null时,引用计数-1. 当发现某个对象的引用计数为0时,就释放其占用的空间.   这种方法开销在整个程序生命周期中持续发生,并且该方法有个缺陷,如果对象之间存在循环引用,可能

python的垃圾回收机制

进程空间 进程运行时需要在内核中占据一段内存空间,用以存储程序和数据. 每个进程空间分布如下所示: 进程空间的结构 text段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域.在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等. data段:数据段(data segment)通常用来存放程序中已初始化的全局变量数据段属于静态内存分配. bss段:bss(Block Started by Symbol) 通常用来存放程序中未初始化的

垃圾回收机制GC知识再总结兼谈如何用好GC(其他信息: 内存不足)

来源 一.为什么需要GC 应用程序对资源操作,通常简单分为以下几个步骤: 1.为对应的资源分配内存 2.初始化内存 3.使用资源 4.清理资源 5.释放内存 应用程序对资源(内存使用)管理的方式,常见的一般有如下几种: 1.手动管理:C,C++ 2.计数管理:COM 3.自动管理:.NET,Java,PHP,GO- 但是,手动管理和计数管理的复杂性很容易产生以下典型问题: 1.程序员忘记去释放内存 2.应用程序访问已经释放的内存 产生的后果很严重,常见的如内存泄露.数据内容乱码,而且大部分时候,

java JVM垃圾回收机制

Java语言出来之前,大家都在拼命的写C或者C++的程序,而此时存在一个很大的矛盾,C++等语言创建对象要不断的去开辟空间,不用的时候有需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都在重复的allocated,然后不停的~析构.于是,有人就提出,能不能写一段程序在实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢? 1960年 基于MIT的Lisp首先提出了垃圾回收的概念,用于处理C语言等不停的析构操作,而这时Java还没有出世呢!所以实际上GC并不是Jav

Java垃圾回收机制的工作原理

Java垃圾回收机制的工作原理 [博主]高瑞林 [博客地址]http://www.cnblogs.com/grl214 一.Java中引入垃圾回收机制的作用 当我们建完类之后,创建对象的同时,进行内存空间的分配,为了防止内存空间爆满,java引入了垃圾回收机制,将不再引用的对象进行回收,释放内存,循环渐进,从而防止内存空间不被爆满. 1.垃圾回收机制的工作原理 创建的对象存储在堆里面,把堆比喻为院子中的土地,把对象比喻为土地的管理者,院子比喻为java虚拟机,当创建一个对象时,java虚拟机将给