This is a different GC design than V8 and Go, which use older collector designs with high overhead. They need to collect very frequently because their stop the world pauses get longer with more garbage. Javas new collectors are near constant time, even with terabytes of garbage, so it's much more efficient to wait until the heap builds up and collect much less frequently. Ironically, Java appears to use tons of ram because it has better garbage collectors.
When you configure Java GC to collect frequently, it turns out Java uses 2-3X less ram than JS, and far less than Python, Ruby, etc. It does use more than Go, about 2X. But the point is it uses a lot less ram than most other popular languages for web dev.
Unfortunately the "Java uses too much ram" is used in defence of using things like JS and interpreted languages, when in actually it uses much less if you configure it to.
There are times when I'd happily trade more frequent GC pauses for a smaller per-process memory footprint. How do you find a reasonably small Xmx that doesn't lead to OutOfMemoryError exceptions?
That's tough to figure out. In new versions of Java, I think 14+, if you use ZGC collector it will return unused memory to the OS. Memory options vary depending on collector, but new versions of ZGC support "soft max" heap size and uncommit. Together it might be close to what you're looking for https://malloc.se/blog/zgc-softmaxheapsize
I should mention the GC situation was worse until the last few years. Until ZGC and Shenandoah came around, Java still didn't collect frequently but when it did there were long pauses. This is what V8 and Go's collectors were designed to avoid. They have more overhead from collecting frequently, but low pauses. With the new Java collectors you get the best of both worlds.
You can set heap ratios and such for older collectors to decrease Java memory use with those, but IMO you're better off using ZGC and uncommit these days
Newer versions of ZGC support class unloading and have some other performance optimizations
Indeed. That fact obviates this option in most cases. You have to spend time tweaking obscure, unstable knobs (the X in Xmx means Oracle is free to alter its meaning at any time) and risk either a.) serious failures in production or b.) poor results because the conservative choices necessary to avoid 'a' achieved little improvement and you wasted your time. The real world for most enterprises is a vast heard of communicating components and toying with GC switches multiplied by N things is a nonstarter.
So while you're technically correct that excessive memory use by conventional JVMs is "not always true," in practice you are wrong. That reality comes with a real cost that appears on a real bill every single month.
Go's approach of minimal knobs leads to unfixable problems in production. Java gives more options to tune GC for your use case
I didn't bring up Go, but since you did the thing I see is that Go -- a much younger language -- is going places Java never has, or did so only haltingly. Caddy is a case in point. Here is Go taking on nginx, haproxy, Envoy, etc.
The people that once imagined using Java for such things have retired or moved on to other battles. No one seriously ponders attempting 'systems' tasks with Java any longer; that whole space was ceded to more efficient languages. My opinion is that Java's poor efficiency -- a big part of which is its excessive memory consumption -- is the reason for this.
That's my opinion. What I know for fact is that today, when people are making design decisions about new services and their deployment, Java is a problem; it is understood that anything implemented in Java is going to sort right to the top of the list of memory pigs in the cluster, and you can only afford so many of those.
This feels like G1GC erasure. I'll also say we've tried out ZGC and while pause times were low, it had a huge CPU overhead and the performance of our application was notably worse and we went back to G1GC. We're still on Java 11, so maybe we'll see some magic when we eventually try the newer versions.
Give it a try.
https://wiki.openjdk.java.net/display/shenandoah/Main
https://www.slideshare.net/jelastic/choosing-right-garbage-c...
In my limited tests I never saw a GC pause over 5ms. I was basically hammering a Spring Boot application with HTTP load tester.
While not-pausing is generally a huge improvement (after several decades of GC development), now what about thrashing of CPU caches?
The various factors I see are:
- JVM overhead 10-15mb seems to be required just to get off the ground,
which directly relates to the JVM replicating a bunch of OS
functionality (inevitable tradeoff for portability)
- Missing value types - any complex data structure ends up with
big overhead from storing references
- Stack frames - if you need a lot of threads you need a lot of stack,
and a surprising number of threads run
Some of this will get addressed in upcoming releases, will be interesting to see how it goes.I think this information is a bit dated. Go has a highly advanced concurrent collector with very low pause times (~1-100μs). V8 also has incremental marking, concurrent marking, and parallel compaction. Its pause times are more like 100-1000μs. V8's GC has been tuned more and more to save memory (i.e. smaller heaps) because people have so many tabs open these days.
Uh, most of the rant about Go GC below is out of date, they made some huge improvements from 2016-now. I'm leaving it up because someone replied to it
Unless something has changed in the last year or two, Go's GC is similar to Java's old CMS collector which is being deprecated.
Go's GC is non generational and non compacting, both hallmarks of modern GC algorithms. It's not a modern "moving" collector. It also has several stop the world phases. It's basically a design from the 70's. The GC uses old simple algorithms because the team was under a time crunch when it was developed. This may have changed, but that's how it was in 2018-2019 timeframe.
The pause times are short because GC runs very frequently. It has to because performance with large amounts of garbage is quite bad. This results in significant GC CPU overhead.
I don't know much about V8 collector besides that it is generational and compacting, so more modern than Go. But it's still a "stop the world" design. In that regard it's still closest to Javas old CMS collector.
Javas new collectors, ZGC and Shenandoah, both have near constant time stop the world phases. You can collect terabytes of garbage with only a few milliseconds pause. In V8 or Go, this would be many seconds pause time as the mark phase is "stop the world" in both.
You can find benchmarks that show one way or the other, but in badly behaving or allocation heavy applications, Java's new collectors or older G1 will perform far better. V8 and Go are dishonest about their GC performance by showing average pause times with high collection frequency. The important GC cycles are the long ones, so you really want to measure worse case pause time under load.
Under heavy load Go's design falls over. It's not compacting, not generational, and the mark phase pauses the application. IMO, it's just not a good GC. V8 is better, it is generational and compacting, but mark phase is still STW. Java's ZGC isn't generational but importantly, the mark and sweep phases don't stop execution. No matter how big your heap is and how much garbage, your GC pauses will be short
> ... In V8 or Go, this would be many seconds pause time as the mark phase is "stop the world" in both.
Like I said before, your information is outdated. V8 has both incremental and concurrent marking. I even mentioned it in my comment, but apparently you didn't read that either. V8 only stops the world for semispace evacuation and compaction. It doesn't compact the entire old generation at once, but decides on a per-page basis.
For Go's GC, I am going by public information presented by one of its primary designers, Rick Hudson, who has since retired.
You can argue with his slides if you want. https://blog.golang.org/ismmkeynote
Java's new GCs sound fantastic! It's great for the field in general. However, I would encourage you to spend less time misrepresenting other people's work and making up numbers.
For Go, I'm going to be a bad HN user and not read the whole article. Sorry, it's just too long for this time of night. It does appear that my understanding of Go GC is out of date. There's been many improvements in the last couple years. Some strange behavior due to not enough knobs to configure GC, but it appears to have a near constant GC pause? https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i...
I'm annoyed that Google doesn't offer much benchmarking results for V8. Huge articles about improvements made with a single benchmark image. And they didn't use standard benchmarks for either so it's unclear what they're even benchmarking. The Go slides you linked include benchmarks from some guys production server he tweeted images of, a bunch of standard benchmarks but they only show % throughout improvement, and no pause times.
Well unfortunately you are still misunderstanding, so let me be more precise so we are talking about the same thing. V8 uses incremental marking (i.e. splitting mark work into smaller chunks and interleaving those chunks with mutator time) as well as concurrent marking (i.e. multiple parallel collector threads marking in the background, concurrent with the mutator). Not mentioned in the article, but sweeping of pages is also incremental (i.e. dead space reclaimed on-demand when free lists run empty) and concurrent when idle (i.e. in the background). So the statement "V8 still uses STW for mark sweep" is just wrong. Like I said before, V8 only stops the world for semispace scavenges (fast, < 1ms) and compaction (slow, ??ms), but compaction is less frequent than mark/sweep, which is incremental and concurrent.
You also misunderstood what is reported here. That 50ms main thread marking time is cumulative, meaning those 50ms are spread over the entire garbage collection cycle, split up into small increments so that the mutator (main thread) is not stopped the entire time. It's explained there in the text and illustrated in the second-to-last diagram.
> quite bad compared to ~5ms or less
Again, it is not 50ms pause, it's 50ms work, split into much, much smaller incremental pauses, typically less than 1 ms each. That number is not presented in your linked article but is pretty typical. The V8 GC needs sub-millisecond pause times because it has a soft realtime requirement in that it may end up on the critical path for frame rendering (60fps = 16.6ms).
> For Go, I'm going to be a bad HN user and not read the whole article.
FTA "...The August 2017 release saw little improvement. We know what is causing the remaining pauses. The SLO whisper number here is around 100-200 microseconds and we will push towards that. If you see anything over a couple hundred microseconds then we really want to talk to you and figure out whether it fits into the stuff we know about or whether it is something new we haven't looked into. In any case there seems to be little call for lower latency. It is important to note these latency levels can happen for a wide variety of non-GC reasons..."
TLDR: if you see pause times of more than a couple hundred microseconds, call the red phone.
Also, please note, I am just trying to provide accurate information about the collectors I do know about, designed by people I work(ed) with. I don't know enough about ZGC or Shenenadoah to confidently assert anything about their performance characteristics, but based on what I read I am actually very excited to see them make it into production. I consider advances in GC to be overall a good thing for everyone, and would encourage you to be more open to learning the advantages and disadvantages of various systems without as much derision and not try to pick sides.
* JVM overhead - all these advanced optimizing compilers and GCs have non-zero footprint. They need RAM to run their code and they need RAM to perform their tasks.
* Compiled code cache - JVM keeps both the original bytecode and generated machine code in RAM.
* OOP overhead - each object has 2 or 3 words of overhead for the object header vs zero in languages like C or Rust. Even when you don't need dynamic dispatch or object locking, you pay for it.
* Inability to compose bigger structures other than by allocating separate objects and using pointers to reference them - these pointers need space and are not cheap on 64-bit architectures. This is going to probably partially improve with Valhalla, but at this point it is mostly guessing and it has been in development for years.
* No support for packed arrays.
* The smallest unit of loadable code is a class. If you needed a single function, the JVM loads a whole class containing it, and its required dependencies. This is not only bad for memory usage but also for startup time. Unless you pay a lot of attention, it is easy to load 80% of code in order to just display a help message (this is based on a real issue I worked on - I'm not making this up). Compare that to code in languages like C - the OS loads code that gets executed.
And back to GC - I agree the default settings are often to blame, but there is a reason JVM defaults to using RAM so aggressively. GC becomes very inefficient when it doesn't have enough "room". And low pause GCs achieve their low pause goals by trading throughput. Switch from parallel STW GC to G1 and your maximum sustainable allocation rate goes down by a few times.
Java doesn't have a "struct". If you want to represent an array of 64-bit signed integers, Java has you covered with its primitive arrays. But if you want to represent an array of anything more interesting than that, (say, a tuple of a double and a long), you have to serialize and deserialize those objects to and from parallel primitive arrays or byte buffers. Because if you do the language-natural thing and use an Object array, you're paying a huge price in memory: 4 or 8 bytes per pointer in that array, plus a 16 byte Object header on each Object. And, of course, those Objects are all individual allocations, not necessarily contiguous. That's a lot of overhead!
Of course, Java programmers concerned with memory usage don't put up with this. Lots of solutions have been devised. OpenHFT's Chronicle Values[0] is one example I came across recently. But this feels like fighting with the language compared to how easy it is to be efficient with memory in C. If you told a beginner C programmer to make an array of compound objects, it's not unlikely that their array will take up exactly as much space in memory as it intuitively seems like it should. (8 byte double + 8 byte long) * 100 values = 1600 bytes in C, no fuss. If you asked the average Java programmer for that they'd give you something that would take up 3 times as much memory. And because Java makes that behavior natural, it "uses too much ram". It doesn't matter as much that it's possible to convince it not too.
Plus many people overlook just writing a couple of native methods and be done with it.
Somehow this is how people write "Python".
Also Valhalla and Panama are around the corner.
On another note, I'm not aware of other freely available GCs in other languages that are able to easily scale to multi-GB/TB scale memory usage. A while ago, I benchmarked an open source key/value golang project and it performed miserably when it reached GB level memory usage.
By aggressive you mean they actual do that now right? As far as I know before ZGC no gc did that and they're still back porting that feature to G1 right?
Edit: I'm actually quite pleased with ZGC I have the eclipse language server use it and my editors memory usage on average is so much lower.
Agreed.
GraalVM is showing enormous promise in this area, alongside efforts like Project Valhalla.
I think the emergence first of microservice and then of FaaSes has lit a fire underneath OpenJDK folks and others in the ecosystem.