C#技术漫谈之垃圾回收机制(GC)(转)

更新时间:2023-07-12 22:33:21 阅读: 评论:0

C#技术漫谈之垃圾回收机制(GC)(转)
GC的前世与今⽣
  虽然本⽂是以作为⽬标来讲述GC,但是GC的概念并⾮才诞⽣不久。早在1958年,由⿍⿍⼤名的图林奖得主John McCarthy所实现的Lisp语⾔就已经提供了GC的功能,这是GC的第⼀次出现。Lisp的程序员认为内存管理太重要了,所以不能由程序员⾃⼰来管理。
  但后来的⽇⼦⾥Lisp却没有成⽓候,采⽤内存⼿动管理的语⾔占据了上风,以C为代表。出于同样的理由,不同的⼈却⼜不同的看
法,C程序员认为内存管理太重要了,所以不能由系统来管理,并且讥笑Lisp程序慢如乌龟的运⾏速度。的确,在那个对每⼀个Byte都要精⼼计算的年代GC的速度和对系统资源的⼤量占⽤使很多⼈的⽆法接受。⽽后,1984年由Dave Ungar开发的Smalltalk语⾔第⼀次采⽤了Generational garbage collection的技术(这个技术在下⽂中会谈到),但是Smalltalk也没有得到⼗分⼴泛的应⽤。
  直到20世纪90年代中期GC才以主⾓的⾝份登上了历史的舞台,这不得不归功于Java的进步,今⽇的GC已⾮吴下阿蒙。Java采⽤
VM(Virtual Machine)机制,由VM来管理程序的运⾏当然也包括对GC管理。90年代末期出现了,
采⽤了和Java类似的⽅法由CLR(Common Language Runtime)来管理。这两⼤阵营的出现将⼈们引⼊了以虚拟平台为基础的开发时代,GC也在这个时候越来越得到⼤众的关注。
  为什么要使⽤GC呢?也可以说是为什么要使⽤内存⾃动管理?有下⾯的⼏个原因:
  1、提⾼了软件开发的抽象度;
  2、程序员可以将精⼒集中在实际的问题上⽽不⽤分⼼来管理内存的问题;
  3、可以使模块的接⼝更加的清晰,减⼩模块间的偶合;
  4、⼤⼤减少了内存⼈为管理不当所带来的Bug;
  5、使内存管理更加⾼效。
  总的说来就是GC可以使程序员可以从复杂的内存问题中摆脱出来,从⽽提⾼了软件开发的速度、质量和安全性。
  什么是GC
  GC如其名,就是垃圾收集,当然这⾥仅就内存⽽⾔。Garbage Collector(垃圾收集器,在不⾄于混
淆的情况下也成为GC)以应⽤程序的root为基础,遍历应⽤程序在Heap上动态分配的所有对象[2],通过识别它们是否被引⽤来确定哪些对象是已经死亡的、哪些仍需要被使⽤。已经不再被应⽤程序的root或者别的对象所引⽤的对象就是已经死亡的对象,即所谓的垃圾,需要被回收。这就是GC⼯作的原理。为了实现这个原理,GC有多种算法。⽐较常见的算法有Reference Counting,Mark Sweep,Copy Collection等等。⽬前主流的虚拟系统 CLR,Java VM和Rotor都是采⽤的Mark Sweep算法。
  ⼀、Mark-Compact 标记压缩算法
  简单地把的GC算法看作Mark-Compact算法。阶段1: Mark-Sweep 标记清除阶段,先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的;阶段2: Compact 压缩阶段,对象回收之后heap 内存空间变得不连续,在heap中移动这些对象,使他们重新从heap基地址开始连续排列,类似于磁盘空间的碎⽚整理。
  Heap内存经过回收、压缩之后,可以继续采⽤前⾯的heap内存分配⽅法,即仅⽤⼀个指针记录heap分配的起始地址就可以。主要处理步骤:将线程挂起→确定roots→创建reachable objects graph→对象回收→heap压缩→指针修复。可以这样理解roots:heap中对象的引⽤关系错综复杂(交叉引⽤、循环引⽤),形成复杂的graph,roots是CLR在heap之外可以找到的各种⼊⼝点。
  GC搜索roots的地⽅包括全局对象、静态变量、局部对象、函数调⽤参数、当前CPU寄存器中的对象指针(还有finalization queue)等。主要可以归为2种类型:已经初始化了的静态变量、线程仍在使⽤的对象(stack+CPU register)。 Reachable objects:指根据对象引⽤关系,从roots出发可以到达的对象。例如当前执⾏函数的局部变量对象A是⼀个root object,他的成员变量引⽤了对象B,则B是⼀个reachable object。从roots出发可以创建reachable objects graph,剩余对象即为unreachable,可以被回收。
  指针修复是因为compact过程移动了heap对象,对象地址发⽣变化,需要修复所有引⽤指针,包括stack、CPU register中的指针以及heap中其他对象的引⽤指针。Debug和relea执⾏模式之间稍有区别,relea模式下后续代码没有引⽤的对象是unreachable的,⽽debug模式下需要等到当前函数执⾏完毕,这些对象才会成为unreachable,⽬的是为了调试时跟踪局部对象的内容。传给了COM+的托管对象也会成为root,并且具有⼀个引⽤计数器以兼容COM+的内存管理机制,引⽤计数器为0时,这些对象才可能成为被回收对象。Pinned objects指分配之后不能移动位置的对象,例如传递给⾮托管
代码的对象(或者使⽤了fixed关键字),GC在指针修复时⽆法修改⾮托管代码中的引⽤指针,因此将这些对象移动将发⽣异常。pinned objects 会导致heap出现碎⽚,但⼤部分情况来说传给⾮托管代码的对象应当在GC时能够被回收掉。     
  ⼆、 Generational 分代算法
  程序可能使⽤⼏百M、⼏G的内存,对这样的内存区域进⾏GC操作成本很⾼,分代算法具备⼀定统计学基础,对GC的性能改善效果⽐较明显。将对象按照⽣命周期分成新的、⽼的,根据统计分布规律所反映的结果,可以对新、⽼区域采⽤不同的回收策略和算法,加强对新区域的回收处理⼒度,争取在较短时间间隔、较⼩的内存区域内,以较低成本将执⾏路径上⼤量新近抛弃不再使⽤的局部对象及时回收掉。分代算法的假设前提条件:
  1、⼤量新创建的对象⽣命周期都⽐较短,⽽较⽼的对象⽣命周期会更长;
  2、对部分内存进⾏回收⽐基于全部内存的回收操作要快;
  3、新创建的对象之间关联程度通常较强。heap分配的对象是连续的,关联度较强有利于提⾼CPU cache的命中率,将heap分成3个代龄区域: Gen 0、Gen 1、Gen 2;
  Heap分为3个代龄区域,相应的GC有3种⽅式: # Gen 0 collections, # Gen 1 collections, #Gen 2 collections。如果Gen 0 heap内存达到阀值,则触发0代GC,0代GC后Gen 0中幸存的对象进⼊Gen1。如果Gen 1的内存达到阀值,则进⾏1代GC,1代GC将Gen 0 heap和Gen 1 heap⼀起进⾏回收,幸存的对象进⼊Gen2。
隆庆帝
  2代GC将Gen 0 heap、Gen 1 heap和Gen 2 heap⼀起回收,Gen 0和Gen 1⽐较⼩,这两个代龄加起来总是保持在16M左右;Gen2的⼤⼩由应⽤程序确定,可能达到⼏G,因此0代和1代GC的成本⾮常低,2代GC称为full GC,通常成本很⾼。粗略的计算0代和1代GC应当能在⼏毫秒到⼏⼗毫秒之间完成,Gen 2 heap⽐较⼤时,full GC可能需要花费⼏秒时间。⼤致上来讲应⽤运⾏期间,2代、1代和0代GC 的频率应当⼤致为1:10:100。
  三、Finalization Queue和Freachable Queue
  这两个队列和对象所提供的Finalize⽅法有关。这两个队列并不⽤于存储真正的对象,⽽是存储⼀组指向对象的指针。当程序中使⽤了new操作符在Managed Heap上分配空间时,GC会对其进⾏分析,如果该对象含有Finalize⽅法则在Finalization Queue中添加⼀个指向该对象的指针。初一数学下册
  在GC被启动以后,经过Mark阶段分辨出哪些是垃圾。再在垃圾中搜索,如果发现垃圾中有被Finalization Queue中的指针所指向的对象,则将这个对象从垃圾中分离出来,并将指向它的指针移动到Freachable Queue中。这个过程被称为是对象的复⽣(Resurrection),本来死去的对象就这样被救活了。为什么要救活它呢?因为这个对象的Finalize⽅法还没有被执⾏,所以不能让它死去。Freachable Queue平时不做什么事,但是⼀旦⾥⾯被添加了指针之后,它就会去触发所指对象的Finalize⽅法执⾏,之后将这个指针从队列中剔除,这是对象就可以安静的死去了。
  Framework的System.GC类提供了控制Finalize的两个⽅法,ReRegisterForFinalize和SuppressFinalize。前者是请求系统完成对象的Finalize⽅法,后者是请求系统不要完成对象的Finalize⽅法。ReRegisterForFinalize⽅法其实就是将指向对象的指针重新添加到Finalization Queue中。这就出现了⼀个很有趣的现象,因为在Finalization Queue中的对象可以复⽣,如果在对象的Finalize⽅法中调⽤ReRegisterForFinalize⽅法,这样就形成了⼀个在堆上永远不会死去的对象,像凤凰涅槃⼀样每次死的时候都可以复⽣。
  托管资源:
  中的所有类型都是(直接或间接)从System.Object类型派⽣的。
  CTS中的类型被分成两⼤类——引⽤类型(reference type,⼜叫托管类型[managed type]),分配在内存堆上;值类型(value type),分配在堆栈上。如图:
  值类型在栈⾥,先进后出,值类型变量的⽣命有先后顺序,这个确保了值类型变量在退出作⽤域以前会释放资源。⽐引⽤类型更简单和⾼效。堆栈是从⾼地址往低地址分配内存。
  引⽤类型分配在托管堆(Managed Heap)上,声明⼀个变量在栈上保存,当使⽤new创建对象时,会把对象的地址存储在这个变量⾥。托管堆相反,从低地址往⾼地址分配内存,如图:
  中超过80%的资源都是托管资源。
  ⾮托管资源: 
  ApplicationContext, Brush, Component, ComponentDesigner, Container, Context, Cursor, FileStream, Font, Icon, Image, Matrix, Object, OdbcDataReader, OleDBDataReader, Pen, Regex, Socket, StreamWriter, Timer, Tooltip, ⽂件句柄, GDI资源, 数据库连接等等资源。可能在使⽤的时候很多都没有注意到!
  的GC机制有这样两个问题:
  ⾸先,GC并不是能释放所有的资源。它不能⾃动释放⾮托管资源。
  第⼆,GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。
  GC并不是实时性的,这会造成系统性能上的瓶颈和不确定性。所以有了IDisposable接⼝,IDisposable接⼝定义了Dispo⽅法,这个⽅法⽤来供程序员显式调⽤以释放⾮托管资源。使⽤using语句可以简化资源管理。
  ⽰例:
///summary
/// 执⾏SQL语句,返回影响的记录数
////summary
///param name="SQLString"SQL语句/param
///returns影响的记录数/returns
publicstaticint ExecuteSql(string SQLString)
{
using (SqlConnection connection =new SqlConnection(connectionString))
{
using (SqlCommand cmd =new SqlCommand(SQLString, connection))
{
try
{
connection.Open();
int rows = cmd.ExecuteNonQuery();
return rows;
}
catch (System.Data.SqlClient.SqlException e)
{
connection.Clo();
throw e;
}
finally
{
cmd.Dispo();
connection.Clo();
}
}
}
}
  当你⽤Dispo⽅法释放未托管对象的时候,应该调⽤GC.SuppressFinalize。如果对象正在终结队列(finalization queue),
GC.SuppressFinalize会阻⽌GC调⽤Finalize⽅法。因为Finalize⽅法的调⽤会牺牲部分性能。如果你的Dispo⽅法已经对委托管资源作了清理,就没必要让GC再调⽤对象的Finalize⽅法(MSDN)。附上MSDN的代码,⼤家可以参考。
publicclass BaResource : IDisposable
{
// 指向外部⾮托管资源
private IntPtr handle;
// 此类使⽤的其它托管资源.
private Component Components;
/
/ 跟踪是否调⽤.Dispo⽅法,标识位,控制垃圾收集器的⾏为
privatebool dispod =fal;
// 构造函数
public BaResource()
{
// Inrt appropriate constructor code here.
}三七粉正确吃法
// 实现接⼝IDisposable.
// 不能声明为虚⽅法virtual.
// ⼦类不能重写这个⽅法.
publicvoid Dispo()
{
Dispo(true);
脑门大// 离开终结队列Finalization queue
// 设置对象的阻⽌终结器代码
//
GC.SuppressFinalize(this);
}
// Dispo(bool disposing) 执⾏分两种不同的情况.
// 如果disposing 等于 true, ⽅法已经被调⽤
// 或者间接被⽤户代码调⽤. 托管和⾮托管的代码都能被释放
// 如果disposing 等于fal, ⽅法已经被终结器 finalizer 从内部调⽤过,
无牌驾驶怎么处罚
/
/你就不能在引⽤其他对象,只有⾮托管资源可以被释放。
protectedvirtualvoid Dispo(bool disposing)
{
// 检查Dispo 是否被调⽤过.
if (!this.dispod)
{
// 如果等于true, 释放所有托管和⾮托管资源
if (disposing)
{
// 释放托管资源.
Components.Dispo();
}
// 释放⾮托管资源,如果disposing为 fal,
// 只会执⾏下⾯的代码.
CloHandle(handle);
handle = IntPtr.Zero;
// 注意这⾥是⾮线程安全的.
丑的近义词// 在托管资源释放以后可以启动其它线程销毁对象,
// 但是在dispod标记设置为true前
// 如果线程安全是必须的,客户端必须实现。
}
dispod =true;
}
// 使⽤interop 调⽤⽅法
// 清除⾮托管资源.
[System.Runtime.InteropServices.DllImport("Kernel32")]
privateexternstatic Boolean CloHandle(IntPtr handle);
// 使⽤C# 析构函数来实现终结器代码
// 这个只在Dispo⽅法没被调⽤的前提下,才能调⽤执⾏。
// 如果你给基类终结的机会.安徽省旅游景点
// 不要给⼦类提供析构函数.
~BaResource()
{
/
/ 不要重复创建清理的代码.
// 基于可靠性和可维护性考虑,调⽤Dispo(fal) 是最佳的⽅式
Dispo(fal);
}
// 允许你多次调⽤Dispo⽅法,
// 但是会抛出异常如果对象已经释放。
// 不论你什么时间处理对象都会核查对象的是否释放,
// check to e if it has been dispod.
publicvoid DoSomething()黄山门
{
if (this.dispod)
{
thrownew ObjectDispodException();
}
}
// 不要设置⽅法为virtual.
// 继承类不允许重写这个⽅法
publicvoid Clo()
{
// ⽆参数调⽤Dispo参数.
Dispo();
}
publicstaticvoid Main()
{
// Inrt code here to create
// and u a BaResource object.
}
}
  GC.Collect() ⽅法
  作⽤:强制进⾏垃圾回收。
  GC的⽅法:
名称说明
Collect()强制对所有代进⾏即时垃圾回收。
Collect(Int32)强制对零代到指定代进⾏即时垃圾回收。
Collect(Int32, GCCollectionMode)强制在 GCCollectionMode 值所指定的时间对零代到指定代进⾏垃圾回收
  GC注意事项:
  1、只管理内存,⾮托管资源,如⽂件句柄,GDI资源,数据库连接等还需要⽤户去管理。
  2、循环引⽤,⽹状结构等的实现会变得简单。GC的标志-压缩算法能有效的检测这些关系,并将不再被引⽤的⽹状结构整体删除。
  3、GC通过从程序的根对象开始遍历来检测⼀个对象是否可被其他对象访问,⽽不是⽤类似于COM中的引⽤计数⽅法。
  4、GC在⼀个独⽴的线程中运⾏来删除不再被引⽤的内存。
  5、GC每次运⾏时会压缩托管堆。
  6、你必须对⾮托管资源的释放负责。可以通过在类型中定义Finalizer来保证资源得到释放。
  7、对象的Finalizer被执⾏的时间是在对象不再被引⽤后的某个不确定的时间。注意并⾮和C++中⼀样在对象超出声明周期时⽴即执⾏析构函数
  8、Finalizer的使⽤有性能上的代价。需要Finalization的对象不会⽴即被清除,⽽需要先执⾏Finalizer.Finalizer,不是在GC执⾏的线程被调⽤。GC把每⼀个需要执⾏Finalizer的对象放到⼀个队列中去,然后启动另⼀个线程来执⾏所有这些Finalizer,⽽GC线程继续去删除其他待回收的对象。在下⼀个GC周期,这些执⾏完Finalizer的对象的内存才会被回收。
  9、 GC使⽤"代"(generations)的概念来优化性能。代帮助GC更迅速的识别那些最可能成为垃圾的对象。在上次执⾏完垃圾回收后新创建的对象为第0代对象。经历了⼀次GC周期的对象为第1代对象。经历了两次或更多的GC周期的对象为第2代对象。代的作⽤是为了区分局部变量和需要在应⽤程序⽣存周期中⼀直存活的对象。⼤部分第0代对象是局部变量。成员变量和全局变量很快变成第1代对象并最终成为第2代对象。
  10、GC对不同代的对象执⾏不同的检查策略以优化性能。每个GC周期都会检查第0代对象。⼤约1/10的GC周期检查第0代和第1代对象。⼤约1/100的GC周期检查所有的对象。重新思考Finalization的代价:需要Finalization的对象可能⽐不需要Finalization在内存中停留额外9个GC周期。如果此时它还没有被Finalize,就变成第2代对象,从⽽在内存中停留更长时间。

本文发布于:2023-07-12 22:33:21,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/82/1093253.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:对象   回收   托管   内存   资源   垃圾   指针   释放
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图