Why Isn't Form Created on Stack GC'ed?

Posted by Hugh Ang at 2/01/2007 12:10:00 PM

Have you run into situations where something is really simple and intuitive so you take it for granted? But if you think harder it makes little sense. Here is an example. In a WinForm application, a form (Form1) creates and opens another form (Form2) on stack (inside a button click handler) non-modally:


 

        private void btnNewForm_Click(object sender, EventArgs e)

        {

            Form2 frm = new Form2();

            frm.Show();

        }



We'd expect Form2 to stay on the screen after the call finishes. After all, the user of the software should be the one to decide when to end the life of Form2 by closing it. But wait, isn't the reference to Form2 created on the stack gone after the function call finishes? So why are we still seeing Form2? (Note that If the form is opened as modal, this question is irrelevant as the ShowDialog() call doesn't return until the modal form is closed)

GC shouldn't bend the rules just for form objects. There must be hidden references that are not seen at programming level. A few SOS commands in the debugger indeed yield the truth for us. I have used .NET 2.0, WinDbg 6.6.0007.5 for this exercise.

After the btnNewForm_Click() function that creates the form returns, we find the Form2 instance on the managed heap:


0:000> !dumpheap -type Form2
Address MT Size
013bd650 00a26d5c 324
total 1 objects
Statistics:
MT Count TotalSize Class Name
00a26d5c 1 324 Forms.Form2
Total 1 objects


Now let's find out what are keeping references of it:


0:000> !gcroot 013bd650
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 1f4
Scan Thread 2 OSTHread c3c

DOMAIN(001545F0):HANDLE(WeakLn):a010d0:Root:013bda70(System.Windows.Forms.NativeMethods+WndProc)->
013bd7a4(System.Windows.Forms.Control+ControlNativeWindow)->
013bd650(Forms.Form2)


DOMAIN(001545F0):HANDLE(Strong):a01160:Root:013bd650(Forms.Form2)

DOMAIN(001545F0):HANDLE(Pinned):a013ec:Root:02384b68(System.Object[])->
0139c948(System.Collections.Generic.Dictionary`2[[System.Object, mscorlib],[System.Collections.Generic.List`1[[Microsoft.Win32.SystemEvents+SystemEventInvokeInfo, System]], mscorlib]])->
0139d100(System.Collections.Generic.Dictionary`2+Entry[[System.Object, mscorlib],[System.Collections.Generic.List`1[[Microsoft.Win32.SystemEvents+SystemEventInvokeInfo, System]], mscorlib]][])->
0139e27c(System.Collections.Generic.List`1[[Microsoft.Win32.SystemEvents+SystemEventInvokeInfo, System]])->
0139e2a4(System.Object[])->
013bdb30(Microsoft.Win32.SystemEvents+SystemEventInvokeInfo)->
013bdb10(Microsoft.Win32.UserPreferenceChangedEventHandler)->
013bd650(Forms.Form2)

DOMAIN(001545F0):HANDLE(Pinned):a013f0:Root:02383030(System.Object[])->
013a10e0(System.Windows.Forms.FormCollection)->
013a10f8(System.Collections.ArrayList)->
013a1110(System.Object[])->
013bd650(Forms.Form2)


DOMAIN(001545F0):HANDLE(WeakSh):a01808:Root:013bd7a4(System.Windows.Forms.Control+ControlNativeWindow)
DOMAIN(001545F0):HANDLE(WeakSh):a0180c:Root:013bd7a4(System.Windows.Forms.Control+ControlNativeWindow)


We can ignore the weak references (WeakSh and WeakLn) and concentrate on the Strong and Pinned(indirectly) ones as they are the reasons that Form2 instance will not be GC'ed. The strong reference is actually a GCHandle (a01160) tied to the Form2 instance. Before the Form2 instance was created I added a breakpoint at call to System.Runtime.InteropServices.GCHandle.Alloc using !SOS.bpmd command. When the breakpoint was hit, I examined the CLR stack:


0:000> !clrstack -p
OS Thread Id: 0x1f4 (0)
ESP EIP
0012ef28 79361fc4 System.Runtime.InteropServices.GCHandle.Alloc(System.Object, System.Runtime.InteropServices.GCHandleType)
PARAMETERS:
value = 0x013bd650
type = 0x00000002

0012ef2c 7b0783cd System.Windows.Forms.Control+ControlNativeWindow.LockReference(Boolean)
PARAMETERS:
this = 0x013bd7a4
locked =

0012ef38 7b06ca41 System.Windows.Forms.Control.UpdateRoot()
PARAMETERS:
this =

0012ef40 7b073366 System.Windows.Forms.Control.SetVisibleCore(Boolean)
PARAMETERS:
this =
value =

0012efe8 7b06382b System.Windows.Forms.Form.SetVisibleCore(Boolean)
PARAMETERS:
this = 0x013bd650
value = 0x00000001

0012f028 7b0cf369 System.Windows.Forms.Control.Show()
PARAMETERS:
this =

0012f02c 00f50423 Forms.Form1.btnNewForm_Click(System.Object, System.EventArgs)
PARAMETERS:
this = 0x01382ce4
sender = 0x0139c18c
e = 0x013bb798

...


I have omitted the bottom frames for brevity. Showing the form eventually calls Control.SetVisibleCore(), which then calls ControlNativeWindow.LockReference(), which simply allocates a GC handle for the form instance as shown in the source code via reflector:



 

        internal void LockReference(bool locked)

        {

            if (locked)

            {

                if (!this.rootRef.IsAllocated)

                {

                    this.rootRef = GCHandle.Alloc(this.GetControl(), GCHandleType.Normal);

                }

            }

            else if (this.rootRef.IsAllocated)

            {

                this.rootRef.Free();

            }

        }




According to the framework documentation, GCHandle is created corresponding to a managed object and can be used to prevent GC of the object when it's used by unmanaged client. In this case though, ControlNativeWindow's private variable rootRef, which holds the handle, does nothing but holding on tight to the form object! That's probably the name LockReference :-) To confirm that this is indeed the handle that SOS reports to us in the debugger, I stepped into the code that creates the GC handle. First I set a breakpoint at GCHandle.Alloc. When it's hit, I disassembled the instruction pointer:


0:000> !u eip
preJIT generated code
System.Runtime.InteropServices.GCHandle.Alloc(System.Object, System.Runtime.InteropServices.GCHandleType)
Begin 79361fc4, size 39
>>> 79361fc4 57 push edi
79361fc5 56 push esi
79361fc6 53 push ebx
79361fc7 50 push eax
79361fc8 8bd9 mov ebx,ecx
79361fca 8bfa mov edi,edx
79361fcc bae4040000 mov edx,4E4h
79361fd1 b901000000 mov ecx,1
79361fd6 e8f51ab100 call mscorwks!JIT_Writeable_Thunks_Buf+0x1a0 (79e73ad0) (JitHelp: CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE)
79361fdb 8d3424 lea esi,[esp]
79361fde 8bd7 mov edx,edi
79361fe0 8bcb mov ecx,ebx
79361fe2 e80da4b800 call mscorwks!MarshalNative::GCHandleInternalAlloc (79eec3f4)
79361fe7 8906 mov dword ptr [esi],eax
79361fe9 83ff03 cmp edi,3
79361fec 7507 jne mscorlib_ni+0x2a1ff5 (79361ff5)
79361fee 8bce mov ecx,esi
79361ff0 e80f380100 call mscorlib_ni+0x2b5804 (79375804) (System.Runtime.InteropServices.GCHandle.SetIsPinned(), mdToken: 06002ff9)
79361ff5 8b0424 mov eax,dword ptr [esp]
79361ff8 59 pop ecx
79361ff9 5b pop ebx
79361ffa 5e pop esi
79361ffb 5f pop edi
79361ffc c3 ret


Note that I have highlighted the assembly code following the call to GCHandleInternalAlloc(). So then I stepped to that instruction 79361fe7:


0:000> pa 79361fe7
79361fe2 e80da4b800 call mscorwks!MarshalNative::GCHandleInternalAlloc (79eec3f4)
eax=00a01160 ebx=013bd650 ecx=79eec4fe edx=013bd650 esi=0012ef18 edi=00000002
eip=79361fe7 esp=0012ef18 ebp=0012efe0 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
mscorlib_ni+0x2a1fe7:
79361fe7 8906 mov dword ptr [esi],eax ds:0023:0012ef18=00000000


For brevity I have omitted instructions up to the point where GCHandleInternalAlloc() is called. The instruction is now at 79361fe7. The return value, which is the GC handle, should be in the EAX register:


0:000> ?eax
Evaluate expression: 10490208 = 00a01160


Note that the hex value a01160 matches exactly what SOS says.

The other two references to the form instance are 1) a delegate (UserPreferenceChangedEventHandler) for system wide event notification - the name is obvious and 2) a collection FormCollection kept by System.Windows.Forms.Application.Application, exposed through a static property called OpenForms.

When the form is being closed, the UserPreferenceChangedEventHandler is removed (via Control.OnHandleDestroyed), Form is removed from the Application.OpenFormsInternal (via Form.OnHandleDestroyed). As to the GCHandle, ControlNativeWindow.rootRef, is freed by LockReference again, with locked=false as seen in the following call stack:


0:000> !clrstack -p
OS Thread Id: 0x2bc (0)
ESP EIP
0012e43c 7b078390 System.Windows.Forms.Control+ControlNativeWindow.LockReference(Boolean)
PARAMETERS:
this = 0x01388a14
locked = 0x00000000

0012e440 7b06ca41 System.Windows.Forms.Control.UpdateRoot()
PARAMETERS:
this =

0012e448 7b06d6ea System.Windows.Forms.Control.SetHandle(IntPtr)
PARAMETERS:
this =
value =

0012e454 7b07a484 System.Windows.Forms.Control+ControlNativeWindow.OnHandleChange(IntPtr)
PARAMETERS:
this =
newHandle =

0012e458 7b07a46e System.Windows.Forms.Control+ControlNativeWindow.OnHandleChange()
PARAMETERS:
this =

0012e45c 7b0897df System.Windows.Forms.NativeWindow.ReleaseHandle(Boolean)
PARAMETERS:
this = 0x01388a14
handleValid =

0012e490 7b07a55a System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr)
PARAMETERS:
this = 0x01388a14
hWnd =
msg = 0x00000082
wparam =
lparam =

0012e664 003420d4 [NDirectMethodFrameStandalone: 0012e664] System.Windows.Forms.UnsafeNativeMethods.IntDestroyWindow(System.Runtime.InteropServices.HandleRef)
0012e67c 7b0892ee System.Windows.Forms.UnsafeNativeMethods.DestroyWindow(System.Runtime.InteropServices.HandleRef)
PARAMETERS:
hWnd =

0012e688 7b0891f8 System.Windows.Forms.NativeWindow.DestroyHandle()
PARAMETERS:
this = 0x01388a14

0012e6cc 7b05f010 System.Windows.Forms.Control.DestroyHandle()
PARAMETERS:
this = 0x013888c0

0012e6d0 7b08901e [InlinedCallFrame: 0012e6d0]
0012e7a0 7b05c583 System.Windows.Forms.Form.Dispose(Boolean)
PARAMETERS:
this = 0x013888c0
disposing =

0012e7b4 00f505ac Forms.Form2.Dispose(Boolean)
PARAMETERS:
this = 0x013888c0
disposing = 0x00000001

0012e7c8 7a4a6633 System.ComponentModel.Component.Dispose()
PARAMETERS:
this = 0x013888c0

0012e7d0 7b224d92 System.Windows.Forms.Form.WmClose(System.Windows.Forms.Message ByRef)
PARAMETERS:
this =
m =

0012e7fc 7b063e7d System.Windows.Forms.Form.WndProc(System.Windows.Forms.Message ByRef)
PARAMETERS:
this =
m =

0012e80c 7b07a72d System.Windows.Forms.Control+ControlNativeWindow.OnMessage(System.Windows.Forms.Message ByRef)
PARAMETERS:
this =
m =

0012e810 7b07a706 System.Windows.Forms.Control+ControlNativeWindow.WndProc(System.Windows.Forms.Message ByRef)
PARAMETERS:
this =
m =

0012e824 7b07a515 System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr)
PARAMETERS:
this = 0x01388a14
hWnd =
msg = 0x00000010
wparam =
lparam =

0012ec04 003420d4 [InlinedCallFrame: 0012ec04] System.Windows.Forms.UnsafeNativeMethods.CallWindowProc(IntPtr, IntPtr, Int32, IntPtr, IntPtr)
0012ec00 7b07a7d4 System.Windows.Forms.NativeWindow.DefWndProc(System.Windows.Forms.Message ByRef)
PARAMETERS:
this =
m = 0x0012ed3c

0012ec44 7b05ed83 System.Windows.Forms.Form.DefWndProc(System.Windows.Forms.Message ByRef)
PARAMETERS:
this =
m =

0012ec48 7b072abc [InlinedCallFrame: 0012ec48]
0012ec90 7b07f795 [InlinedCallFrame: 0012ec90]

1 comments:

Anonymous said...

If you call form.Dispose the form will be garbage collected. (I see no reason why this extra step is required.)