首页 技术 正文
技术 2022年11月16日
0 收藏 764 点赞 3,902 浏览 7624 个字

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 队列里的对象

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

 public class Student
{
public string Name { get; set; }
public string Address { get; set; }
public Student(string name, string address)
{
Name = name;
Address = address;
}
}
class Program
{
static void Main(string[] args)
{
Student wang = new Student("wang", "Beijing");
Student lee = new Student("lee", "Shanghai"); //GC.Collect();
Console.ReadLine();
}
}

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,继续看:

 :> !clrstack -a
OS Thread Id: 0x3f70 ()
>...省略无关内容... 00000000002ceef0 000007ff001701e8 System.IO.TextReader+SyncTextReader.ReadLine()
PARAMETERS:
this = 0x0000000002625a98 00000000002cef50 000007fee82dc6a2 Test.Program.Main(System.String[])
PARAMETERS:
args = 0x0000000002623598
LOCALS:
0x00000000002cef70 = 0x0000000002623658
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,

:> !do 0x0000000002623658
Name: Test.Student
MethodTable: 000007ff00033538
EEClass: 000007ff001623a8
Size: (0x20) bytes
(D:\Test\PInvoke\CPP\Test\bin\Debug\Test.exe)
Fields:
MT Field Offset Type VT Attr Value Name
000007fee7577d90 System.String instance 00000000026235b8 <Name>k__BackingField
000007fee7577d90 System.String 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 <<
:> !do 00000000026235e0
Name: System.String
MethodTable: 000007fee7577d90
EEClass: 000007fee717e560
Size: (0x28) bytes
(C:\Windows\assembly\GAC_64\mscorlib\2.0..0__b77a5c561934e089\mscorlib.dll)
String: Beijing
Fields:
MT Field Offset Type VT Attr Value Name
000007fee757f000 System.Int32 instance m_arrayLength
000007fee757f000 c System.Int32 instance m_stringLength
000007fee75797d8 System.Char instance m_firstChar
000007fee7577d90 System.String shared static Empty
>> Domain:Value 0000000000bcb1d0: <<
000007fee7579688 400009a System.Char[] shared static WhitespaceChars
>> Domain:Value 0000000000bcb1d0:0000000002621a98 <<
:> !do 00000000026235b8

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

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

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

  static void Main(string[] args)
{
Student wang = new Student("wang", "Beijing");
Student lee = new Student("lee", "Shanghai"); GC.Collect();
Console.ReadLine();
}

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

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

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实现模式:

     public class ComplexCleanupBase : IDisposable
{
// some fields that require cleanup
private SafeHandle handle; private bool disposed = false; // to detect redundant calls public ComplexCleanupBase()
{
// allocate resources
} protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
if (handle != null)
handle.Dispose();
// dispose-only, i.e. non-finalizable logic
} // shared cleanup logic
disposed = true;
}
} public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
} ~ComplexCleanupBase()
{
Dispose(false);
}
}

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

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

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

  protected void Button1_Click(object sender, EventArgs e)
{
byte[] b = new byte[] { , , , , };
FileStream fs = new FileStream(@"d:\temp.dat", FileMode.Create);
fs.Write(b, , b.Length); } protected void Button2_Click(object sender, EventArgs e)
{
GC.Collect();
} protected void Button3_Click(object sender, EventArgs e)
{
File.Delete(@"d:\temp.dat");
}

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

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

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

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

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

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

  public class Foo
{
Timer _timer; public Foo()
{
_timer = new Timer();
_timer.Elapsed += _timer_Elapsed;
_timer.Start();
} void _timer_Elapsed(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Tick");
}
}
class Program
{
static void Main(string[] args)
{
Foo foo = new Foo();
foo = null;
Console.ReadLine();
}
}

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

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

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

   public class Foo : IDisposable
{
Timer _timer; public Foo()
{
_timer = new Timer();
_timer.Elapsed += _timer_Elapsed;
_timer.Start();
} void _timer_Elapsed(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Tick");
} public void Dispose()
{
_timer.Dispose();
}
~Foo()
{
Dispose();
}
}
class Program
{
static void Main(string[] args)
{
using (Foo foo = new Foo())
{
System.Threading.Thread.Sleep();
}
Console.ReadLine();
}
}
相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,121
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,593
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,438
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,209
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,845
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,930