JVM Garbage Collectors Benchmarks Report
ionutbalosin.com
ionutbalosin.com
(a) laziness
(b) wanting to avoid discussions about whether the exact way something was written was fair to some GC or another
(c) wanting to avoid "can't reproduce these exact numbers on a different CPU or different JDK version" discussions
(d) the code doesn't matter since these microbenchmarks are not the actual application you are interested in
Seriously, if you are interested enough in this that you would be willing to run the benchmarks, you presumably have some JVM workload you care about. (Or you could just grab a standard benchmark suite.) If you are interested enough, you can run your workload with the different GC flags and see what they do for you, in your context. Being able to reproduce the author's microbenchmark numbers on your laptop would not tell you anything about how your workload of interest runs on the big servers (or whatever).
No doubt we could have spent time optimising this but moving to G1 brought this down to rarely exceeding 100ms, Shenandoah brought this down further to typically <10ms.
Never got around to trying any of the newer GCs before it was rearchitected.
https://d3i71xaburhd42.cloudfront.net/605efbc0fed86899dcbf87...
Christine Flood is pretty great and she has some wonderful talks on YouTube if you're interested in how Shenandoah works. The image above comes from one of her papers on GC (https://www.semanticscholar.org/paper/Shenandoah%3A-An-open-...) and shows order-of-magnitude improvements in pause times with average pause times 1/10th that of G1 and total pause times 1/27th that of G1.
Shenandoah and ZGC are also made for larger heaps and often datastore have larger heaps.
As far as latency goes the new collectors do perform better in my experience, but you should try and measure.
But yes, they also affect mean latency.
> The period of time during which the garbage collector is active [and threads are stopped] is referred to as its latency. [1]
This does of course affects the distribution of response latency. High GC latency causes more varying response times (though not necessarily higher average response time).
[1] https://docs.microsoft.com/en-us/dotnet/standard/garbage-col...
The greatest challenges for Java gc is when heap sizes are big and when data is turned over slower than pure transactional. In other words allocate and retain for a while and free, with a 32gb heap. That’s where gc tuning becomes a dark art.
The bottom line is allocation rapidly and keeping up with that is easy enough. Mix in hundreds of parallel and live app threads, large memory structures that live for minutes or hours, and add a transactional gc turnover.
I think go does so well because if you want a cache in go you get smart and use the c escape route.
If/when Valhalla lands it'll help soften a few of the rough edges here.
However, the biggest difference is that there's only one Go GC, while there are several supported ones for Hotspot. With the exception of CMS, which is deprecated, those collectors occupy different niches, meaning there are times you'd use each of them.[0]
The Go collector is optimized for relative simplicity and low latency, and currently tolerates significant overhead to hit that target.
[0] Maybe Shenendoah and ZGC are close enough that they're just direct competitors? Perhaps they're even designed to supercede G1? I'm not quite sure about that, but even so, it would leave at least 2-3 options, depending on whether you include Serial/Epsilon as serious options.
For more, see the excellent blog post here: https://blog.golang.org/ismmkeynote
However, to your point, so far as the languages tend to allocate more or less data on the heap, that might change which benchmarks look good, the same way that you'll care about different automotive benchmarks if you want to tow a trailer or race.
My coworkers were saying how they had 100s of knobs they could turn and try to try and tune the GC to either reduce the number of pauses, or do one big one every 10 seconds. . Java's options are more then I wish to list here. Go has `GOGC=` and that's it.
As one to one performance, that would be hard to measure, but my guess is Java would win in the end.
Here's a nice presentation transcript that summarizes the design decisions behind Go's GC: https://blog.golang.org/ismmkeynote And as you can see, it's basically entirely driven by low latency, but not necessarily high throughout.
Another point to consider besides GC is that Java at least has JIT which really benefits hot paths; Go does not.
Golang prefer one simple way over playing with runtime knobs. That said the gc characteristics of go, are partly reactionary to java at that time (10yrs ago) unpredictable latency and pauses, and they optimized to address that also in response to early prod users. All gc languages have gc overhead from a throughput perspective 15-20% afaics
Tuning the GC is a challenge in and of itself. When you get so many knobs to turn, it can get hard to rationalise the overall impact of any particular one. It's part of why skilled DBAs are worth their weight in gold, for example (there was a fun paper & tool from CMU a couple of years ago, OtterTune https://github.com/cmu-db/ottertune, which tries to apply machine learning to what is almost a black-box optimisation problem)
https://blog.plan99.net/modern-garbage-collection-part-2-1c8...
https://www.reddit.com/r/golang/comments/5j7phw/modern_garba...
> "Go: 67 ms max, 1062 pauses, 23.6 s total pause, 22 ms mean pause, 91 s total runtime
> Java, G1 GC, no tuning: 86 ms max, 65 pauses, 2.7 s total pause, 41 ms mean pause, 20 s total runtime"
What I got from this is that Go has faster GC pauses at the cost of throughput (i.e. more pauses resulting in higher total pause time).
The new JVM Shenandoah GC also makes this tradeoff by adding another layer of indirection ("Brooks forwarding pointer") which has a throughput performance cost but gives you really low GC pauses.
If the code doing allocation is single threaded, then parallel/serial GCs could have stop the world pauses, while the other collectors would continue allocating while the GC runs.
That's a real effect. It means that sometimes you can improve the throughput of a process with a lot of sequential work by allocating extra-CPUs and using concurrent collectors. However, that's something of a niche case--more often, you choose between throughput and lower pause times.
If a service has reduced it's latency that only increases the throughput the overall system. CPU/Memory efficiency of a single process often much less important because there's often headroom and GC can help leverage underutilized that cpu/memory.
The test is just:
void test(Blackhole blackhole) {
blackhole.consume(new byte[_8_MB]);
}
So it's not like each "operation" is actually a large number of elementary operations, either.I guess CMS manages to perform fine here because it can flexibly resize the young generation and isn't region-based while G1 has special optimizations for what they call humongous allocations.
It's a test that checks how collectors perform under two combined deviations from the assumptions for which they're optimized (i.e. that object graphs have a high branching level that allows parallelization, short-lived objects are small on average)
You know this article is on his website, don't you? His name is the domain name.
https://ionutbalosin.com/about-me/
> is/was he also a coder
Seems unlikely anyone would be writing an article about Java GC, including having coded up benchmarks, without being able to code.
He does - it’s on his website.
It's a blog, not a peer-reviewed research paper. The guy is widely known in the JVM community. I'll vouch for his work if you want.
Ionuț Baloșin
SOFTWARE ARCHITECT AND INDEPENDENT TECHNICAL TRAINER