|
| 1 | +# Oilpan: C++ Garbage Collection |
| 2 | + |
| 3 | +Oilpan is an open-source garbage collection library for C++ that can be used stand-alone or in collaboration with V8's JavaScript garbage collector. |
| 4 | +Oilpan implements mark-and-sweep garbage collection (GC) with limited compaction (for a subset of objects). |
| 5 | + |
| 6 | +**Key properties** |
| 7 | + |
| 8 | +- Trace-based garbage collection; |
| 9 | +- Incremental and concurrent marking; |
| 10 | +- Incremental and concurrent sweeping; |
| 11 | +- Precise on-heap memory layout; |
| 12 | +- Conservative on-stack memory layout; |
| 13 | +- Allows for collection with and without considering stack; |
| 14 | +- Non-incremental and non-concurrent compaction for selected spaces; |
| 15 | + |
| 16 | +See the [Hello World](https://chromium.googlesource.com/v8/v8/+/main/samples/cppgc/hello-world.cc) example on how to get started using Oilpan to manage C++ code. |
| 17 | + |
| 18 | +Oilpan follows V8's project organization, see e.g. on how we accept [contributions](https://v8.dev/docs/contribute) and [provide a stable API](https://v8.dev/docs/api). |
| 19 | + |
| 20 | +## Threading model |
| 21 | + |
| 22 | +Oilpan features thread-local garbage collection and assumes heaps are not shared among threads. |
| 23 | +In other words, objects are accessed and ultimately reclaimed by the garbage collector on the same thread that allocates them. |
| 24 | +This allows Oilpan to run garbage collection in parallel with mutators running in other threads. |
| 25 | + |
| 26 | +References to objects belonging to another thread's heap are modeled using cross-thread roots. |
| 27 | +This is even true for on-heap to on-heap references. |
| 28 | + |
| 29 | +## Heap partitioning |
| 30 | + |
| 31 | +Oilpan's heaps are partitioned into spaces. |
| 32 | +The space for an object is chosen depending on a number of criteria, e.g.: |
| 33 | + |
| 34 | +- Objects over 64KiB are allocated in a large object space |
| 35 | +- Objects can be assigned to a dedicated custom space. |
| 36 | + Custom spaces can also be marked as compactable. |
| 37 | +- Other objects are allocated in one of the normal page spaces bucketed depending on their size. |
| 38 | + |
| 39 | +## Precise and conservative garbage collection |
| 40 | + |
| 41 | +Oilpan supports two kinds of GCs: |
| 42 | + |
| 43 | +1. **Conservative GC.** |
| 44 | +A GC is called conservative when it is executed while the regular native stack is not empty. |
| 45 | +In this case, the native stack might contain references to objects in Oilpan's heap, which should be kept alive. |
| 46 | +The GC scans the native stack and treats the pointers discovered via the native stack as part of the root set. |
| 47 | +This kind of GC is considered imprecise because values on stack other than references may accidentally appear as references to on-heap object, which means these objects will be kept alive despite being in practice unreachable from the application as an actual reference. |
| 48 | + |
| 49 | +2. **Precise GC.** |
| 50 | +A precise GC is triggered at the end of an event loop, which is controlled by an embedder via a platform. |
| 51 | +At this point, it is guaranteed that there are no on-stack references pointing to Oilpan's heap. |
| 52 | +This means there is no risk of confusing other value types with references. |
| 53 | +Oilpan has precise knowledge of on-heap object layouts, and so it knows exactly where pointers lie in memory. |
| 54 | +Oilpan can just start marking from the regular root set and collect all garbage precisely. |
| 55 | + |
| 56 | +## Atomic, incremental and concurrent garbage collection |
| 57 | + |
| 58 | +Oilpan has three modes of operation: |
| 59 | + |
| 60 | +1. **Atomic GC.** |
| 61 | +The entire GC cycle, including all its phases (e.g. see [Marking](#Marking-phase) and [Sweeping](#Sweeping-phase)), are executed back to back in a single pause. |
| 62 | +This mode of operation is also known as Stop-The-World (STW) garbage collection. |
| 63 | +It results in the most jank (due to a single long pause), but is overall the most efficient (e.g. no need for write barriers). |
| 64 | + |
| 65 | +2. **Incremental GC.** |
| 66 | +Garbage collection work is split up into multiple steps which are interleaved with the mutator, i.e. user code chunked into tasks. |
| 67 | +Each step is a small chunk of work that is executed either as dedicated tasks between mutator tasks or, as needed, during mutator tasks. |
| 68 | +Using incremental GC introduces the need for write barriers that record changes to the object graph so that a consistent state is observed and no objects are accidentally considered dead and reclaimed. |
| 69 | +The incremental steps are followed by a smaller atomic pause to finalize garbage collection. |
| 70 | +The smaller pause times, due to smaller chunks of work, helps with reducing jank. |
| 71 | + |
| 72 | +3. **Concurrent GC.** |
| 73 | +This is the most common type of GC. |
| 74 | +It builds on top of incremental GC and offloads much of the garbage collection work away from the mutator thread and on to background threads. |
| 75 | +Using concurrent GC allows the mutator thread to spend less time on GC and more on the actual mutator. |
| 76 | + |
| 77 | +## Marking phase |
| 78 | + |
| 79 | +The marking phase consists of the following steps: |
| 80 | + |
| 81 | +1. Mark all objects in the root set. |
| 82 | + |
| 83 | +2. Mark all objects transitively reachable from the root set by calling `Trace()` methods defined on each object. |
| 84 | + |
| 85 | +3. Clear out all weak handles to unreachable objects and run weak callbacks. |
| 86 | + |
| 87 | +The marking phase can be executed atomically in a stop-the-world manner, in which all 3 steps are executed one after the other. |
| 88 | + |
| 89 | +Alternatively, it can also be executed incrementally/concurrently. |
| 90 | +With incremental/concurrent marking, step 1 is executed in a short pause after which the mutator regains control. |
| 91 | +Step 2 is repeatedly executed in an interleaved manner with the mutator. |
| 92 | +When the GC is ready to finalize, i.e. step 2 is (almost) finished, another short pause is triggered in which step 2 is finished and step 3 is performed. |
| 93 | + |
| 94 | +To prevent a user-after-free (UAF) issues it is required for Oilpan to know about all edges in the object graph. |
| 95 | +This means that all pointers except on-stack pointers must be wrapped with Oilpan's handles (i.e., Persistent<>, Member<>, WeakMember<>). |
| 96 | +Raw pointers to on-heap objects create an edge that Oilpan cannot observe and cause UAF issues |
| 97 | +Thus, raw pointers shall not be used to reference on-heap objects (except for raw pointers on native stacks). |
| 98 | + |
| 99 | +## Sweeping phase |
| 100 | + |
| 101 | +The sweeping phase consists of the following steps: |
| 102 | + |
| 103 | +1. Invoke pre-finalizers. |
| 104 | +At this point, no destructors have been invoked and no memory has been reclaimed. |
| 105 | +Pre-finalizers are allowed to access any other on-heap objects, even those that may get destructed. |
| 106 | + |
| 107 | +2. Sweeping invokes destructors of the dead (unreachable) objects and reclaims memory to be reused by future allocations. |
| 108 | + |
| 109 | +Assumptions should not be made about the order and the timing of their execution. |
| 110 | +There is no guarantee on the order in which the destructors are invoked. |
| 111 | +That's why destructors must not access any other on-heap objects (which might have already been destructed). |
| 112 | +If some destructor unavoidably needs to access other on-heap objects, it will have to be converted to a pre-finalizer. |
| 113 | +The pre-finalizer is allowed to access other on-heap objects. |
| 114 | + |
| 115 | +The mutator is resumed before all destructors have ran. |
| 116 | +For example, imagine a case where X is a client of Y, and Y holds a list of clients. |
| 117 | +If the code relies on X's destructor removing X from the list, there is a risk that Y iterates the list and calls some method of X which may touch other on-heap objects. |
| 118 | +This causes a use-after-free. |
| 119 | +Care must be taken to make sure that X is explicitly removed from the list before the mutator resumes its execution in a way that doesn't rely on X's destructor (e.g. a pre-finalizer). |
| 120 | + |
| 121 | +Similar to marking, sweeping can be executed in either an atomic stop-the-world manner or incrementally/concurrently. |
| 122 | +With incremental/concurrent sweeping, step 2 is interleaved with mutator. |
| 123 | +Incremental/concurrent sweeping can be atomically finalized in case it is needed to trigger another GC cycle. |
| 124 | +Even with concurrent sweeping, destructors are guaranteed to run on the thread the object has been allocated on to preserve C++ semantics. |
| 125 | + |
| 126 | +Notes: |
| 127 | + |
| 128 | +* Weak processing runs only when the holder object of the WeakMember outlives the pointed object. |
| 129 | +If the holder object and the pointed object die at the same time, weak processing doesn't run. |
| 130 | +It is wrong to write code assuming that the weak processing always runs. |
| 131 | + |
| 132 | +* Pre-finalizers are heavy because the thread needs to scan all pre-finalizers at each sweeping phase to determine which pre-finalizers should be invoked (the thread needs to invoke pre-finalizers of dead objects). |
| 133 | +Adding pre-finalizers to frequently created objects should be avoided. |
0 commit comments