ZGC is a new generation of garbage collector introduced from jdk11, the expected stopping time is less than 10ms, and the stopping time is independent of heap size, and it can support tb-level heap.

As a fan of go, isn’t go’s GC already pretty good? The Initial Mark has a little STW, and the usual gc pause is less than ms? In fact, the effect of go GC is still far from the promise of ZGC, not when it comes to large heaps. It has no compaction, and running compaction has many benefits.

  1. it avoids heap fragmentation, and memory allocation requires only one pointer jump.
  2. after compaction, related objects can usually be adjacent to each other in memory, contributing to locality
  3. the ability to reclaim large amounts of memory at a really high speed. the execution time of compaction is only related to the active objects, not to the total number of objects, and the smaller the percentage of active objects compared to all objects, the more efficient the reclaim, in contrast to the sweep overhead of go, which is directly related to the number of objects.

Compaction means relocation of object pointers, and in CMS and G1GC, both compaction and relocation are done in the younger generation of STW.

This requires a mechanism that can do object relocation concurrently.

Load Barrier

In ZGC, this is the Load Barrier mechanism, which is very different from the CMS / G1GC Writer Barrier. The Write Barrier, including the INC Barrier and SATB Barrier, takes effect at the time when the object modifies its external reference.

The Load Barrier is not the direct opposite of the Write Barrier, but takes effect when “the heap pointer is dereferenced:”

1
2
3
Object o = obj.FieldA
<Load barrier>
Object p = o         // no barrier, it's not dereferncing any heap reference

There are more things to do compared to the Write Barrier, and there is different logic at different stages. In addition to the Mark marker tracking, it is possible to initiate moving objects (Relocate) and even redirect references (Remap), changing the pointer in place to point to a new object address.

There are two issues to think about.

  1. in the tracking of Mark markers, Write Barrier will track each write operation, marker operation queuing, but in the Load Barrier scenario, each read operation is queued is a considerable amount of overhead, and this repeated queuing operations do not make sense, a reference that has been accessed many times, it is said that only need to be queued once.
  2. how to know whether an object needs to Relocate, similarly, an object only needs to Relocate once in a round of GC, the relocated object does not need to make repeated attempts to relocate.

Colored Pointer & Multi-Mapping

For these two types of meta information, ZGC uses a set of Colored Pointer technique to save directly into the pointer:

  1. Marked pointer, marked with Marked marker, next time you see this pointer, don’t do Marked queue again.
  2. For the redirected pointer, mark it as Remapped, which means it has been successfully transferred, so don’t try to Relocate it.

ZGC is designed with a restriction that it only supports 64-bit architecture. As we all know, the pointer in 64-bit architecture often only uses 48 bits for addressing, and the 16 bits not used here can be used to store some meta information.

image

Here are 4 bits of meta information.

  • Finalizable: for destructor processing.
  • Remapped: indicates that the reference has been redirected.
  • Marked0 and Marked1: indicate that the pointer has been marked.

Ignore the Finalizable bit for now.

The Remapped, Marked0, Marked1 bits, only one of them is always 1 and the others are 0.

Some architectures such as ARM support Pointer Masking mechanism, which can tell the CPU a Pointer Mask, and then the CPU will ignore the bits specified in the mask when dereferencing. x86 architecture unfortunately does not have this mechanism, for which ZGC uses the Multi-Mapping mechanism.

1
2
3
4
5
6
7
8
9
  +--------------------------------+ 0x0000140000000000 (20TB)
  |         Remapped View          |
  +--------------------------------+ 0x0000100000000000 (16TB)
  |     (Reserved, but unused)     |
  +--------------------------------+ 0x00000c0000000000 (12TB)
  |         Marked1 View           |
  +--------------------------------+ 0x0000080000000000 (8TB)
  |         Marked0 View           |
  +--------------------------------+ 0x0000040000000000 (4TB)

Remmaped View, Marked1 View, Marked0 View all point to the same block of memory! It’s the same effect as Pointer Masking.

Mark and Relocate

The Load Barrier does different things at different stages. In the Mark stage, the Load Barrier adds the accessed object to the mark queue, and then drops the mark information into the page’s Bitmap. As mentioned earlier, it is not necessary to add the same reference to the marker queue twice, so the marker Marked0 or Marked1 is added to the pointer, and if the reference with the Marked marker is accessed next time, it is not repeatedly added to the marker queue.

After the Mark phase is over, the marked objects, i.e. the living objects, can be used for moving. The Relocation Set + Forwarding Table design makes the execution time of the Relocation phase more controllable on the one hand, and saves the memory overhead of pointer redirection information on the other. On the contrary, SGC 1.0 will maintain a Forwarding Pointer in each object header, which is not as economical as ZGC Forwarding Table.

During the Relocate phase, the GC thread will traverse the objects in the Relocation Set to do the move. If the Load Barrier encounters a pointer in Marked state, it will check whether the reference exists in the Forwarding Table, if yes, it will modify the content of the pointer to the new address and mark it as Remapped; if no, it will initiate the move and modify the Forwarding Table. The GC threads will do Relocate concurrently and will take a CAS to do arbitration.

image

The Relocate phase completes the movement of objects in the Relocation Set, but the redirection of pointers (Remap) is only initiated based on the Load Barrier. A surviving object may not be actually accessed during the Relocate phase, so the reference will remain in the Marked state and will have to be checked in the Forwarding Table the next time it is accessed.

Here we may return to the question: Why are there two kinds of marked bits, Marked0 and Marked1?

ZGC will “incidentally” redirect all the pointers of the previous Marked state (Remap) when traversing all objects and references in the next Mark phase. After the new Mark phase, all the pointers of the previous Marked state can be converged to Remapped state, and all the Forwarding Tables can be released. In short, in the next Mark phase, the information from the previous Mark phase is used, so two types of markers are distinguished.