It is not a big secret for people who have watched the development of Shenandoah
that a major criticism is that it employs GC barriers
for every single write and read : be it reference or primitive.
Shenandoah 2.0
claims that this is not a problem anymore and it is solved via so-called load reference barriers. How is this exactly happening?
I will assume that the reader knows what a barriers is and why it is needed. For a very short intro here is another answer of mine on the topic.
In order to properly understand that, we need to first look at where the initial problem really was. Let's take a rather simple example:
static class User {
private int zip;
private int age;
}
static class Holder {
private User user;
// other fields we don't care about
}
And now let's image a theoretical method like this:
public void access(Holder holder){
User user = holder.user;
for(;;){ // some loop here
int zip = user.zip;
System.out.println(zip);
user.age = // some value taken from the loop for example
}
}
The idea is not to show a correct example, but an example that does:
a read (user.zip;
)
a write (user.age = ...
)
Now because Shenandoah 1.0
needed to introduce barriers everywhere, this code would look:
public void access(Holder holder){
User user = RB(holder).user;
for(;;){ // some loop here
int zip = RB(user).zip;
System.out.println(zip);
WB(user).age = // some value taken from the loop for example
}
}
Notice the RB(holder).user
(RB
stands for read barrier
) and WB(user).age
(WB
stands for write barrier
). Now imagine that the loop is hot
- you will pay the price for so many barriers. Even if there is no GC activity during the execution of the loop, the barriers are still in place and there has to be code that conditionally checks if the barrier needs to be executed or not.
Long story short: those barriers are not free, by any means.
These barriers are needed to maintain heap consistency, because there are two copies of an Object in memory during evacuation phase, you need to always read and write consistently. Consistently here means that in Shenandoah 1.0
a read could have happened from the "to-space" or "from-space" (called "weak to-space invariant"), while a write could happen from to-space
only.
Shenandoah 2.0
says that it will ensure a so-called "to-space invariant" (as opposed to the previous weak one). Basically - it says that all the writes and reads are going to happen from/into the "to-space". During evacuation there are two copies of the Object: one in the old region (called "from-space") and one in the new region (called "to-space").
It achieves this "to-space" invariant with a rather simple, yet brilliant idea. Instead of employing barriers where writes
happen, it ensures that the Object that was initially loaded was for sure loaded from the "to-space". This is done via load-reference-barriers. This is far more trivial to understand via refactoring the previous example:
public void access(Holder holder){
User user = LRB(holder).user;
for(;;){ // some loop here
int zip = user.zip;
System.out.println(zip);
user.age = // some value taken from the loop for example
}
}
We have introduced the LRB
barrier and removed two other. So, load-reference-barriers happen when an object is loaded, they call this : at the definition site, instead of when reading or storing to it, they call this at their use-site. You can think about it as if these barriers are inserted where aload
and getField
(for references) is used.