In native code, memory is explicitly managed by the program calling Malloc()/Free() (or comparable APIs). In managed code, there's a garbage collector (GC).
A GC does not mean that you no longer get memory leaks. It just means that runtime calls Free() for you at some arbitrary point after the runtime detects that the object is no longer accessible. This effectively errs on the side of safety (not freeing objects) to avoid the dangling pointer problem, which commonly results in a crash. Thus if the object is always accessible, even a GC can't release it and hence you get a memory leak. A common way this happens is to have some delegate or static reference. This is a commonly written about topic, so I don't have much to add on the base problem. Check out Maoni's or Chris Lyon's blogs for GC goodness.
The point I do want to make is that this is a paradigm shift in the sort of bugs produced.
Common bugs in native memory management:
|calling Free() twice on the same memory||Crash, undefined behavior.|
|never calling Free() (or Release())||Memory leak|
|Using a pointer after the original memory was freed (dangling pointer).||Crash, Using undefined memory, arbitrary undefined behavior|
Note: these problems scale beyond memory usage to general resource consumption, but I'm keeping a limited scope since this a blog entry and not a dissertation. (eg, If we're using C++ destructors instead of Free(), and the C++ dtor would release OS resources)
Common bugs in in a GC environment (managed code):
|Not releasing a reference||Memory leak|
|Finalizer called at random times||Native resource not released when expected. This could be a resource leak. Or it could lock resources longer than expected, preventing others from using them)
The finalizer can't be sure what state it's called in.
Since you can't call Free() in the GC world, there are whole classes of bugs you no longer need to worry about.
One way to view it is that GCs basically downgrade bugs from Crashes to Memory-leaks. From a debugger + tool's perspective, there are still bugs in the user's code and so the user still needs tools to debug them. But not the conventional tools targetted at native memory leaks don't play so well in the world of managed-memory leaks.
The malloc/free bugs are hard because the culprit may be far from the point of failure. Eg, when Free() is called twice, you may find the 2nd Free() crashes. But now you may need to find the first call to Free() which may have occurred at some arbitrary point in the past. In general, diagnosing unmanaged memory leaks usually requires logging tools that track the malloc/free calls and do analysis on them; or fancy memory page protection schemes that catch dangling pointers. (See UMDH or PageHeap)
Managed-memory leaks are conceptually easy because you can diagnose them statically from a single snapshot in time by just inspecting the current heap. Unfortunately, ICorDebug does not provide this functionality so you don't have great VS integration here. In practice, you need to use SOS.
In conclusion: So the shift from manual memory management to a GC has two major wins from a bug perspective:
- it downgrades the severity of bugs (from Crash / undefined behavior to just memory-leak)
- it produces bugs that can be easier to diagnose but requires a shift in tools paradigm to attack these new bugs.