Index :html
(1)类型语法、内存管理和垃圾回收基础程序员
(2)面向对象的实现和异常的处理面试
(3)字符串、集合与流算法
(4)委托、事件、反射与特性数据库
(5)多线程开发基础数组
(6)ADO.NET与数据库开发基础缓存
(7)WebService的开发与应用基础安全
在.NET中全部的内建类型都继承自System.Object类型。在C#中,不须要显示地定义类型继承自System.Object,编译器将自动地自动地为类型添加上这个继承申明,如下两行代码的做用彻底一致:网络
public class A { } public class A : System.Object { }
在.NET中的类型分为值类型和引用类型,它们各有特色,其共同点是都继承自System.Object,但最明显的区分标准倒是是否继承自System.ValueType(System.ValueType继承自System.Object),也就是说全部继承自System.ValueType的类型是值类型,而其余类型都是引用类型。经常使用的值类型包括:结构、枚举、整数型、浮点型、布尔型等等;而在C#中全部以class关键字定义的类型都是引用类型。数据结构
PS:严格来说,System.Object做为全部内建类型的基类,自己并无值类型和引用类型之分。可是System.Object的对象,具备引用类型的特色。这也是值类型在某些场合须要装箱和拆箱操做的缘由。
(1)赋值时的区别
这是值类型与引用类型最显著的一个区别:值类型的变量直接将得到一个真实的数据副本,而对引用类型的赋值仅仅是把对象的引用赋给变量,这样就可能致使多个变量引用到一个对象实例上。
(2)内存分配的区别
引用类型的对象将会在堆上分配内存,而值类型的对象则会在堆栈上分配内存。堆栈空间相对有限,可是运行效率却比堆高不少。
(3)继承结构的区别
因为全部的值类型都有一个共同的基类System.ValueType,所以值类型具备了一些引用类型所不具备的共同性质,比较重要的一点就是值类型的比较方法:Equals。全部的值类型已经实现了内容的比较(而再也不是引用地址的比较),而引用类型没有重写Equals方法仍是采用引用比较。
(1)装箱:CLR须要作额外的工做把堆栈上的值类型移动到堆上,这个操做就被称为装箱。
(2)拆箱:装箱操做的反操做,把堆中的对象复制到堆栈中,而且返回其值。
装箱和拆箱都意味着堆和堆栈空间的一系列操做,毫无疑问,这些操做的性能代价是很大的,尤为对于堆上空间的操做,速度相对于堆栈的操做慢得多,而且可能引起垃圾回收,这些都将大规模地影响系统的性能。所以,咱们应该避免任何没有必要的装箱和拆箱操做。
如何避免呢,首先分析装箱和拆箱常常发生的场合:
①值类型的格式化输出
②System.Object类型的容器
对于第①种状况,咱们能够经过下面的改动示例来避免:
int i = 10; Console.WriteLine("The value is {0}", i.ToString());
对于第②种状况,则可使用泛型技术来避免使用针对System.Object类型的容器,有效避免大规模地使用装箱和拆箱:
ArrayList arrList = new ArrayList(); arrList.Add(0); arrList.Add("1"); // 使用泛型数据结构代替ArrayList List<int> intList = new List<int>(); intList.Add(1); intList.Add(2);
首先,struct(结构)是值类型,而class(类)是引用类型,全部的结构对象都分配在堆栈上,而全部的类对象都分配在堆上。
其次,struct与class相比,不具有继承的特性,struct虽然能够重写定义在System.Object中的虚方法,但不能定义新的虚方法和抽象方法。
最后,struct不能有无参数的构造方法(class默认就有),也不能为成员变量定义初始值。
public struct A { public int a = 1; // 这里不能编译经过 }
结构对象在构造时必须被初始化为0,构造一个全0的对象是指在内存中为对象分配一个合适的空间,而且把该控件置为0。
如何使用struct or class?当一个类型仅仅是原始数据的集合,而不须要复杂的操做时,就应该设计为struct,不然就应该设计为一个class。
(1)ref关键字:引用传递参数,须要在传递前初始化;(ref 要求参数在传入前被初始化)
(2)out关键字:引用传递参数,须要在返回前初始化;(out 要求参数在方法返回前被初始化)
ref和out这两个关键字的功能极其相似,都用来讲明该参数以引用方式进行传递。你们都知道,.NET的类型分为引用类型和值类型,当一个方法参数是引用类型时,传递的本质就是对象的引用。因此,这两个关键字的做用都发生在值类型上。
(3)params关键字:容许方法在定义时不肯定参数的数量,这种形式很是相似数组参数,但形式更加简洁易懂。
But,params关键字的使用也有必定局限:当一个方法申明了一个params参数后,就不容许在其后面再有任何其余参数。
例以下面一段代码,定义了两个彻底相等的方法:NotParams和UseParams,使用由params修饰参数的方法时,能够直接把全部变量集合传入而无须先申明一个数组对象。
class Program { static void Main(string[] args) { // params string s = "I am a string"; int i = 10; double f = 2.3; object[] par = new object[3] { s, i, f }; // not use params NotParams(par); // use params UseParams(s, i, f); Console.ReadKey(); } // Not use params public static void NotParams(object[] par) { foreach (var obj in par) { Console.WriteLine(obj); } } // Use params public static void UseParams(params object[] par) { foreach (var obj in par) { Console.WriteLine(obj); } } }
(1)浅复制:复制一个对象的时候,仅仅复制原始对象中全部的非静态类型成员和全部的引用类型成员的引用。(新对象和原对象将共享全部引用类型成员的实际对象)
(2)深复制:复制一个对象的时候,不只复制全部非静态类型成员,还要复制全部引用类型成员的实际对象。
下图展现了浅复制和深复制的区别:
在.NET中,基类System.Object已经为全部类型都实现了浅复制,类型所要作的就是公开一个复制的接口,而一般的,这个接口会由ICloneable接口来实现。ICloneable只包含一个方法Clone,该方法既能够被实现为浅复制也能够被实现为深复制,具体如何取舍则根据具体类型的需求决定。此外,在Sys-tem.Object基类中,有一个保护的MemeberwiseClone()方法,它便用于进行浅度复制。因此,对于引用类型,要想实现浅度复制时,只须要调用这个方法就能够了:
public object Clone() { return MemberwiseClone(); }
下面的代码展现了一个使用ICloneable接口提供深复制的简单示例:
public class DeepCopy : ICloneable { public int i = 0; public A a = new A(); public object Clone() { // 实现深复制-方式1:依次赋值和实例化 DeepCopy newObj = new DeepCopy(); newObj.a = new A(); newObj.a.message = this.a.message; newObj.i = this.i; return newObj; } public new object MemberwiseClone() { // 实现浅复制 return base.MemberwiseClone(); } public override string ToString() { string result = string.Format("I的值为{0},A为{1}", this.i.ToString(), this.a.message); return result; } } public class A { public string message = "我是原始A"; } public class Program { static void Main(string[] args) { DeepCopy dc = new DeepCopy(); dc.i = 10; dc.a = new A(); DeepCopy deepClone = dc.Clone() as DeepCopy; DeepCopy shadowClone = dc.MemberwiseClone() as DeepCopy; // 深复制的目标对象将拥有本身的引用类型成员对象 deepClone.a.message = "我是深复制的A"; Console.WriteLine(dc); Console.WriteLine(deepClone); Console.WriteLine(); // 浅复制的目标对象将和原始对象共享引用类型成员对象 shadowClone.a.message = "我是浅复制的A"; Console.WriteLine(dc); Console.WriteLine(shadowClone); Console.ReadKey(); } }
其执行结果以下图所示,能够清楚地看到对深复制对象的属性的赋值不会影响原始对象,而浅复制则相反。
从上面的代码中能够看到,在深复制的实现中,若是每一个对象都要这样去进行深度复制就太麻烦了,能够利用序列化/反序列化来对对象进行深度复制:先把对象序列化(Serialize)到内存中,而后再进行反序列化,经过这种方式来进行对象的深度复制:
[Serializable] public class DeepCopy : ICloneable { ...... public object Clone() { // 实现深复制-方式1:依次赋值和实例化 //DeepCopy newObj = new DeepCopy(); //newObj.a = new A(); //newObj.a.message = this.a.message; //newObj.i = this.i; //return newObj; // 实现深复制-方式2:序列化/反序列化 BinaryFormatter bf = new BinaryFormatter(); MemoryStream ms = new MemoryStream(); bf.Serialize(ms, this); ms.Position = 0; return bf.Deserialize(ms); } ...... } [Serializable] public class A { public string message = "我是原始A"; }
PS:通常可被继承的类型应该避免实现ICloneable接口,由于这样作将强制全部的子类型都须要实现ICloneable接口,不然将使类型的深复制不能覆盖子类的新成员。
每个.NET应用程序最终都会运行在一个OS进程中,假设这个OS的传统的32位系统,那么每一个.NET应用程序均可以拥有一个4GB的虚拟内存。.NET会在这个4GB的虚拟内存块中开辟三块内存做为 堆栈、托管堆 以及 非托管堆。
(1).NET中的堆栈
堆栈用来存储值类型的对象和引用类型对象的引用(地址),其分配的是一块连续的地址,以下图所示,在.NET应用程序中,堆栈上的地址从高位向低位分配内存,.NET只须要保存一个指针指向下一个未分配内存的内存地址便可。
对于全部须要分配的对象,会依次分配到堆栈中,其释放也会严格按照栈的逻辑(FILO,先进后出)依次进行退栈。(这里的“依次”是指按照变量的做用域进行的),假设有如下一段代码:
TempClass a = new TempClass(); a.numA = 1; a.numB = 2;
其在堆栈中的内存图以下图所示:
这里TempClass是一个引用类型,拥有两个整型的int成员,在栈中依次须要分配的是a的引用,a.numA和a.numB。当a的做用域结束以后,这三个会按照a.numB→a.numA→a的顺序依次退栈。
(2).NET中的托管堆
众所周知,.NET中的引用类型对象时分配在托管堆上的,和堆栈同样,托管堆也是进程内存空间中的一块区域。But,托管堆的内存分配却和堆栈有很大区别。受益于.NET内存管理机制,托管堆的分配也是连续的(从低位到高位),可是堆中却存在着暂时不能被分配却已经无用的对象内存块。
当一个引用类型对象被初始时,会经过指向堆上可用空间的指针分配一块连续的内存,而后使堆栈上的引用指向堆上刚刚分配的这块内存块。下图展现了托管堆的内存分配方式:
如上图所示,.NET程序经过分配在堆栈中的引用来找到分配在托管堆的对象实例。当堆栈中的引用退出做用域时,这时仅仅就断开和实际对象实例的引用联系。而当托管堆中的内存不够时,.NET会开始执行GC(垃圾回收)机制。GC是一个很是复杂的过程,它不只涉及托管堆中对象的释放,并且须要移动合并托管堆中的内存块。当GC以后,堆中再也不被使用的对象实例才会被部分释放(注意并非彻底释放),而在这以前,它们在堆中是暂时不可用的。在C/C++中,因为没有GC,所以能够直接free/delete来释放内存。
(3).NET中的非托管堆
.NET程序还包含了非托管堆,全部须要分配堆内存的非托管资源将会被分配到非托管堆上。非托管的堆须要程序员用指针手动地分配和释放内存,.NET中的GC和内存管理不适用于非托管堆,其内存块也不会被合并移动,因此非托管堆的内存分配是按块的、不连续的。所以,这也解释了咱们为什么在使用非托管资源(如:文件流、数据库链接等)须要手动地调用Dispose()方法进行内存释放的缘由。
这是一个经典的基础知识题目,它涉及了字符串的类型、堆栈和堆的内存分配机制,所以被不少人拿来考核开发者的基础知识功底。首先,咱们都知道,判断值类型的标准是查看该类型是否会继承自System.ValueType,经过查看和分析,string直接继承于System.Object,所以string是引用类型,其内存分配会遵守引用类型的规范,也就是说以下的代码将会在堆栈上分配一块存储引用的内存,而后再在堆上分配一块存储字符串实例对象的内存。
string a = "edc";
如今再来看看string abc="aaa"+"bbb"+"ccc",按照常规的思路,字符串具备不可变性,大部分人会认为这里的表达式会涉及不少临时变量的生成,可能C#编译器会先执行"aaa"+"bbb",而且把结果值赋给一个临时变量,再执行临时变量和"ccc"相加,最后把相加的结果再赋值给abc。But,其实C#编译器比想象中要聪明得多,如下的C#代码和IL代码能够充分说明C#编译器的智能:
// The first format string first = "aaa" + "bbb" + "ccc"; // The second format string second = "aaabbbccc"; // Display string Console.WriteLine(first); Console.WriteLine(second);
该C#代码的IL代码以下图所示:
正如咱们所看到的,string abc="aaa"+"bbb"+"ccc";这样的表达式被C#编译器当作一个完整的字符串"aaabbbccc",而不是执行某些拼接方法,能够将其看做是C#编译器的优化,因此在本次内存分配中只是在栈中分配了一个存储字符串引用的内存块,以及在托管堆分配了一块存储"aaabbbccc"字符串对象的内存块。
那么,咱们的常规思路在.NET程序中又是怎么体现的呢?咱们来看一下一段代码:
int num = 1; string str = "aaa" + num.ToString(); Console.WriteLine(str);
这里咱们首先初始化了一个int类型的变量,其次初始化了一个string类型的字符串,并执行 + 操做,这时咱们来看看其对应的IL代码:
如上图所示,在这段代码中执行 + 操做,会调用String的Concat方法,该方法须要传入两个string类型的参数,也就产生了另外一个string类型的临时变量。换句话说,在这次内存分配中,堆栈中会分配一个存储字符串引用的内存块,在托管堆则分配了两块内存块,分别存储了存储"aaa"字符串对象和"1"字符串对象。
可能这段代码仍是不熟悉,咱们再来看看下面一段代码,咱们就感受十分亲切熟悉了:
string str = "aaa"; str += "bbb"; str += "ccc"; Console.WriteLine(str);
其对应的IL代码以下图所示:
如图能够看出,在拼接过程当中产生了两个临时字符串对象,并调用了两次String.Concat方法进行拼接,就不用多解释了。
GC是垃圾回收(Garbage Collect)的缩写,它是.NET众多机制中最为重要的一部分,也是对咱们的代码书写方式影响最大的机制之一。.NET中的垃圾回收是指清理托管堆上不会再被使用的对象内存,而且移动仍在被使用的对象使它们紧靠托管堆的一边。下图展现了一次垃圾回收以后托管堆上的变化(这里仅仅为了说明,简化了GC的执行过程,省略了包含Finalize方法对象的处理以及大对象分配的特殊性):
如上图所示,咱们能够知道GC的执行过程分为两个基本动做:
(1)一是找到全部再也不被使用的对象:对象A和对象C,并标记为垃圾;
(2)二是移动仍在被使用的对象:对象B和对象D。
这样以后,对象A和对象C所占用的内存空间就被腾空出来,以备下次分配的时候使用。
PS:一般状况下,咱们不须要手动干预垃圾回收的执行,不过CLR仍然提供了一个手动执行垃圾回收的方法:GC.Collect()。当咱们须要在某一批对象再也不使用而且及时释放内存的时候能够调用该方法来实现。But,垃圾回收的运行成本较高(涉及到了对象块的移动、遍历找到再也不被使用的对象、不少状态变量的设置以及Finalize方法的调用等等),对性能影响也较大,所以咱们在编写程序时,应该避免没必要要的内存分配,也尽可能减小或避免使用GC.Collect()来执行垃圾回收。
因为有了垃圾回收机制的支持,对象的析构(或释放)和C++有了很大的不一样,这就须要咱们在设计类型的时候,充分理解.NET的机制,明确怎样利用Dispose方法和Finalize方法来保证一个对象正确而高效地被析构。
(1)Dispose方法
// 摘要: // 定义一种释放分配的资源的方法。 [ComVisible(true)] public interface IDisposable { // 摘要: // 执行与释放或重置非托管资源相关的应用程序定义的任务。 void Dispose(); }
Microsoft考虑到不少状况下程序员仍然但愿在对象再也不被使用时进行一些清理工做,因此.NET提供了IDispose接口而且在其中定义了Dispose方法。一般咱们会在Dispose方法中实现一些托管对象和非托管对象的释放以及业绩业务逻辑的结束工做等等。
But,即便咱们实现了Dispose方法,也不能获得任何有关释放的保证,Dispose方法的调用依赖于类型的使用者,当类型被不恰当地使用,Dispose方法将不会被调用。所以,咱们通常会借助using等语法来帮助Dispose方法被正确调用。
(2)Finalize方法
刚刚提到Dispose方法的调用依赖于类型的使用者,为了弥补这一缺陷,.NET还提供了Finalize方法。Finalize方法相似于C++中的析构函数(方法),但又和C++的析构函数不一样。Finalize在GC执行垃圾回收时被调用,其具体机制以下:
①当每一个包含Finalize方法的类型的实例对象被分配时,.NET会在一张特定的表结构中添加一个引用而且指向这个实例对象,暂且称该表为“带析构方法的对象表”;
②当GC执行而且检测到一个不被使用的对象时,须要进一步检查“带析构方法的对象表”来查询该对象类型是否含有Finalize方法,若是没有则将该对象视为垃圾,若是存在则将该对象的引用移动到另一张表,暂且称其为“待析构的对象表”,而且该对象实例仍然被视为在被使用。
③CLR将有一个单独的线程负责处理“待析构的对象表”,其执行方法内部就是依次经过调用其中每一个对象的Finalize方法,而后删除引用,这时托管堆中的对象实例就被视为再也不被使用。
④下一个GC执行时,将释放已经被调用Finalize方法的那些对象实例。
(3)结合使用Dispose和Finalize方法:标准Dispose模式
Finalize方法因为有CLR保证调用,所以比Dispose方法更加安全(这里的安全是相对的,Dispose须要类型使用者的及时调用),但在性能方面Finalize方法却要差不少。所以,咱们在类型设计时通常都会使用标准Dispose模式:Finalize方法做为Dispose方法的后备,只有在使用者没有调用Dispose方法的状况下,Finalize方法才被视为须要执行。这一模式保证了对象可以被高效和安全地释放,已经被普遍使用。
下面的代码则是实现这种标准Dispose模式的一个模板:
public class BaseTemplate : IDisposable { // 标记对象是否已经被释放 private bool isDisposed = false; // Finalize方法 ~BaseTemplate() { Dispose(false); } // 实现IDisposable接口的Dispose方法 public void Dispose() { Dispose(true); // 告诉GC此对象的Finalize方法再也不须要被调用 GC.SuppressFinalize(this); } // 虚方法的Dispose方法作实际的析构工做 protected virtual void Dispose(bool isDisposing) { // 当对象已经被析构,则没必要再继续执行 if(isDisposed) { return; } if(isDisposing) { // Step1:在这里释放托管资源 } // Step2:在这里释放非托管资源 // Step3:最后标记对象已被释放 isDisposed = true; } public void MethodA() { if(isDisposed) { throw new ObjectDisposedException("对象已经释放"); } // Put the logic code of MethodA } public void MethodB() { if (isDisposed) { throw new ObjectDisposedException("对象已经释放"); } // Put the logic code of MethodB } } public sealed class SubTemplate : BaseTemplate { // 标记子类对象是否已经被释放 private bool disposed = false; protected override void Dispose(bool isDisposing) { // 验证是否已被释放,确保只被释放一次 if(disposed) { return; } if(isDisposing) { // Step1:在这里释放托管的而且在这个子类型中申明的资源 } // Step2:在这里释放非托管的而且这个子类型中申明的资源 // Step3:调用父类的Dispose方法来释放父类中的资源 base.Dispose(isDisposing); // Step4:设置子类的释放标识 disposed = true; } }
真正作释放工做的只是受保护的虚方法Dispose,它接收一个bool参数,主要用于区分调用者是类型的使用者仍是.NET的GC机制。二者的区别在于经过Finalize方法释放资源时不能再释放或使用对象中的托管资源,这是由于这时的对象已经处于不被使用的状态,颇有可能其中的托管资源已经被释放掉了。在Dispose方法中GC.SuppressFinalize(this)告诉GC此对象在被回收时不须要调用Finalize方法,这一句是改善性能的关键,记住实现Dispose方法的本质目的就在于避免全部释放工做在Finalize方法中进行。
在.NET的GC执行垃圾回收时,并非每次都扫描托管堆内的全部对象实例,这样作太耗费时间并且没有必要。相反,GC会把全部托管堆内的对象按照其已经再也不被使用的可能性分为三类,而且从最有可能不被使用的类别开始扫描,.NET对这样的分类类别有一个称呼:代(Generation)。
GC会把全部的托管堆内的对象分为0代、1代和2代:
第0代,新近分配在堆上的对象,历来没有被垃圾收集过。任何一个新对象,当它第一次被分配在托管堆上时,就是第0代。
第1代,经历过一次垃圾回收后,依然保留在堆上的对象。
第2代,经历过两次或以上垃圾回收后,依然保留在堆上的对象。若是第2代对象在进行完垃圾回收后空间仍然不够用,则会抛出OutOfMemoryException异常。
对于这三代,咱们须要知道的是并非每次垃圾回收都会同时回收3个代的全部对象,越小的代拥有着越多被释放的机会。
CLR对于代的基本算法是:每执行N次0代的回收,才会执行一次1代的回收,而每执行N次1代的回收,才会执行一次2代的回收。当某个对象实例在GC执行时被发现仍然在被使用,它将被移动到下一个代中上,下图简单展现了GC对三个代的回收操做。
根据.NET的垃圾回收机制,0代、1代和2代的初始分配空间分别为256KB、2M和10M。说完分代的垃圾回收设计,也许咱们会有疑问,为何要这样弄?其实分代并非空穴来风的设计,而是参考了这样一个事实:
一个对象实例存活的时间越长,那么它就具备更大的机率去存活更长的时间。换句话说,最有可能立刻就不被使用的对象实例,每每是那些刚刚被分配的对象实例,并且新分配的对象实例一般都会被立刻大量地使用。这也解释了为何0代对象拥有最多被释放的机会,而且.NET也只为0代分配了一块只有256KB的小块逻辑内存,以使得0代对象有机会被所有放入处理器的缓存中去,这样作的结果就是使用频率最高而且最有可能立刻能够被释放的对象实例拥有了最高的使用效率和最快的释放速度。
由于一次GC回收以后仍然被使用的对象会被移动到更高的代上,所以咱们须要避免保留已经再也不被使用的对象引用,将对象的引用置为null是告诉.NET该对象不须要再使用的最直接的方法。
在前面咱们提到Finalize方法会大幅影响性能,经过结合对代的理解,咱们能够知道:在带有Finalize方法的对象被回收时,该对象会被视为正在被使用从而被留在托管堆中,且至少要等一个GC循环才能被释放(为何是至少一个?由于这取决于执行Finalize方法的线程的执行速度)。很明显,须要执行Finalize方法的那些对象实例,被真正释放时最乐观的状况下也已经位于1代的位置上了,而若是它们是在1代上才开始释放或者执行Finalize方法的线程运行得慢了一点,那该对象就在第2代上才被释放,相对于0代,这样的对象实例在堆中存留的时间将长不少。
在.NET中引用类型对象实例一般经过引用来访问,而GC判断堆中的对象是否仍然在被使用的依据也是引用。简单地说:当没有任何引用指向堆中的某个对象实例时,这个对象就被视为再也不使用。
在GC执行垃圾回收时,会把引用分为如下两类:
(1)根引用:每每指那些静态字段的引用,或者存活的局部变量的引用;
(2)非根引用:指那些不属于根引用的引用,每每是对象实例中的字段。
垃圾回收时,GC从全部仍在被使用的根引用出发遍历全部的对象实例,那些不能被遍历到的对象将被视为再也不被使用而进行回收。咱们能够经过下面的一段代码来直观地理解根引用和非根引用:
class Program { public static Employee staticEmployee; static void Main(string[] args) { staticEmployee = new Employee(); // 静态变量 Employee a = new Employee(); // 局部变量 Employee b = new Employee(); // 局部变量 staticEmployee.boss = new Employee(); // 实例成员 Console.ReadKey(); Console.WriteLine(a); } } public class Employee { public Employee boss; public override string ToString() { if(boss == null) { return "No boss"; } return "One boss"; } }
上述代码中一共有两个局部变量和一个静态变量,这些引用都是根引用。而其中一个局部变量 a 拥有一个成员实例对象,这个引用就是一个非跟引用。下图展现了代码执行到Console.ReadKey()这行代码时运行垃圾回收时的状况。
从上图中能够看出,在执行到Console.ReadKey()时,存活的根引用有staticEmployee和a,前者由于它是一个公共静态变量,然后者则由于后续代码还会使用到a。经过这两个存活的根引用,GC会找到一个非跟引用staticEmployee.boss,而且发现三个仍然存活的对象。而b的对象则将被视为再也不使用从而被释放。(更简单地确保b对象再也不被视为在被使用的方法时把b的引用置为null,即b=null;)
此外,当一个从根引用触发的遍历抵达一个已经被视为在使用的对象时,将结束这一个分支的遍历,这样作能够避免陷入死循环。
首先,必须明确一点:即便在拥有垃圾回收机制的.NET托管堆上,仍然是有可能发生内存泄露现象的。
其次,什么是内存泄露?内存泄露是指内存空间上产生了再也不被实际使用却又不能被分配的内存空间,其意义很普遍,像内存碎片、不完全的对象释放等都属于内存泄露现象。内存泄露将致使主机的内存随着程序的运行而逐渐减小,不管其表现形式怎样,它的危害是很大的,所以咱们须要努力地避免。
按照内存泄露的定义,咱们能够知道在大部分的时候.NET中的托管堆中存在着短暂的内存泄露状况,由于对象一旦再也不被使用,须要等到下一个GC时才会被释放。这里列举几个在.NET中常见的几种对系统危害较大的内存泄露状况,咱们在实际开发中须要极力避免:
(1)大对象的分配
.NET中全部的大对象(这里主要是指对象的大小超过指定数值[85000字节])将分配在托管堆内一个特殊的区域内,暂且将其称为“大对象堆”(这也算是CLR对于GC的一个优化策略)。大对象堆中最重要的一个特色就是:没有代级的概念,全部对象都被视为第2代。在回收大对象堆内的对象时,其余的大对象不会被移动,这是考虑到大规模地移动对象须要耗费过多的资源。这样,在程序过多地分配和释放大对象以后,就会产生不少内存碎片。下图解释了这一过程:
如图所示能够看出,随着对象的分配和释放不断进行,在不进行对象移动的大对象堆内,将不可避免地产生小的内存碎片。咱们所须要作的就是尽可能减小大对象的分配次数,尤为是那些做为局部变量的,将被大规模分配和释放的大对象,典型的例子就是String类型。
(2)不恰当地保存根引用
最简单的一个错误例子就是不恰当地把一个对象申明为公共静态变量,一个公共的静态变量将一直被GC视为一个在使用的根引用。更糟糕的是:当这个对象内部还包含更多的对象引用时,这些对象一样不会被释放。例以下面一段代码:
public class Program { // 公共静态大对象 public static RefRoot bigObject = new RefRoot("test"); public static void Main(string[] args) { Console.ReadKey(); } } public class RefRoot { // 这是一个占用大量内存的成员 public string[] BigMember; public RefRoot(string content) { // 初始化大对象 BigMember = new string[1000]; for (int i = 0; i < 1000; i++) { BigMember[i] = content; } } }
在代码中,定义了一个公共静态的大对象,这个对象将直到程序运行结束后才会被GC释放掉。若是在整个程序中各个类型不断地使用这个静态成员,那这样的设计有助于减小大对象堆内的内存碎片,可是若是整个程序极少地甚至只有一次使用了这个成员,那考虑到它占用的内存会影响总体系统性能,设计时则应该考虑设计成实例变量,以便GC可以及时释放它。
(3)不正确的Finalize方法
前面已经介绍了Finalize方法时由GC的一个专用的线程进行调用,抛开Microsoft怎样实现的这个具体的调度算法,有一点能够确定的是:不正确的Finalize方法将致使Finalize方法不能被正确执行。若是系统中全部的Finalize方法不能被正确执行,包含它们的对象也只能驻留在托管堆内不能被释放,这样的状况将会致使严重的后果。
那么,什么是不正确的Finalize方法?Finalize方法应该只致力于快速而简单地释放非托管资源,而且尽量快地返回。相反,不正确的Finalize方法则可能包含如下这样的一些代码:
①没有保护地写文件日志;
②访问数据库;
③访问网络;
④把当前对象赋给某个存活的引用;
例如,当Finalize方法试图访问文件系统、数据库或者网络时,将会有资源争用和等待的潜在危险。试想一个不断尝试访问离线数据库的Finalize方法,将会在长时间内不会返回,这不只影响了对象的释放,也使得排在Finalize方法队列中的全部后续对象得不到释放,这个连锁反应将会致使很快地形成内存耗尽。此外,若是在Finalize方法中把对象自身又赋给了另一个存活的引用,这时对象内的一部分资源已经被释放掉了,而另一部分尚未,当这样一个对象被激活后,将致使不可预知的后果。
(1)朱毅,《进入IT企业必读的200个.NET面试题》
(2)张子阳,《.NET之美:.NET关键技术深刻解析》
(3)王涛,《你必须知道的.NET》