【译】让垃圾回收器高效工作(三)
这篇文章我们谈谈固定对象的内存地址(pinning)和弱引用……这两个和垃圾回收处理密切相关的东西。
固定对象的内存地址:
固定对象的内存地址和实现Finalize方法的对象有一点是相同的 …… 两者都是因为我们的程序不得不和本地代码打交道。
怎么固定对象的内存地址呢?有三种方法
1. 使用GCHandle的静态方法Alloc(object val,GCHandleType type) ,将type值设为GCHandleType.Pinned
2. 在C#中使用fixed关键字
3. 在调用本地代码时,本本地代码固定(例如:to marshal LPWSTR as a String object, Interop pins the buffer for the duration of the call)
对于小对象堆来说,在代码中固定对象地址是导致内存碎片的唯一原因,如果没有固定内存地址的对象那么在小对象堆中就不应该有碎片。
而对于大对象堆,固定内存地址的操作是无效的,因为现在的垃圾回收机制是不会移动大对象堆的对象的。不过,这一点只是GC的内部实现,你不应该依赖于这个实现,如果大对象需要固定内存地址,你还是要写固定需要的代码。
内存碎片从来都不是一个好东西。它会增加垃圾回收工作的难度 —— 如果没有固定内存地址的对象,垃圾回收器在移动内存时只需要将非垃圾对象覆盖空闲内存就可以了,而堆上存在固定地址的对象,垃圾回收器就不得不在移动中考虑,不覆盖这类对象,也不能移动它们。
那么如何才能知道你的程序中有多少内存碎片呢?你可以使用!dumpheap命令:“dumpheap –type Free -stat”会给出所有释放对象占用内存的统计信息。 通常情况下如果碎片大小占总大小的比例小于10%的话,就没什么可担忧的。因此如果你看到释放对象的绝对数很大,但是总数少于10%就没必要害怕。
如果你确实需要固定对象的地址,请注意下面几件事情:
1. 短时间的固定开销会很小
“短时间”,多短算短呢? 如果固定内存地址的对象在垃圾回收之前就成为垃圾对象了,那么这个时间就是足够短了。因为固定内存其实就是在对象头置一个bit位,如果在对象存活期没有发生垃圾回收,那么就没有额外开销。如果在垃圾回收发生后这个对象还活着,垃圾回收器在移动内存时就得做更多的计算保证不会移动此对象,也不会覆盖它。
2. 固定老对象的代价会比固定年轻对象的代价要小一些
何为“老对象”呢?是指经过两次垃圾回收,已经被迁移到2代堆的对象;这时候对象的所在的内存区域已经相对稳定了。造成内存碎片的可能性会小一些。
两个固定对象内存地址好实践:
1. 在大对象堆上分配固定地址的对象,每次使用使使用其中的一部分
这样做的优点是显而易见的,大对象堆不会做内存移动操作,所以就不存在因为固定对象地址导致的开销了;缺点是没有现成的API来把大对象分成一小块一小块使用,这需要开发人员按需编码使用。
2. 分配一个小对象的缓冲池,(and then hand them out when needed)
例如,我有一个缓冲池,方法M有一个byte[]数组需要固定内存地址。如果这个数组已经是2代对象了,就固定它。而如果缓冲区不需要使用很长时间,那么就在0代和1代回收时回收它。这样所有在缓冲池中的对象就都是2代对象了。
void M(byte[] b)
{
if (GC.GetGeneration(b) == GC.MaxGeneration)
{
RealM(b);
return;
}
// GetBuffer will allocate one if no buffers
// are available in the buffer pool.
byte[] TempBuffer = BufferPool.GetBuffer();
RealM(TempBuffer);
CopyBackToUserBuffer(TempBuffer, b);
BufferPool.Release(TempBuffer);
}
弱引用:
弱引用是如何实现的呢?
一个弱引用对象有托管和非托管两个部分。托管部分是WeakReference对象本身。在它的构造函数中我们创建一个GC句柄,它是非托管的部分 —— 这会在AppDomain的句柄表中插入一项(GCHandle类的Alloc方法都是这么做的,只不过是将不同类型插入到各自的表中)。当弱引用指向的对象没有强引用时就会被垃圾回收器回收掉。因为WeakReference对象本身是一个托管对象,所以它没有强引用时也会被回收。
弱引用没必要引用小对象:
如果你有一个非常小的对象,比如说一个DWORD字段,对象的大小是12byte(12byte是对象的最小尺寸)。而WeakReference对象有一个IntPtr和一个bool字段,而GC句柄是一个指针的大小(32位机器4byte,64位机器8byte);这就是说你需要使用15个byte的对象来延长一个12byte对象的长度,这是不划算的。显然你不应该创建很多弱引用对象来引用一些小对象。
那么,弱引用有什么用呢?
弱引用的作用是在垃圾回收发生之前即便对象上没有强引用,也可以再次使用该对象。如果执行垃圾回收它会被回收。
为什么要使用弱引用来跟踪一个对象的释放,而不是用Finalizer方法呢? 使用弱引用的优点是跟踪的对象不会被推迟到下次垃圾回收时才真正的被回收;缺点是需要消耗一点内存,只有当用户代码检查弱引用指向的对象为null时才清除对象。
下面是两个弱对象的使用实例:
Option A):
class A { WeakReference _target; MyObject Target { set { _target = new WeakReference(value); } get { Object o = _target.Target; if (o != null) { return o; } else { // my target has been GC'd - clean up Cleanup(); return null; } } } void M() { // target needs to be alive throughout this method. MyObject target = Target; if (target == null) // target has been GC'd, don't bother return; else { // always need target to be alive. DoSomeWork(); } GC.KeepAlive(target); } }
Option B):
class A { WeakReference _target; MyObject ShortTemp; MyObject Target { set { _target = new WeakReference(value); } get { Object o = _target.Target; if (o != null) { return o; } else { // my target has been GC'd - clean up Cleanup(); return null; } } } void M() { // target needs to be alive throughout this method. MyObject target = Target; ShortTemp = target; if (target == null) { // target has been GC'd, don't bother return; } else { // could assert that ShortTemp is not null. DoSomeWork(); } ShortTemp = null; } }
使用弱对象维护缓存:
你可以创建一个WeakReference指向对象的数组
WeakReferencesToObjects WeakRefs[];
数组中的每个元素都是弱对象指向的对象。我们可以定期的遍历这个数组看哪个对象已经死了然后释放引用此对象的WeakReference对象。
如果我们经常处理已经释放的对象,缓存就会在每次垃圾回收之后失效。
如果对你来说这还不够,你可以使用二级缓存机制。
维持一段时间的缓存项的强引用列表,在这段时间之后,将强引用转换成弱引用,弱引用意味着这些项将要被剔除。
你可能有不同的策略来处理缓存,比如根据缓存被读取次数——读取次数少的项被转换成弱引用;或者根据缓存项的个数,如果缓存项数超过某个数字就把超过的缓存项设置为弱引用。这取决于你的应用。调优缓存是另一个完整的主题——或许将来我会写一下。
原文:http://blogs.msdn.com/b/maoni/archive/2004/12/19/327149.aspx
原作者:Maoni Stephens
相关随笔:
参考:
使用GCHandle固定内存
http://www.codeproject.com/KB/cs/PinnedObject.aspx
http://msdn.microsoft.com/en-us/library/83y4ak54.aspx