现在的位置: 首页 > 综合 > 正文

GC/Windbg/IDisposable/IL/MeteData/JIT 杂谈

2012年10月27日 ⁄ 综合 ⁄ 共 12646字 ⁄ 字号 评论关闭

    GC工作原理大家都已经知道,简单的说就是按代回收托管对象。哪些托管对象会被回收,是通过每个程序的Root来识记,被Root标记的引用对象以及递归出所有相同的引用对象都是活对象,而未被标记的则意味着要被回收。何时回收对象?对于每个对象,CLR首先尝试把对象分配在0代中,如果0代已满,那么GC才会开始工作,把0代中的对象转移到1代中;如果1代也满,GC将会把1代中的对象转移到2代中。也就是说,CLR会在适当的时机,才开始进行垃圾回收。但是对于非托管对象的处理就显得无能为力,但是GC仍然有自己的解决方式。如果非托管对象实现了Finalize方法,代表此非托管对象可终结。因此在非托管对象被创建时,这些对象引用会被GC维护到一个特殊的全局队列中,这个队列被称为终结队列即FinalizeQueue. FinzlizeQueue标记的这些对象,程序中没有正确回收或者没有调用这些对象的Finalize/Close,亦或者实现析构,GC的Finalize线程也不会去回收这些对象。因此,根据上面GC的分析,这里就引出了如何得知这些非托管对象没有被正确的释放掉呢?当我们的程序中大量使用了非托管资源时,经常出现内存不可读不可用的问题时,windbg就是一个好的调试工作,至少最简单的情况可以帮助我们分析哪些对象的Dispose没有被调用,这里只是针对.net 简单使用。上面标记红色的字是windbg中会用到的命令。

    windbg怎么用,园子里可以找到很多文章,这里不多介绍了。下面是测试使用的代码

View Code

        static void Main(string[] args)
{
for (int i = 0; i < 3; i++)
{
Thread t1 = new Thread(new ThreadStart(Create));
t1.Start();
}

Console.ReadLine();
}

static void Create()
{
Mongo mongo = new Mongo("Server=172.16.0.25:27017;Pooled=true");
mongo.Connect();
IMongoDatabase simple = mongo["simple"];
mongo.Disconnect();
mongo.Dispose();
}

运行Demo.exe  开启windbg 将demo.exe附加进来    首次使用windbg 调试.net 程序要加载mscorwks.dll   命令  .loadby sos mscorwks 

0:006> !threads
*********************************************************************
* Symbols can not be loaded because symbol path is not initialized. *
* *
* The Symbol Path can be set by: *
* using the _NT_SYMBOL_PATH environment variable. *
* using the -y <symbol_path> argument when starting the debugger. *
* using .sympath and .sympath+ *
*********************************************************************
PDB symbol for mscorwks.dll not loaded
ThreadCount: 7
UnstartedThread: 0
BackgroundThread: 3
PendingThread: 0
DeadThread: 3
Hosted Runtime: no
PreEmptive GC Alloc Lock
ID OSID ThreadOBJ State GC Context Domain Count APT Exception
0 1 12a4 004c99f0 a020 Enabled 01b1c108:01b1c4ac 004c5428 1 MTA
2 2 610 004d8a18 b220 Enabled 00000000:00000000 004c5428 0 MTA (Finalizer)
XXXX 3 0 00544d88 9820 Enabled 00000000:00000000 004c5428 0 MTA
XXXX 4 0 00546558 9820 Enabled 00000000:00000000 004c5428 0 Ukn
XXXX 5 0 005583b0 9820 Enabled 00000000:00000000 004c5428 0 Ukn
3 6 17dc 0055bb30 80a220 Enabled 00000000:00000000 004c5428 0 MTA (Threadpool Completion Port)
5 7 e58 0056c2c0 180b220 Enabled 01b6c0a8:01b6dfe8 004c5428 0 MTA (Threadpool Worker)

可以看到有7个线程,DeadThread的线程就是我们循环创建的3个线程 .(说明Thread创建的线程不会被马上回收掉)

通过FinalizeQueue查看哪些对象未被回收掉

0:005> !FinalizeQueue
SyncBlocks to be cleaned up: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 151 finalizable objects (03dd3a80->03dd3cdc)
generation 1 has 0 finalizable objects (03dd3a80->03dd3a80)
generation 2 has 0 finalizable objects (03dd3a80->03dd3a80)
Ready for finalization 0 objects (03dd3cdc->03dd3cdc)
Statistics:
MT Count TotalSize Class Name
51bb4a04 1 20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
51bb49ac 1 20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
5134a69c 1 20 Microsoft.Win32.SafeHandles.SafeFileMapViewHandle
51b971ac 1 24 System.Threading.TimerBase
51ba12a4 2 40 Microsoft.Win32.SafeHandles.SafePEFileHandle
5134a644 2 40 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
5134a5ec 2 40 Microsoft.Win32.SafeHandles.SafeLocalMemHandle
00168fd4 3 60 MongoDB.Connections.Connection
517a9fd4 3 72 System.Net.Sockets.TcpClient
5133ec98 3 72 System.Net.SafeCloseSocket
00e00a10 3 96 MongoDB.Connections.RawConnection
5133e590 5 100 System.Net.SafeCloseSocket+InnerSafeCloseSocket
51baeab4 6 120 Microsoft.Win32.SafeHandles.SafeFileHandle
5134a6f4 6 120 Microsoft.Win32.SafeHandles.SafeProcessHandle
51334fb0 1 160 System.Diagnostics.Process
51baa298 11 176 System.WeakReference
51b97b24 10 200 Microsoft.Win32.SafeHandles.SafeWaitHandle
51bb10bc 4 224 System.Threading.Thread
5133e420 3 228 System.Net.Sockets.Socket
51b85070 4 320 System.IO.FileStream
51bab304 18 360 Microsoft.Win32.SafeHandles.SafeRegistryHandle
51baa2f0 11 484 System.Threading.ReaderWriterLock
51b981a8 40 800 Microsoft.Win32.SafeHandles.SafeTokenHandle
51349f2c 10 1200 System.Diagnostics.PerformanceCounter
Total 151 objects

其中mongodb Connection 3条 RawConnection  3条以及  Socket 3条 是由掉死的3个线程 创建的

看一下RawConnection 的情况  MT00e00a10

0:005> !dumpheap -mt 00e00a10
Address MT Size
01b116e8 00e00a10 32
01b12db4 00e00a10 32
01b1f924 00e00a10 32
total 3 objects
Statistics:
MT Count TotalSize Class Name
00e00a10 3 96 MongoDB.Connections.RawConnection
Total 3 objects

看看为什么GC没有回收,说明有根在标记这个对象为活动状态 ,我们通过GCRoot 来查看根的情况    address  01b116e8

0:005> !GCRoot 01b116e8
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 15b8
Scan Thread 2 OSTHread 1340
Scan Thread 3 OSTHread 1298
DOMAIN(003A5428):HANDLE(Pinned):1213ec:Root:02a73250(System.Object[])->
01b14e1c(System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[MongoDB.Connections.IConnectionFactory, MongoDB]])->
01b1f8c0(System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[MongoDB.Connections.IConnectionFactory, MongoDB]][])->
01b1f36c(MongoDB.Connections.PooledConnectionFactory)->
01b1f39c(System.Collections.Generic.Queue`1[[MongoDB.Connections.RawConnection, MongoDB]])->
01b1fed4(System.Object[])->
01b116e8(MongoDB.Connections.RawConnection)

冒似是mongo连接池的队列一直在活动,导致RawConnetion没有被回收掉。看一下是不是RawConnection中Socket连接未关闭

0:010> !dumpobj 01b32db4
Name: MongoDB.Connections.RawConnection
MethodTable: 008f0a10
EEClass: 0086adc8
Size: 32(0x20) bytes
(E:\MongGo\samus-mongodb-csharp-6397a0f\Demo\bin\Debug\MongoDB.dll)
Fields:
MT Field Offset Type VT Attr Value Name
517a9fd4 4000234 4 ...Sockets.TcpClient 0 instance 01b32dd4 _client
00000000 4000235 8 0 instance 01b32e64 _authenticatedDatabases
51b845b4 4000236 10 System.Boolean 1 instance 0 _isDisposed
51b845b4 4000237 11 System.Boolean 1 instance 0 <IsInvalid>k__BackingField
51b88148 4000238 14 System.DateTime 1 instance 01b32dc8 <CreationTime>k__BackingField
008f011c 4000239 c ...ngoServerEndPoint 0 instance 01b401c8 <EndPoint>k__BackingField

->gcroot  address  01b32dd4 

0:010> !GCRoot 01b32dd4
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 1300
Scan Thread 2 OSTHread a38
Scan Thread 3 OSTHread 6b0
Scan Thread 5 OSTHread 10b4
DOMAIN(00215428):HANDLE(Pinned):1b13ec:Root:02a93250(System.Object[])->
01b31750(System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[MongoDB.Connections.IConnectionFactory, MongoDB]])->
01b40210(System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[MongoDB.Connections.IConnectionFactory, MongoDB]][])->
01b3fcbc(MongoDB.Connections.PooledConnectionFactory)->
01b3fcec(System.Collections.Generic.Queue`1[[MongoDB.Connections.RawConnection, MongoDB]])->
01b3308c(System.Object[])->
01b32db4(MongoDB.Connections.RawConnection)->
01b32dd4(System.Net.Sockets.TcpClient)

windbg对于调试非托管资源泄漏很方便,想了解更多读读这本书<CLRViaCS.第3版> . 书有点贵啊!详细windbg gc的调试可参照MSDN的这篇文章 CLR 完全介绍: 研究内存问题

 

    然而在实际应用很可能非托管资与托管资源是相互引用,所以就要按照一定的顺序来释放。.net 下提供了实现标准Dispose模式的IDisposable接口用以实现非托管资源的释放,当然你也可以自己手动释放,只要合理其实都一样。只是继承IDisposable我们多了选择,可以选择自己释放,也可以交由于GC代为管理。结合MSDN的例子,IDisposable的实现方式如下,详细介绍写在代码里。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ECEPDI.Utility
{
/*为什么有时候要使用IDisposable接口
* 因为我们的类中使用了非托管资源如COM接口,句柄等等,如你在使用完这些非托管资源能及时释放掉也不必实现IDisposable接口
* 或者,我们通过继承IDisposable接口后,在这个对象被使用完自己手动调用Dispose释放掉,或者等GC Finalize调用析构回收
*
*/
public class MyResource:IDisposable
{
// Pointer to an external unmanaged resource.
private IntPtr handle;
// Other managed resource this class uses.
//private Component component = new Component();
// Track whether Dispose has been called.

// The class constructor.
public MyResource(IntPtr handle)
{
this.handle = handle;
}


///<summary>
/// 垃圾回收器会通过析构函数调用Finalize回收FinalizeQueu对象(析构函数会被Finalize方法调用的)
///</summary>
~MyResource()
{
Dispose(false); //析构被编译后会加入try,catch包装 base.Finalize
}
#region IDisposable 成员


///<summary>
/// 提供给使用者自己释放对象
///</summary>
public void Dispose()
{
//由程序使用者自己释放对象
Dispose(true);

//因为调用者释放了对象,因此不需要垃圾回收器再次终结对象
GC.SuppressFinalize(this);
}

#endregion

///<summary>
/// 必须重复调用Dispose方法的标记
///</summary>
private bool disposed = false;

///<summary>
/// 所有工作都由此方法完成
/// (其它方法都是调用此方法,为何设计此方法就是要把非托管资源手动释放掉,
/// 析构和手动都要如此操作,只是手动操作还在主动把非托管资源释放掉)
///</summary>
///<param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{

if (!this.disposed)
{
if (disposing)
{
//托管资源的释放--------------------------------
//component.Dispose();
//没有dispose方法就设置为null-------------------
}

//-----------------------------------------------
//此处释放非托管资源
//-----------------------------------------------
CloseHandle(handle);
handle = IntPtr.Zero;
this.disposed = true;

}
}

// Use interop to call the method necessary
// to clean up the unmanaged resource.
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static Boolean CloseHandle(IntPtr handle);


///<summary>
/// 类似dbconnection Close方式释放对象 (其实调用的是Dispose)
///</summary>
public void Close()
{
Dispose();
}

}
}

实现IDisposable 接口后,我们有了两种方式释放对象中引用的资源,一种手动调用Dispose释放,另一种就是通过析构函数交由GC处理。 析构函数只是把我们dispose 非托管资源的代码交给GC Finalize调用,但是C#(或者其他的支持CLR的语言)中,由于GC的不确定性,析构方法被调用的时机我们无法确定,因此正常情况还是要我们及时手动的using或调用Dispose方法回收掉。

PS:注意析构函数的对象,至少要两次以上GC.Collect()才能回收掉,第一次GC回收只做了一次移动标记的工作,第二次GC才认为此对象不可到达才真正回收。

当然你可以在程序结束时执行GC.Collect(),GC.WaitForPendingFinalizers()强制回收。

强调一下:调用Dispose释放对象,不是释放对象本身,而是释放对象引用的的资源(非托管以及托管资源,一般托管资源都是交给GC处理的).调用Dispose的当前对象肯定属于活动对象,不可能在执行自己的Dispose时被释放或者被GC回收。当然我们可以在对象调用Dispose后,将其设置为null释放掉内存。

 

补充:

0:004> !dumpmt -md 00194438
EEClass: 006d6d28
Module: 00193488
Name: MongoDB.Mongo
mdToken: 020000ae (E:\MongGo\samus-mongodb-csharp-0.90.0.1\Demo\bin\Debug\MongoDB.dll)
BaseSize: 0x10
ComponentSize: 0x0
Number of IFaces in IFaceMap: 2
Slots in VTable: 17
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
6f9e6a90 6f86494c PreJIT System.Object.ToString()
6f9e6ab0 6f864954 PreJIT System.Object.Equals(System.Object)
6f9e6b20 6f864984 PreJIT System.Object.GetHashCode()
6fa57540 6f8649a8 PreJIT System.Object.Finalize()
0019c099 001943d0 NONE MongoDB.Mongo.Dispose()
0019c09d 001943d8 NONE MongoDB.Mongo.get_ConnectionString()
00593968 001943e0 JIT MongoDB.Mongo.GetDatabase(System.String)
00593918 001943e8 JIT MongoDB.Mongo.get_Item(System.String)
0019c0a9 001943f0 NONE MongoDB.Mongo.Connect()
0019c0ad 001943f8 NONE MongoDB.Mongo.TryConnect()
0019c0b1 00194400 NONE MongoDB.Mongo.Disconnect()
0019c08d 001943b0 NONE MongoDB.Mongo..ctor()
005901e8 001943b8 JIT MongoDB.Mongo..ctor(System.String)
00591878 001943c4 JIT MongoDB.Mongo..ctor(MongoDB.Configuration.MongoConfiguration)
0019c0b5 00194408 NONE MongoDB.Mongo.GetDatabases()
0019c0b9 00194414 NONE MongoDB.Mongo.<GetDatabases>b__1(MongoDB.Document)
0019c0bd 00194420 NONE MongoDB.Mongo.<GetDatabases>b__2(System.String)

留意JIT项,会发现很多项为NONE ,说是这些方法从未被调用过,故JIT函数从未编译这些项IL代码为CPU指令。PreJIT指这些方法被提前预编译好为本地指令。看一下下面JIT编译的相关信息    !u + md地址 查看JIT编译方法的相关信息

 

0:004> !U 001943e0
Normal JIT generated code
MongoDB.Mongo.GetDatabase(System.String)
Begin 00593968, size 5b
00593968 55 push ebp
00593969 8bec mov ebp,esp
0059396b 83ec14 sub esp,14h
0059396e 894dfc mov dword ptr [ebp-4],ecx
00593971 8955f8 mov dword ptr [ebp-8],edx
00593974 833d4036190000 cmp dword ptr ds:[193640h],0
0059397b 7405 je 00593982
0059397d e8efdb1670 call mscorwks!CorLaunchApplication+0xe99e (70701571) (JitHelp: CORINFO_HELP_DBG_IS_JUST_MY_CODE)
00593982 33d2 xor edx,edx
00593984 8955f4 mov dword ptr [ebp-0Ch],edx
00593987 90 nop
00593988 b908138f00 mov ecx,8F1308h (MT: MongoDB.MongoDatabase)
0059398d e88ae6baff call 0014201c (JitHelp: CORINFO_HELP_NEWSFAST)
00593992 8945f0 mov dword ptr [ebp-10h],eax

 

通过dumpmt -md  +MT地址 可获取该类型下的所有方法信息

0:004> !dumpmd 001943f0
Method Name: MongoDB.Mongo.Connect()
Class: 006d6d28
MethodTable: 00194438
mdToken: 060005a9
Module: 00193488
IsJitted: no
CodeAddr: ffffffff

通过dumpmd  +methoddesc地址 获取方法的信息
0:004> !DumpModule 00193488     (dumpModule +module地址)  获取模块(dll,module)信息:模块中定义的类/模块引用的类

0:004> !DumpAssembly 004727b0
Parent Domain: 00445598
Name: E:\MongGo\samus-mongodb-csharp-0.90.0.1\Demo\bin\Debug\MongoDB.dll
ClassLoader: 00472820
SecurityDescriptor: 005b5f60
Module Name
00193488 E:\MongGo\samus-mongodb-csharp-0.90.0.1\Demo\bin\Debug\MongoDB.dll
0:004> !DumpAssembly 004727b0
Parent Domain: 00445598
Name: E:\MongGo\samus-mongodb-csharp-0.90.0.1\Demo\bin\Debug\MongoDB.dll
ClassLoader: 00472820
SecurityDescriptor: 005b5f60
Module Name
00193488 E:\MongGo\samus-mongodb-csharp-0.90.0.1\Demo\bin\Debug\MongoDB.dll

从methodTable/methodDesc 可以查出module信息 从dumpModule可获得assembly的信息  dumpAssembly 查询程序集相关信息

不管是dumpmt/dumpmd  

 mdToken可以获得对应类型或方法的元数据4位字节的2进制标识

而MethodTable (元数据+IL被加载后的数据) 并且记录了所有方法的结构数据MethodsDesc(IL+CALL JIT/或者已经编译的本地CPU指令)

而MethodsDesc 则保存对应方法IL代码 以及指向JIT的地址,当首方法首先被调用,JIT会将IL编译成本地指令,并且保在MethodsDesc 中,

这样在下次调时就不必被JIT函数编译而直接执行。(我们平时代码中反射的程序集信息其实就是元数据)

下面是JIT工作的具体方式:

Calling a method for the first time:

  • Your program code calls a method Foo()
  • The CLR looks at the type that implements Foo() and gets the metadata associated with it
  • From the metadata, the CLR knows what memory address the IL (Intermediate byte code) is stored in.
  • The CLR allocates a block of memory, and calls the JIT.
  • The JIT compiles the IL into native code, places it into the allocated memory, and then changes the function pointer in Foo()'s type metadata to point to this native code.
  • The native code is ran.

Calling a method for the second time:

  • Your program code calls a method Foo()
  • The CLR looks at the type that implements Foo() and finds the function pointer in the metadata.
  • The native code at this memory location is ran.

当执行到某一个方法,CLR 根据从元数据(应该指的是MethodTable)查找到此方法地址(根据mdtoken找到MethodDesc,MethodDesc),CLR此时会从托管堆上分配空间,并且调用JIT 编译IL为本地CPU指令,并改变函数指针到这堆内存上执行本地CPU指令。

PS:在园子里看文章说debug/relased 两个模式编译的IL代码是一样的,但是IL经JIT编译本地指令的时候,released模式的指令被优化了。

抱歉!评论已关闭.