Skip to content

The Garbage Collector

Felipe Aburaya edited this page Jan 20, 2017 · 3 revisions

A few words about C++ and memory management

Keep in mind C++ was meant to be used applying the RAII concept rather than garbage collection (hereby called GC) as the language design pursues the least possible overhead. C++ developers should stick to RAII most of the time because that is how things work well in the language. Moreover, according the modern style that came with C++11, STL smart pointers are to be applied whenever possible, as it provides great performance and productivity. That is why the framework does not obligates you to use GC. That is utter important to emphasize.

Though, in a few scenarios GC is desirable and saves considerable development time. As an example, imagine the memory management of a complex data structures (such as graphs) whose deallocation is quite delicate, once it might be hard to determine when a resource can be released not generating dangling pointers. In this case, the cost of time to develop an implementation based in RAII might make it unattractive compared to one based in GC, even considering the performance penalty of the last, and specially when performance is not a concern.

However, it is worth to remember GC also has drawbacks. Release of resources is not deterministic in garbage collected environments. So you should not delegate the release of critical resources (such as database connections or file locks) to the destructors of garbage collected objects. Also, the GC as implemented in .NET and Java is known to delay the deallocation of memory, which is the main reason most developers avoid to use these languages when developing memory hungry applications.

How the the framework implements garbage collection

The GC implementation strategy adopted by 3FD is meant to fit the cases when native code is a must and GC is as desirable as a fast memory reclaim. The framework provides a 'tracing garbage collector'. In other words, it basically analyzes the "reachability" of the managed memory addresses and free the ones that become unreachable. It provides great flexibility over reference counting because it is not vulnerable to cyclic references, so the programmer does not have to figure out when to use a "weak reference".

The garbage collector runs in a single dedicated thread per process. Every time a reference to a memory address is changed or destroyed, a message notifying the event is sent to a queue. This queue is processed by the garbage collector concurrently with the application in a cycle with a configurable interval of time (50 ms for example) and using a lock-free algorithm. The messages in the queue that deassign a memory address from a sptr object will request the garbage collector to assess the reachability of these addresses. Whenever an object becomes unreachable, its destructor will be called promptly. This is not a "stop the world" approach: garbage collection runs parallel to allocation, one not blocking the other.

This implementation guarantees to free all unreachable memory as soon as possible. Also, it relies on the use of object destructors, which means currently working code can start using GC with minimal adaptations. On the other side, it must be taken in to consideration the fact that this implementation with fast memory reclaim costs considerable CPU resources when the garbage collector is stressed with heavy loading.

Using the garbage collector

As already mentioned before, there is no obligation on using the garbage collector, so the client applications will actually work with two types of dynamic memory: regular and managed by GC (from now on called only "managed"). If you do not use GC, then it does not incur any overhead. Actually, if you do not use it, the GC facility is not even instantiated.

All managed memory is allocated from the default process heap, and can only be referred by instantiations of the sptr template class, which is a framework implementation for a "smart pointer". It is analogous, but far more flexible than C++ STL smart pointers, and use the same amount of memory and basic syntax of a C-style pointer. So, once the garbage collector implementation relies on object destructors and the syntax for data access is the traditional one, the burden of migrating from RAII to GC is only to replace the old pointers with sptr objects.

For performance reasons, the framework delegates reachability analysis (which is somewhat expensive in CPU time), memory deallocation and execution of destructors to a parallel thread. It means object destructors are called asynchronously and you cannot know when the resources will be released (aside the fact they will be released ASAP). So, despite the fast memory reclaim, resources managed directly or indirectly by the garbage collector are still released in a non-deterministic manner.

Futhermore, the garbage collector can only trace memory held by sptr objects. If a C-pointer holds a managed memory address, it does not count as a strong reference, hence the same address can be freed at any time without warning and thus yielding a dangling pointer. For this reason, sptr objects were designed to be black boxes that do not expose the held memory addresses, and client code should not have direct access to them. Not obeying this recommendations will almost certainly lead to disastrous consequences during the runtime.

The code below gives a taste of how to code using garbage collection:

#include "3FD/exceptions.h"
#include "3FD/runtime.h" // header needed to perform the framework instantiation
#include "3FD/sptr.h" // header needed to use garbage collection

using namespace _3fd::core;
using _3fd::gc::sptr;

class MyClass
{
    // ... declaration and definition of this class
}

int main()    
{
    // Create a framework instance
    FrameworkInstance framework;

    // Once the framework instance has been initialized, you can use its features:

    CALL_STACK_TRACE;

    try
    {
        // Create a sptr object
        sptr<MyClass> mySafePointer;

        // Assign a dinamically allocated object to it using the macro 'has'
        mySafePointer.has(MyClass("some text argument", true));

        // Use the safe pointer just like you use a C-Style pointer:

        if(mySafePointer->m_someIntegerMember > 478)
        {
            // ...
        }

        /* End of scope is reached. The object 'mySafePointer' will be destructed, 
        which will tell the GC that the referred memory location became inaccessible, 
        hence leading to its deallocation. */
    }
    // Catch a possible memory allocation failure:
    catch(IAppException &ex)
    {
        // ...
    }

    /* When the end of the scope that declares the framework instance is reached, 
       the instance is requested to terminate. After the GC is shutdown, garbage 
       collection becomes unavailable. */
}