Well, you are in luck. Java provides more than five hundred flags to configure runtime behavior. So there is no limit on things you can achieve with that.
It hasn’t been true at all for modern GCs. G1GC basically has only one knob that you may change: the target pause time. This changes between better throughput vs lower latency (which two problems are mostly opposite ends of the same axis).
And your sarcasm aside, Java has the state-of-the-art GCs by a long shot.
2 knobs after a decade vs how many in Java? Yes, that is exactly what simple looks like.
After years of real world experience, I can say that the Go garbage collector worked fantastically across a range of applications that I've been involved in. No tool is perfect for every job, of course, and having hundreds of knobs does not make Java perfect for every job either.
I'd say it makes Java's GC worse at nearly every job as that means the defaults probably aren't what you want but are probably what you are going to use.
The Java problem is that you wind up in a blame game where one team blames the code and the other blames the GC settings and you wind up doing a lot of fruitless flailing. No knobs really cuts down on that nonsense.
In situations where you actually are allocating on the heap, Java’s default GC significantly out performs Go’s. Just look at the binary tree bench marks.
Even that simplification is too much. Java uses a generational GC, so the fewer objects that survive the nursery, the less work it has to do.
That benchmark does not mean Java's GC is faster when you're "actually allocating on the heap". It means Java's GC is faster when you're spewing garbage onto the heap as fast as possible.
Since Go makes heavy use of stack allocation where possible, short-lived garbage usually doesn't end up on the heap, but this benchmark is designed to force that outcome. Go's garbage collector is optimized to minimize latency and STW, but this does come at the cost of decreased throughput. The opposite can be said of the majority of Java GC implementations, which trade latency and STW time for increased throughput.
This depends on implementation. ZGC is not generational yet, but I don’t see how generational or not would invalidate a GC benchmark for the default implementation.
> That benchmark does not mean Java's GC is faster when you're "actually allocating on the heap".
It actually does at least in the discussion of _default gc implementations_. Just because the default (and only) implementation for Go sacrifices throughput (and relies on the compiler to stack allocate) doesn’t invalidate the benchmark.
And honestly if I was in Vegas I’d still bet that ZGC (Java’s non-generational, latency sensitive GC) would beat Go’s implementation here.
> This depends on implementation. ZGC is not generational yet, but I don’t see how generational or not would invalidate a GC benchmark for the default implementation.
I never said the benchmark was invalidated, I said that you interpreted the meaning of the benchmark wrong. I like how your reply completely ignored what I said the benchmark meant. That benchmark has a very narrow focus, and generational makes a huge difference for that one benchmark.
> And honestly if I was in Vegas I’d still bet that ZGC (Java’s non-generational, latency sensitive GC) would beat Go’s implementation here.
I would love to see a holistic set of benchmarks comparing them. Even just that one very narrow benchmark you linked would be fun to see — if ZGC is so good, surely one of the implementations of the benchmark on the website is using ZGC? But I haven’t had time to dig through them.
But, part of Go’s charm is that idiomatic code rarely puts tons of pressure on the GC anyways, and a GC can never be faster than stack allocation for a multitude of reasons… which is also why C# has value types.
But, if I understood correctly, that design both requires you to explicitly mark a type as a value type when you define it and it doesn’t support inheritance, so I’m skeptical that it will see any real adoption for a long time.
Java hasn’t had decades to develop a culture around this feature the way that C# has, and neither are like Go where everything is value typed unless it is one of the built in pointer types, or you explicitly put the value behind a pointer. (The pointers themselves are values, of course, but I think few people are interested in that distinction here.)
Go isn’t perfect by a long shot, I just don’t think adding the millionth keyword is going to be a silver bullet for Java’s predisposition towards GC pressure.
Java’s standard lib has been planned with value types in mind for some time now, plenty of classes will be able to take advantage of them. Also, inheritance is not rampant in Java, at least not in classes that are so numerous that value types would help them.
Also, Java is an exceedingly small language, so the millionth keyword comment is unwarranted.
> Also, Java is an exceedingly small language, so the millionth keyword comment is unwarranted.
Small compared to what?
Java and C++ are extremely competitive in feature bloat, and I can hardly think of anything that is comparable to them. Even C# has a smaller surface area, in my opinion, even though it manages to implement a variety of useful features that Java currently lacks (like async/await and value types), but C# is still a large language — no doubt about it. C# just manages the interactions of its features better than Java, in my opinion, which makes it feel simpler.
I have honestly never heard anyone call Java a small language. It is far from that!
> Java’s standard lib has been planned with value types in mind for some time now, plenty of classes will be able to take advantage of them.
Aren’t the majority of the standard container types using some form of inheritance? That’s what I recall, and that rules them out.
What?? C++ is perhaps the biggest language I can think of followed by Swift. C# is well on the path of C++.
But Java? It only has classes, inheritance (no multiple inheritance as c++), interfaces, objects which are instances of said classes, 7 primitive types and I am basically at the end of java’s feature list. Lambdas are often hated because they were implemented at first as classes with a single method (they no longer compile to that), so they are syntactic sugar only in a way. Classes can have static methods as well, and there are 3 visibility modifiers (which are also language feature of Go, just implicit in naming convention).
> and I am basically at the end of java’s feature list.
Not even close. There are quite a few keywords that modify behavior, including abstract, final, transient, synchronized, etc. You also have method overloading, including constructors which use a separate syntax from normal method declaration. Java also relies heavily on non-linear control flow thanks to exception handling. Go has panics, but it isn’t normal to see them in my experience, and indicates a serious bug with your code, whereas Java exceptions are extremely normal to see… but that doesn’t mean non-linear control flow is simple.
All Java classes have implicit methods like toString and equals, which you need to be aware of, and then this also lets you override the behavior of the equals method… yet you can’t override any operator, for some reason. And the actual equals operator is not at all what the average person expects when they’re getting started in Java, since it is referential equality, not value equality, except when it isn’t. I guess it is “simple” that the language lets you override the equals method, since that is consistent with other methods, but why is it a method at all? Why doesn’t the equals operator just do the expected thing, which would be equivalent to calling an invisible, non-overridable equals method? If no operator should be overridable, then no operator should be overridable, and the equals method is effectively an operator since the regular equals operator is just a footgun most of the time, except for the cases where it is performing value equality checks. Java has other footguns that stem from the language, not the standard library, like hashCode. I could go on, but really, why bother?
If you only want to talk about the basic features of the language and ignore the many keywords and edge cases you can run into in the syntax, then even C++ is simple. It’s the interaction of all these features that makes the language complex, including the implicit nullability of almost everything.
> there are 3 visibility modifiers (which are also language feature of Go, just implicit in naming convention).
Java actually has four access modifiers, not three, one of them is just implicit.[0] Go only has the notion of public and private, and private in Go is probably closer to the "default" access modifier in Java.
Compare to actually smaller languages like Go, or really small languages like Lua, Lisp, Tcl, or Smalltalk.
Java isn’t the worst language by a long shot, but it is one I’m unlikely to intentionally pick for anything.
You’ve left a bunch of comments all around this thread defending Java against even the smallest slights. I’m not interested in debating pointless aspects of Java forever this morning. I’ve used Java. I’ve used C++. I’ve used many languages in many contexts. You may disagree with my conclusions, and that’s fine. In my experience, Go takes less code and is less error prone than Java, in addition to being nicer to deploy and run. That's enough for me to choose it over Java, even if Go isn't perfect either.
I do comment about Java when I feel it is needlessly bashed - like in this case, comparing its complexity to C++’s, which couldn’t be further from the truth. Sure, brainfuck is also a smaller language than Java, it doesn’t mean the latter is at the other end of the complexity-spectrum.
I have zero problem with anyone choosing their favorite language for some job, but do not make a language look better by spreading bullshit about another (GC-knobs, java language being complex).
> That benchmark does not mean Java's GC is faster when you're "actually allocating on the heap"
The benchmark doesn’t show it, but it is nonetheless true. Java uses thread local allocation buffers, which is possible due to moving GC, and it is literally as fast as it gets (a single, non-atomic pointer bump)
Again, GCs just aren’t that simple. You seem to be taking a very literal interpretation of what was said, but that person was implying that Go is only fast when you’re avoiding the heap. They weren’t talking about the actual, literal speed of allocation. My interpretation of their comment is confirmed in their later reply to me, as far as I can tell.
Java’s GC design requires more barriers which hinder performance throughout the lifetime of a heap allocation, not just at the time the value was allocated.
Either way, Go also uses per-thread caches in its allocator to avoid contention between threads. I don’t believe it is a bump allocator, but this goes back to other tradeoffs being discussed. The number of barriers, the duration of STW, etc.
I don’t understand why so many people in here feel the need to declare the superiority of Java GC. Java’s garbage collectors are better for Java, due to the way that Java allocates practically everything on the heap. That doesn’t mean they have zero performance tradeoffs and that they’re perfect. They do have tradeoffs, and not just in complexity of tuning.
You clearly seem to be here to troll / just crap on Go while trying to appear fake-earnest, and that goes against HN guidelines. I believe this falls under "sneering".
I've actually also used Rust plenty in professional contexts over the last five years. Once again: no tool is perfect for every job.
As I recall, C#'s GC famously has very few knobs as well. I don't think you can swap the GC implementation, either, which is a source of even more complexity in the world of Java.
I had a look through some of your comments here and elsewhere and just wanted to give you some friendly feedback: This style of commenting is quite toxic. I think you'll feel happier if you let go of the tribalism and engage with others in good faith.