Garbage collection (by )

The next level of sophistication avoids the need for the two semispaces - no more unnecessary moving of memory blocks, and you can use all of your RAM.

The technique is called mark-sweep; the idea is that when memory is running low, the garbage collector is invoked, as with the semispaces. However, what the collector does this time is to start off by marking memory blocks that are known to be in use, then following any references in them to find other blocks in use and marking them as such, and continuing the process until there are no more references left to follow.

This is pretty similar to semispaces so far. The difference is that the collector now scans the entire list of memory blocks, and any that are not marked get freed.

This has all the advantages of semispaces, except that it does not handle the allocation and freeing of memory itself - you need some underlying system to keep track of what memory is in use and what isn't - so you don't get the efficiency of just moving a marker to allocate memory. However, all of your memory can be used since it doesn't need semispaces, and the collection cycle does not involve time-consuming copying, just marking memory blocks.

But it's still a whole bunch of work to pause the system to do when memory runs low. One approach to this is to have the collection cycle running continuously; every time the system has no more pressing tasks, a bit of marking and sweeping can be done. The one caveat is that the running program can mess things up; if an object that has already been marked takes on a reference to a memory block that was referred to by an as yet unmarked object, but the unmarked object loses its reference before the collection cycle reaches it, the referenced object will never be marked, and will be swept. Therefore, one has to add an extra operation; whenever taking a reference to an object, mark it. This smacks slightly of referencing counting, but isn't half as bad.

The other flaw is that the marking and sweeping are separate actions - and no memory is released until the sweeping. If the system runs out of memory, it must wait for the marking to complete, and the sweep to begin, before memory allocation requests become possible again.

And that's the general state of the art. That's what most Java implementations use. Microsoft's .NET runtime uses a variant - it's mark-sweep, but marked objects are moved down into free areas near the start of memory to "compact" them together and make the free memory all one block for fast marker-moving allocation. A few systems still use reference counting, and warn programmers not to create loops.

But somehow, I felt this wasn't the pinnacle of technology it could be...

Pages: 1 2 3 4 5 6 7

4 Comments

  • By Faré, Fri 18th Nov 2005 @ 3:21 am

    So as to be able to move your object to the top of the list, yet preserve the invariant that no object points to a newer object, you need to also move up all the objects that point to your born-again object, and so on recursively. In practice, this is only affordable if your object has no one pointing to it except the current context -- in which case what you have is a linearity constraint on objects.

  • By Alaric Snell-Pym, Fri 18th Nov 2005 @ 10:31 am

    Ah, but I do have a linearity constraint on objects 😉 That's why I was amused by the similarity to "copy on write" handling of tree structures on disk, which basically works the same way (to safely update a tree structure like a B-Tree, make copies of the changed leaf nodes into empty space, then work out what intermediate nodes would need changing to reflect the new locations of the leaf nodes and do the same with them, then continue bubbling the change up the tree until you have a new root pointer, etc).

    I'm sure there's some Fundamental Truth in the fact that the same underlying technique turns out to be useful both on disk and in memory, yet in very different contexts (ACID properties on disk, fast garbage collection in RAM).

  • By Alaric Snell-Pym, Sun 20th Nov 2005 @ 2:33 pm

    Oooh, while sawing up logs (a great time for thinking about abstract stuff) I was struck by a flaw...

    When a memory block is modified and gets brought up to the head of the chain, IF the collector has not yet reached the block in this pass, then the blocks referenced by this block will not get marked since the collector will then not visit the block in question until the next pass. If nothing else refers to the same blocks, they'll be freed. Oops!

    So we need to make sure that a block moved to the top of the chain still gets seen. My first thought was that the application could just quickly scan the block and mark all the referenced blocks, but that's wrong - it's the collector's job.

    So my next thought was to have a (either shared and lock-free, or per-processor, to stop it from becoming a point of contention in SMP systems) stack of 'touched' objects; when altering an object, the application would merely need to push a reference onto this stack (it wouldn't even need to do the move to the head of the chain). Now, the collector, whenever it's about to examine the next object in the chain, would first look on the stack(s) and go through any memory blocks on them, marking all the referenced blocks. That way, it will never be considering a block for freeing unless it has already 'scanned' all modified objects, so there's no chance of it mistakenly freeing something. Whenever the collector has scanned a block from the stack, it can then also do the chore of moving that block to the head of the chain, moving the task from the application code.

    However, there is a problem - an application that just sits there modifying the same large array of pointers over and over again would keep the collector forever rescanning that large array; never getting any real collection done. What we need is to only stack memory blocks for scanning if they've not yet been scanned anyway. This is easily resolved; have a 'scanned' flag in each block, that the collector sets whenever it scans a block, be it due to the block being on the stack or by traversing the chain. Newly allocated objects also have the 'scanned' flag set, since all the objectss they refer to must have been reachable anyway, and thus will be marked - they don't need rescanning until the next pass. When the collector finishes scanning the chain and is about to start again, it has to clear all the 'scanned' flags; but rather than walking the chain doing this, it's easier to just reverse the interpretation of the 'scanned' flag. Then newly created blocks will need to be marked as 'unscanned' for the next scan.

    There's a potential race condition in that if the collector changes the global variable that says what newly created blocks should be marked as between the application reading the current setting and the application putting the newly created block at the head of the chain, it could end up with the wrong setting. Therefore, before doing the swap, the collector should take a copy of the pointer to the current head of the chain; then when it starts its walk of the chain, it should force the correct value into all the blocks it examines until it hits the point in the chain it marked, this way ensuring nothing gets missed.

Other Links to this Post

  1. Snell-Pym » Garbage collection — Tue 31st Jul 2007 @ 5:56 pm

RSS feed for comments on this post. TrackBack URI

Leave a comment

WordPress Themes

Creative Commons Attribution-NonCommercial-ShareAlike 2.0 UK: England & Wales
Creative Commons Attribution-NonCommercial-ShareAlike 2.0 UK: England & Wales