目录
1.前言
上一篇了解了C#的两大数据类型--值类型和引用类型以及它们的内存管理相关内容,本篇根据上篇内存管理进一步了解一下其垃圾回收机制
2.垃圾
.Net类型分为两大类,一个就是值类型,另一个就是引用类型。前者是分配在栈上,随着函数的执行作用域执行完毕而自动出栈,并不需要GC回收;后者是分配在堆上(Heap),因此它的内存释放和回收需要通过GC来完成。一个引用类型对象所占的内存需要被GC回收,而满足回收的条件,首先就要需要称为垃圾。那么.Net如果判定一个引用类型对象是垃圾呢,.Net的判断很简单,只要判定此对象或者其包含的子对象没有任何引用是有效的,那么系统就认为它是垃圾。
2.为什么要使用GC
1、提高了软件开发的抽象度;
2、程序员可以将精力集中在实际的问题上而不用分心来管理内存的问题;
3、可以使模块的接口更加的清晰,减小模块间的偶合;
4、大大减少了内存人为管理不当所带来的Bug;
5、使内存管理更加高效。
总的说来就是GC可以使程序员可以从复杂的内存问题中摆脱出来,从而提高了软件开发的速度、质量和安全性。
3.垃圾回收机制GC
1.简介
C#中和Java一样是一种系统自动回收释放资源的语言,在C#环境中通过 GC(Garbage Collect)进行系统资源回收.GC垃圾回收主要是是指保存在Heap上的资源。
.NET的GC机制有这样两个问题:
首先,GC并不是能释放所有的资源。它不能自动释放非托管资源。
第二,GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。GC的清理频率程序员无法决定,CLR会自动控制。当一个对象标记为垃圾的时候,这个对象不一定会被立即回收。
GC并不是实时性的,这会造成系统性能上的瓶颈和不确定性。所以有了IDisposable接口,IDisposable接口定义了Dispose方法,这个方法用来供程序员显式调用以释放非托管资源。使用using语句可以简化资源管理。
2.代
垃圾收集器通过分代支持对象的年龄化是推荐的但不是必须的。一代在内存里是一个具有相对年龄的对象的单位。对象的代号或年龄或年龄标识对象属于那个分代。在应用程序的生命周期里,越近创建的对象属于越新的代,并且比早创建的对象具有较低的分代号。最近分代里的对象代号是0.
在new对象时,要先搜索空闲链表,找到最适合内存块,分配,调整内存块链表,合并碎片。new操作几乎可以在O(1)的时间完成,把堆顶指针加1。工作原理是: 当托管堆上剩余空间不足,或者Generator 0 的空间已满的时候GC运行,开始回收内存。垃圾回收的开始,GC对堆内存的压缩调整,对象集中到顶部。GC在扫描垃圾的时候会占用一定的CPU时间片的,最初的GC算法真的是扫描整个堆,效率低。现在的GC把堆中的对象分成3代,最近进入堆的是第0代(generation 0), 其次是generation 1, generation2. 第一次GC只扫描第0代。如果回收的空间足够当前使用就不必扫描其它generation的对象。具体过程:当程序运行时,有对象需要存储在堆里面,GC就会创建第0代(假设空间大小为256K),对象就会存储在第0代里面,当程序继续运行,运行到第0代的大小不足以存放对象,这时候就就会创建第1代(假设空间为10M),GC就会把第0代里面的“垃圾对象”清理掉,把“活着”的对象放在第1代,这时候第0代就空了,用于存放新来的对象,当第0代满了的时候,就会继续执行以上操作,随着程序的运行,第1代不能满足存放要求,这时候就会创建第2代,清理方式如上相同。所以,GC创建对象的效率比C++高效,不需要扫描全部堆空间。它通过扫描策略,再加上内存管理策略带来的性能提升,足以补偿GC所占用的CPU时间。
GC进行垃圾回收是系统决定的,下面是进行强制回收的执行代码(非特殊情况下不要使用此方法,会影响系统效率,削弱垃圾回收器中优化引擎的作用,而垃圾回收器可以确定运行垃圾回收的最佳时间)
//对所有代进行垃圾回收。
GC.Collect();
//对指定的代进行垃圾回收。
GC.Collect(int generation);
//强制在 System.GCCollectionMode 值所指定的时间对零代到指定代进行垃圾回收。
GC.Collect(int generation, GCCollectionMode mode);
3.托管资源与非托管资源
托管资源:是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源。托管资源的回收工作是不需要人工干预的,有.NET运行库在合适调用垃圾回收器进行回收。
非托管资源:.NET不知道如何回收的资源,最常见的一类非托管资源是包装操作系统资源的对象,例如文件,窗口,网络连接,数据库连接,画刷,图标等。这类资源,垃圾回收器在清理的时候会调用Object.Finalize()方法。默认情况下,方法是空的,对于非托管对象,需要在此方法中编写回收非托管资源的代码,以便垃圾回收器正确回收资源。
4.对于非托管资源的释放方法
.Net提供可三种方法,也是最常见的三种,大致如下
1、析构函数
public class cat
{
~cat()
{
//析构函数语法
}
}
构造函数可以在创建对象实例的时候执行某些操作,析构函数正好相反是资源创建以后被系统回收的时候执行的操作,垃圾回收器在回收对象之前会调用析构函数,所以在函数代码块中可以写释放非托管资源的代码。析构函数没有返回值,没有参数,没有修饰符。
对于内存中的垃圾分为两种,一种需要调用对象的析构函数,另一种是不需要调用的。对于需要调用析构函数的回收需要通过两步完成,第一步是调用对象的析构函数,第二步是回收内存,但是要注意这两步不是在GC一次轮询完成,即需要两次轮询;
析构函数只能被GC来调用,那么无法确定它什么时候被调用,因此用它作为资源的释放并不是很合理,因为资源释放不及时;但是为了防止资源泄露,毕竟它会被GC调用,因此析构函数可以作为一个补救方法。
2、继承IDisposable接口,实现Dispose方法;
3、Close方法
对于dispose方法和Close方法自己用过最多的就是在数据连接上
而Close与Dispose这两种方法的区别在于,调用完了对象的Close方法后,此对象有可能被重新进行使用;而Dispose方法来说,此对象所占用的资源需要被标记无用了,也就是此对象被销毁了,等待GC回收,不能再被使用。
/// <summary>
/// 执行查询,返回要查询的结果集
/// </summary>
/// <param name="sql">sql语句</param>
/// <param name="par_CommandType">sql语句的类型</param>
/// <param name="par_SqlParameter">参数的类型</param>
/// <returns>返回结果集</returns>
public DataTable ExecuteTable(string sql,CommandType par_CommandType,params System.Data.SqlClient.SqlParameter [] par_SqlParameter)
{
DataTable datatable = new DataTable("dt");
using(SqlConnection conn=new SqlConnection(connString))
{
using(SqlCommand command=new SqlCommand(sql,conn))
{
try
{
//数据库连接
if(conn.State!=ConnectionState.Open)
{
conn.Open();
}
//sql语句类型的设置
command.CommandType = par_CommandType;
command.CommandTimeout = 36000;
//如果传入了参数,把其添加到命令对象中
if(par_SqlParameter!=null)
{
for(int i=command.Parameters.Count-1;i>=0;i--)
{
command.Parameters.RemoveAt(i);
}
command.Parameters.AddRange(par_SqlParameter);
}
//创建一个适配器对象,用以将结果填充到datatable中
SqlDataAdapter adapt = new SqlDataAdapter(command);
adapt.Fill(datatable);
}
catch(SqlException e)
{
throw new Exception(e.Message);
}
finally
{
command.Parameters.Clear();
command.Dispose();
conn.Close();
}
}
}
return datatable;
}
5.通过代码感受三种方法的区别
public class Program
{
static void Main(string[] args)
{
// Show destructor
new Program().Create();
Console.WriteLine("After created!");
new Program().CallGC();
Console.ReadKey();
}
private void Create()
{
DisposeClass myClass = new DisposeClass();
}
private void CallGC()
{
GC.Collect();
}
}
public class DisposeClass : IDisposable
{
public void Close()
{
Console.WriteLine("Close called!");
}
~DisposeClass()
{
Console.WriteLine("Destructor called!");
}
#region IDisposable Members
public void Dispose()
{
Console.WriteLine("Dispose called!");
}
#endregion
}
当调用了Create方法后并没有直接调用析构函数,而是等显示调用GC.Collect才被调用。
static void Main(string[] args)
{
using (DisposeClass myClass = new DisposeClass())
{
//other operation here
}
Console.ReadKey();
}
}
public class DisposeClass : IDisposable
{
public void Close()
{
Console.WriteLine("Close called!");
}
~DisposeClass()
{
Console.WriteLine("Destructor called!");
}
#region IDisposable Members
public void Dispose()
{
Console.WriteLine("Dispose called!");
}
#endregion
}
我在代码中添加了Console.WriteLine(),所以只看到了一行显示,其实在此刻代码并没有运行完毕,若去掉这一行代码,进程结束前还会运行析构函数。
对于Dispose来说,也需要显示的调用,但是对于继承了IDisposable的类型对象可以使用using这个关键字,这样对象的Dispose方法在出了using范围后会被自动调用。
那么对于如上DisposeClass类型的Dispose实现来说,事实上GC还需要调用对象的析构函数,正常来说Dispose函数,应该通过调用GC.SuppressFinalize(this )来告诉GC,让它不用再调用对象的析构函数中。那么改写后的DisposeClass如下:
public class DisposeClass : IDisposable
{
public void Close()
{
Debug.WriteLine("Close called!");
}
~DisposeClass()
{
Debug.WriteLine("Destructor called!");
}
#region IDisposable Members
public void Dispose()
{
Debug.WriteLine("Dispose called!");
//不在执行析构函数
GC.SuppressFinalize( this );
}
#endregion
}
参考
https://www.jb51.net/article/41819.htm
https://blog.csdn.net/u011555996/article/details/102554037
https://www.cnblogs.com/qtiger/p/11177157.html
https://blog.csdn.net/chenyu964877814/article/details/7461948