G1 uses two kinds of barriers to maintain certain GC invariants, while mutators update the objects graph concurrently.
The pre-barrier is some code mutators execute before a store operation, and it is used to maintain the
Snapshot-At-The-Beginning (SATB) invariant, i.e. all objects reachable at the marking-start will be marked in this
marking cycle.
The post-barrier is some code mutators execute after a store operation, and it is used to maintain the
cross-generation invariant, i.e. all pointers from old-gen to young-gen must be identified in some way.
In this post, we will focus on the pre-barrier and see its implementation in OpenJDK. (These two kinds of barriers are
usually next to each other in the codebase, so one can easily do the same for the post-barrier after going through this
post.)
if (is_marking_active) { if (pre_val != null) { enqueue(pre_val); } }
The pre_val is the previous value that is about to be overwritten by a store operation, e.g. in the case of o.field = new_obj, pre_val would hold the value in o.field before the assignment. The barrier code consists of two checks and
a function call to record the previous value.
Next we will use a trivial java program to help us study how this barrier logic is implemented in the interpreter and
JIT compilers.
The interesting part is the f method, which contains a store operation overwriting a field (a.x) — this triggers
the pre-barrier. The following command prints all relevant assembly for the interpreter, JIT compilers, and the stub
code.
The bytecode corresponding to “store operation” is putfield, so the top-level caller is TemplateTable::putfield,
which eventually calls G1BarrierSetAssembler::g1_write_barrier_pre, and the generated assembly looks like:
Instructions annotated with (1) and (2) are the two aforementioned if checks. The code corresponding to (3), handling
buffers being full, is inside enqueue, which is (4), i.e. G1BarrierSetRuntime::write_ref_field_pre_entry.
Note that (3) and (4) are not “inlined” in the assembly of hello.f; instead, they live in a RuntimeStub, which
means another method (other than hello.f) can call this stub as well. This decreases the overall C1 assembly
footprint, with a slight cost of reduced throughput.