Like native code, managed code supports two major options for memory allocations: a stack and a heap. The managed stack has a lot of similarities to the native stack. Both stacks contain stack frames (also called activation records) to store data that is bound to a method call, like parameters, local variables, and temporary memory. In fact, the CLR implements the managed stack mainly in terms of the native stack. Due to the similarities between the native and managed stacks, it is often sufficient to see both concepts as one. However, sometimes an explanation of certain internals, like the garbage collection and .NET's security model, requires differentiating them.
Similar to the C++ free store (new/delete) and the heap (malloc/free), the lifetime of a memory allocation on the managed heap is not bound to a method call or a scope within a method call. The lifetime of a memory allocation on the managed heap is controlled by a garbage collector (GC). Therefore, this managed heap is also referred to as the "garbage-collected" heap, or simply the "GC" heap.
To differentiate allocations on the native heap from allocations on the managed heap, the operator gcnew is used. The following program creates a new instance of System::String with the value aaaaaaaaaa on the GC heap, and writes its value to the console:
// compile with "cl /clr gcnew.cpp"
System::Console::WriteLine(gcnew System::String(L'a', 10));
Only instances of managed types can be allocated on the GC heap. Trying to instantiate a native type like std::string via gcnew will cause a compiler error. However, since primitives are managed types in the managed compilation model, they can be instantiated on the managed heap, too. The following expression is legal if you compile with /clr:
Since the managed compilation model can treat primitives as native primitives if the actual context requires this, the following expression is also legal if you compile to managed code:
When a local variable is supposed to refer to an object on the GC heap, a simple native pointer is not sufficient. The following line of code is illegal:
A native pointer could easily be copied into an unmanaged memory location. The copied pointer would be outside of the runtime's control. This would conflict with the requirements of .NET's GC—to decide whether an object's memory can be reclaimed or not, the GC must be aware of all variables referring to the GC heap.
The CLR implements the GC heap with a compacting algorithm. Instead of managing fragments of deallocated memory, objects can be relocated during a garbage collection so that one object follows the next and there is one free memory block at the end of the heap. To ensure that the moved objects are still accessible, the runtime must update all variables referring to relocated objects. This is another reason why the GC must be aware of all references.
Although this implementation strategy sounds like a huge amount of work during a garbage collection, a defragmenting GC can be very helpful for implementing performant and scalable applications. Allocating memory on the GC heap is an extremely scalable operation compared to unmanaged allocation, particularly in scenarios in which many threads allocate memory synchronously. The GC heap maintains a pointer to the start of the free memory area. To allocate memory on the GC heap, it is often enough to block other allocating threads only for a very short time. In a simplified view, it is sufficient to block other threads only to save the free memory pointer into a temporary variable and to increment the free memory pointer. The temporarily saved pointer can then act as the pointer to the allocated memory.
Since a native pointer is not sufficient to refer to a location on the GC heap, another kind of variable is introduced by C++/CLI. It is called a tracking handle, because the GC keeps track of variables of this kind. Instead of an asterisk, a caret (A) is used to define a tracking handle:
In the same way, a handle to the String object can be stored in a local variable:
System::StringA str = gcnew System::String(L'a', 10);
A tracking handle either refers to an object on the GC heap, or is a variable referring to nothing. To avoid confusions with the value 0, the keyword nullptr has been introduced for this null value.
System::StringA str = nullptr;
The keyword nullptr can also be used to check if a tracking handle refers to an object or not:
bool bRefersToAnObject = (str != nullptr);
As an alternative, you can use this construct: bool bRefersToAnObject = !str;
There are significant differences between native pointers and tracking handles. A tracking handle can only be used as a simple handle to an object—for example, to call a method of an object. Its binary value must not be used by your code. You cannot perform pointer arithmetic on a tracking handle. Allowing pointer arithmetic would imply that a programmer can control internals of the GC—for example, the order in which objects are allocated on the GC heap. Even if a thread creates two objects in two continuous operations, a different thread can create an object that is allocated between the two objects. A garbage collection can also be implemented so that the order of the objects can change during a garbage collection, or the memory for new objects can be allocated so that newer objects are allocated at lower addresses. All this is outside of the programmer's control.
Although the concept of tracking handles differs from the concept of native pointers, there are a lot of similarities. As with native pointers, the size of a tracking handle is platform dependent. On a 32-bit CLR, a tracking handle's size is 32 bits; on a 64-bit CLR, it is 64 bits. In fact, both native pointers and tracking handles store addresses in the virtual memory of the process. In the current implementation of the CLR (version 2.0), a tracking handle points to the middle of an object's header. Figure 2-2 shows that an object header is 8 bytes long: 4 bytes for a type identifier, 4 bytes for flags, and some other object-specific data that the CLR uses to provide different services. The actual member variables of the object follow the object header.
Flags,... Type Identifier Field 1 Field 2
Figure 2-2. A tracking handle referring to a managed object
The type identifier shown in Figure 2-2 can be compared to the vtable pointer of a C++ object. However, it is used not only for method dispatching and dynamic casting, but also for certain other services of the runtime environment. For this chapter, it is sufficient to know that an object has an 8-byte object header followed by its fields, as shown in Figure 2-2.
Since a tracking handle has certain similarities to a native pointer, C++/CLI uses the arrow operator, ->, to access an object via a tracking reference:
System::StringA strUpper = str->ToUpper();
Was this article helpful?