描述
让我们从GC的视角看一下这个问题。如果要可靠的找出垃圾,我们需要一个高效的方法。显然,我们需要决定哪些是垃圾,哪些不是。为了确定哪些需要保留,首先我们假设所有没有被使用的都是垃圾。设想一下我们有两个朋友:JIT和CLR,他们负责跟踪什么正在使用并且给出一个保存清单。我们称这份清单为根清单,因为我们使用它作为起始点。我们需要保存一个主清单用来描述我们想保留的东西都在哪。那些清单中保留的东西工作时需要依赖的东西也会被添加到清单中。(比如:如果我们保留电视,我们肯定不能丢掉遥控器,所以遥控器也要保留到清单中)
这也是GC的保留策略。它从JIT和CLR哪里接收并保存一个根对象的清单,并且递归查找根对象的引用形成一个保留图表。
根的组成:
- 全局/静态 指针。一种方式确保我们的对象不被垃圾回收期回收的方式,就是使用静态变量
- 栈中的指针。在栈中正在执行的程序肯定不希望被丢掉
- CPU寄存器指针。在托管堆中正在被CPU引用的对象需要被保留
在上面的图中,objects1,3,和5在托管堆中被根对象引用,1和5是直接引用,3是在递归查找时被发现。就像之前的比喻,如果1是电视机的话,那么3就是遥控器。在所有对象都检查完毕后,我们就准备好了进入下一个环节,压缩。
压缩
现在我们已经把需要保留的对象都记录下来了,需要做的就是把要保留的对象放在一起。
幸运的是在我们需要存放东西之前,并不需要清理。当Object 2不需要时,GC会向下移动Object 3并修正Object1引用Object3的指针。
下一步,把Object5挪下来
现在所有的东西都清理完毕,我们只需要写一个便签放在压缩好的堆顶部,让大家知道在哪放新的对象。
知道GC的原理,能让我们明白把对象移动到一起是相当繁重的工作。就像你看到的,如果我们可以减小需要移动对象的尺寸,就可以提高GC的处理效率,因为需要移动的东西少了。
非托管堆是什么样的
对于一个负责的垃圾处理者,一个需要使用面对的问题是在清理房间时如何处理汽车中的东西。清理时,我们需要清理所有的东西。如果我们要清理一台笔记本电脑,电池却在车上呢?
这些情形下GC需要执行了代码来清理非托管资源(如:文件,数据库连接,网络连接等等)。一个可行的方式是通过析构函数。
class Sample
{
~Sample()
{
// FINALIZER: CLEAN UP HERE
}
}
对象创建时,所有含有析构函数的对象都会被放进一个finalization queue。对象1,4和5包含析构函数并且在finalization queue上。让我们来看一下当object 2和4不再有引用并且已经准备好GC时会发生什么。
Object 2被常规对待,然而当轮到Object 4的时候,GC发现它在finalization queue上,object 4 的析构器被添加到一个叫freachable的queue中。
这里有一个专用的线程来执行freachable queue。一旦析构器被这个线程执行,Object 4被从freachable queue中移除。只有这时Object 4才准备好被回收。
所以Object 4会一直存在知道下一个GC周期。
因为在类中加入析构函数引起了GC繁重的额外工作,并且对GC的性能乃至我们的程序产生不利的影响。只在不得不使用析构函数的时候使用它。
一个更好的释放非托管资源的方式是实现IDisposable 接口。
IDisposaible
实现IDisposable 的类通过执行Dispose() 方法进行回收(接口中唯一的方法签名)。所以假设有一个类ResouceUser用这种方式替代析构函数,如下:
public class ResourceUser
{
~ResourceUser() // THIS IS A FINALIZER
{
// DO CLEANUP HERE
}
}
我们可以用IDisposable 接口更好的实现同样的功能:
public class ResourceUser : IDisposable
{
#region IDisposable Members
public void Dispose()
{
// CLEAN UP HERE!!!
}
#endregion
}
IDisposable接口和using关键字是配合使用的,在using块的最后,Dispose()会被调用。在using块之后,对象不应该再被引用,因为显而易见它应该被GC清理掉。
public static void DoSomething()
{
ResourceUser rec = new ResourceUser();
using (rec)
{
// DO SOMETHING
} // DISPOSE CALLED HERE
// DON'T ACCESS rec HERE
}
我喜欢使用using块,因为它看起来可读性更强,而且变量在块外不可用。
public static void DoSomething()
{
using (ResourceUser rec = new ResourceUser())
{
// DO SOMETHING
} // DISPOSE CALLED HERE
}
通过使用using和实现IDisposible 接口,使得在GC清理我们的类时没有任何额外的工作。
注意静态变量
class Counter
{
private static int s_Number = 0;
public static int GetNextNumber()
{
int newNumber = s_Number;
// DO SOME STUFF
s_Number = newNumber + 1;
return newNumber;
}
}
如果两个线程同时调用GetNextNumber()方法并且都在s_Number增长前赋值给newNumber变量,那么它们将得到相同的结果!一种方式确保同一时间只有一个线程可以访问代码块是使用锁,应该lock尽可能小的代码块,因为其他线程需要等待lock住的代码块执行完。
class Counter
{
private static int s_Number = 0;
public static int GetNextNumber()
{
lock (typeof(Counter))
{
int newNumber = s_Number;
// DO SOME STUFF
newNumber += 1;
s_Number = newNumber;
return newNumber;
}
}
}
再次注意静态变量
下一个要注意的是被静态变量引用的ojbects,记住,被根引用的东西不会被清理。下面是一个特别恶心的例子:
class Olympics
{
public static Collection<Runner> TryoutRunners;
}
class Runner
{
private string _fileName;
private FileStream _fStream;
public void GetStats()
{
FileInfo fInfo = new FileInfo(_fileName);
_fStream = _fileName.OpenRead();
}
}
因为Runner集合是 Olympics类的静态变量,不光是它永远不会被GC清理,你可能注意到了,每次我们在执行GetStats()方法时,stream被打开一次。因为它没有关闭也不会被GC清理,这个代码就是个灾难。试想一下执行100000次的后果。
单例
一个技巧是对于通用的类,在内存中永远保持一个实例。这就是GOF中的单例模式。单例模式应该慎用,因为它是全局变量,在多线程编程中,多个线程可以改变它的状态,经常引起一些奇怪的行为和头疼的问题。如果要用单例模式,最好要有充分的理由。
public class Earth
{
private static Earth _instance = new Earth();
private Earth() { }
public static Earth GetInstance() { return _instance; }
}
上面是单例模式的经典实现方式,并且这种方式是线程安全的。因为CLR确保static变量的线程安全。
结论
提高GC性能的方式:
- 清理。不要让资源一直开着。确保所有用完的连接都关闭并且尽快清理非过关资源。一个使用非托管资源的规则,尽量晚的实例化,尽量早的释放。
- 不要过度引用,记住,如果object在用,所有它引用的对象都不会清理,以此类推。当使用完一个变量时,可以手动设置它为null,告诉GC它可以被清理。一个小技巧是,可以赋给变量一个轻量级的空Object,以避免出现null reference异常。引用的对象越少,清理时GC需要遍历记录的描述越少。
- 少用析构函数,尽量使用实现IDisposible接口和using代码块实现。
- 尽量将父和子对象放在一起,GC移动一块大的内存会比处理很多碎片要容易,所以当实例化一个对象时,尽量一起实例化它引用的对象。
原文链接:
https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-iv/