HI version is available. Content is displayed in original English for accuracy.
Advertisement
Advertisement
⚡ Community Insights
Discussion Sentiment
21% Positive
Analyzed from 857 words in the discussion.
Trending Topics
#barrier#objects#don#https#why#java#instructions#parallel#jdk#read

Discussion (17 Comments)Read Original on HackerNews
Slightly less annoying summary from ChatGPT free: https://chatgpt.com/share/6a9ac7a3-15a0-83eb-8c2a-6f72cd9beb....
Caveat emptor: it makes high level sense, but I haven’t thought about it in detail.
Could it be human? Sure. But it doesn’t seem likely to me.
> So the difference has to be in what the JIT generated, and the profiler gives us exactly that.
> That is the whole vocabulary. Let’s read some code.
> Decoding the x86 version instruction by instruction is out of scope here.
> What is not architecture specific is the logic.
Here’s a segment flagged by Pangram: https://www.pangram.com/history/87e25169-30a4-4030-a65a-dba8...
I'm already at the point of "the reward/effort of HN is getting pretty low", but the question is, where to leave for?
The benchmark creates an array in the old generation (by being big enough) and stores an object (allocated in the new generation). This triggers the GC barrier for every writes. Something rare in real application.
The G1 barrier before Java 26 is slow because:
- the GC barrier and some GC threads do concurrent operations on the same memory zone (the card table)
- the barrier is big (a lot of assembler instructions) so it also troubles the loop unrolling optimization performed by JITs
Parallel GC has a simple barrier and do not care about latency (no GC check inside the loop).
The barrier implementation of G1GC was changed in Java 26, so update your Java runtime version and move on.
1. Upgrade your JDK for the best performance (as the article says, the slowdown is gone in JDK 26).
2. Don't try to help the GC by pooling objects. Mutating old objects can be expensive, while allocating new ones is cheap (at least for objects that don't do some exceptionally expensive initialisation).
There are also middle ground options, like pooling objects but giving the pool a lifecycle that is tied to a request.
The parallel collector is a perfectly fine collector, particularly for smaller heaps. Even the serial collector isn't bad for things like a containerized environment, yet G1 replaces it by default now [1].
It's not a bad algorithm, but especially when you start talking about sub 2G environments I've not seen a situation where the parallel and serial collectors won't handily beat G1 on pretty much every metric. Major collectors with modern CPUs just doesn't take much time for a lot of memory.
[1] https://openjdk.org/jeps/523
tl;dr: In JDk 25, filling a large array of references with objects living in a different heap region is extremely slow when using G1 GC as opposed to parallel GC. Solution: Move to Java 26. Or increase G1HeapRegionSize.
Details:
They used Amazon Corretto as JDK. When Arrays.fill() was called with UseG1GC and UseParallelGC, the former was really, really slow.
Then they go on to give an ARM64 primer (because they ran it on an Apple M4 Max).
The part where they mention g1BarrierSetAssembler_x86.cpp and g1BarrierSetAssembler_aarch64.cpp is where at least some readers get lost. They are essentially saying that the logic (for both x86 and ARM64) is the same. The reason why only those two files matter for this test, I believe, comes down to JEP 304[1]. Just accept it and continue reading. (And validate it later if you want to.)
They list around twelve lines of ARM instructions that matter for this test - cheat sheet, essentially.
The three lines of actually generated ARM instructions are shown then for parallel GC (the much faster one in this case).
They explain what a write barrier is (basically GC bookkeeping). So as a result, there are three instructions, but they are neat and tight. Also, we see that eight elements are filled in each iteration of the loop which fills the arrays.
Then, they show what was generated for G1 GC. And it is indeed too long. Around 20 instructions. However, some of them shouldn't have been executed because of three exit conditions. They explain why none of them fired.
They prove that by subtracting eight from the humongous threshold size, it is allocated to an Eden/young region and the problem vanishes because one of the three exit conditions is triggered thus.
Then, they show that it is a problem that increases almost linearly (my guess, based on the numbers) with the array size- so the larger the array size is, the more the time delay!!
Finally, the solution: JDK 26 took care of this issue. They also point to JEP 522 (I didn't go through it yet). If you have an older version, use the G1HeapRegionSize flag if you can do so.
That is it, essentially. Definitely long, but well-written, based on actual testing.
[1] https://openjdk.org/jeps/304
[2] https://openjdk.org/jeps/522