How the Boehm Garbage Collector Works
discontinuously.com
discontinuously.com
As far as crafting input to match allocated addresses, that is an interesting idea. One could, at the very least, create data that lies within the heap's address range. The issue is, you'd need a lot of data to blacklist a significant amount of memory, and if that's the case why not just DoS with a ton of data?
In practice, a fragmented heap is probably a much bigger issue than actual leaks. As the article points out, a conservative GC cannot perform compaction.
Interior pointers can be a perfectly reasonable thing to have. For example, many, if not most, implementations of multiple inheritance rely on different values for the 'this' pointer depending on the type of 'this', where that type is somewhere up the inheritance graph. For a class C : public A, public B {}, the physical value of A * = new C and B * = new C will usually be different. The way this is often implemented is by having the addresses of the various vtables for different ancestors stored in the object data, and the conversion from the descendant class to one of the ancestor class returns an interior pointer to one of these vtable locations inside the object data.
>The way this is often implemented is by having the addresses of the various vtables for different ancestors stored in the object data, and the conversion from the descendant class to one of the ancestor class returns an interior pointer to one of these vtable locations inside the object data.
Suggests that a pointer value would still exist that would reference the beginning of the allocation block, stored in the vtable so it would still avoid erroneous collection.
Edit: but you are right that there are some valid reasons to have an interior pointer, which is why the GC has the friendly macros.
C++ terms:
class C : public A, public B {};
Typical class layout, in C terms: struct A
{
// exists assuming virtual methods on A
// the data this vtable points to is read-only but C++ ctor semantics requires
// updating vtable pointer during inherited constructor calls
A_vtable *vtable;
// A data
};
struct B
{
B_vtable *vtable;
};
struct C
{
C_vtable *vtable; // incorporates A's vtable
// struct A a; data members of A but without vtable
struct B b;
};
A pointer to an instance of C, but typed as B * , will point at the structure contained in C; it will be an interior pointer. There's no reason to expect that there will be another live pointer of type C * pointing to the object; a pointer of type C * needs to be sufficient. Typecasting back down the hierarchy to C * again (with dynamic_cast) will need to adjust the pointer to the start of the object; this normally requires RTTI, and may be done by e.g. storing the offset of the vtable inside the object in the vtable itself (perhaps at a negative offset).The above is all implementation dependent, of course, but what I've described above maps almost exactly to how Delphi implements COM interfaces (where A would be the base class and B would be a pure virtual class, or interface - no data members).
To leak memory is to fail to deallocate it at all.
When I write any of these, the only time I free anything is when I've had to malloc something huge.
About once a year, we'd lose a week or two to tracking down a memory issue. While the root cause was never a GC bug, the GC would make it dramatically more difficult to figure out what the real problem was.
The closest we had to an actual GC bug was a leak involving a data structure containing a list of IP addresses that was incorrectly interpreted as containing pointers. It took forever to figure out what had happened, since the issue only appeared if you worked with a specific set of IP addresses--and even after we had a replication case, the GC made it extremely difficult to determine why memory was growing rapidly. The fix was simple once we identified the precise problem--flag that memory as "atomic" (i.e., guaranteed not to contain pointers)--but finding the precise data structure that was holding memory live was a nightmare.
We eventually ran into a slow memory growth issue that we couldn't figure out. After much debugging, one developer became fed up with the situation and tore the GC out. It took him about two weeks to produce a version of the codebase that leaked less than the GC-enabled one, and another couple weeks to eliminate virtually all memory leaks. We continued to find minor leaks for another several months, none of which were difficult to correct. (We used a debugging malloc library that flagged leaked memory on exit, so we always knew exactly where the leak had originated.)
Not only did the GC-less version leak less, it used about 40% less memory.
I would strongly recommend against using the Boehm collector for any long-running processes. The tradeoffs may be more acceptable in short-term processes where slow leakage over time is unimportant.
Once or twice a tear, we'd have an unbounded memory growth to trace. Most of the time, we were able to solve it by providing layouts for structures that mixed pointers and pointer-like data. The debug facility it provides for providing a random backtrace to a root was helpful in those cases.
However, more than once we had a leak that it was beyond our ability to trace, despite a significant amount of diagnostic work. We found some workarounds that were specific to our app, which helped.
In the end, the costs to us were high enough that we rewrote our codebase in C# (our software was Windows-only). It was pretty much a line-for-line conversion, but it worked and we no longer had any memory leaks (except for one, in .NET timers, that took only minutes to trace using readily-available diagnostic tools).
I do believe a 64-bit process using the Boehm collector would be much less likely to suffer these issues, but this was before x64 was prevalent in corporate server rooms.
On your second question: Crafted input can indeed result in significant memory retention, if you aren't careful to exclude user input from the GC and have large, cyclic structures to collect, on a 32-bit machine, with almost any text encoding.
I'm getting the impression that posts here imply you're better off without a GC, I'm not sure if that's the intention, but it strikes me as wrong.
Your message does betray some confusion about garbage collection. The basic principle of GC is that you do not deallocate RAM manually (and, under most GCs, cannot), and programs written under it expect deallocation to happen automatically. You are not 'supposed to' free manually, as that would be wasted effort, and some standard configurations of the Boehm collector actually ignore all calls to free().
There is a very useful description of garbage collection at http://blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047... which includes and expands on the useful mental model that "garbage collection is simulating a computer with an infinite amount of memory".
This isn't Boehm's fault, specifically; any conservative GC would have similar trouble. You can't get completely away from cooperating with the GC and have good GC both.
However, it's worth noting that you can't easily mark/annotate all data structures, especially when large third-party libraries are involved, and it's not guaranteed to solve the problem. Implying otherwise might dangerously mislead people.
As an example, look at Mono, which gives the GC very precise information [1]. Even so, at least some projects suffer ever-increasing memory usage when run under it [2], which the Mono team are fixing by writing their own garbage collector.
[1] http://www.mono-project.com/Mono:Runtime#Garbage_Collection [2] http://www.mono-project.com/FAQ:_ASP.NET#Memory_Usage
Failing to stop the world (or ensure all pointers are in memory rather than registers) can result in live objects being collected. This happened to Ruby at one point: http://timetobleed.com/the-broken-promises-of-mrireeyarv/
Edit: This boils down to breaking the compiler's model of memory as specified by the C standard; all bets are off.
malloc() has overhead too, typically immediately before the pointer it returns there's data about the allocation, such as the size of the alloc, magic for detecting bad free()'s, etc (though I guess a simple slab allocator would just need a single bit for each allocation, plus a pointer per slab).
Can anyone enlighten me? Is it only the stack that is checked because that seems possible in a reasonable time.
It checks both stack and heap.
A GC works by finding all objects that are reachable (referenced by some other object or a root), then discarding all other objects. So a cyclic data structure is completely discarded as soon as there are no live references to any of its objects.
As usual, Wikipedia has lots more information: http://en.wikipedia.org/wiki/Garbage_collection_(computer_sc...
GC roots are generally things like: static/global variables, active stack frames (function parameters and local variable), CPU registers.