Hacker News new | past | comments | ask | show | jobs | submit login

The style of programming for the dominant JVM languages (Java, Scala, Kotlin) involves an over reliance on churning through a lot of short-term garbage. I think this partially due to how annoying the platform is to actually use. Incredibly complex and chalk full of programmer pitfalls (like type-erasure). In fact, the majority of devs have no clue at how the JVM works, treating it as just “magic.”

I would imagine that Go’s GC is worse than the tunable JVM ones, but go isn’t powerful enough that one would ever be tempted to program in an abstraction heavy style. While I would argue it takes a lot more work to program in Go ca, day Scala, I think the constraints imposed lead to better software. I for one have never seen a JVM desktop or business app that worked super well. The JVM manages to be a highly optimized efficient platform in which almost exclusively slow, laggy, and memory hungry apps are produced. With that being said, it’s a highly productive platform (if you’re using something besides Java), especially for backend business apps.




GC is complicated because what works well for one thing might not work well for other things. GC can be both about clever things and about hard trade-offs. That said, I'll try to talk about GC without having a religious war erupt. Keep in mind, something here might be wrong.

Go's GC is tuned for low latency. People hate pause times. They hate it even more than they hate slowness. Go makes a trade-off for short pause times, but in doing so they do sacrifice throughput. That's great for a web server. With a web server, you care a lot about pauses offering a bad experience. A 500ms pause is going to give a customer a bad experience. That's less good for batch processing. Let's say you're running a backfill job that you expect to take 10-15 hours. You don't really care if it pauses for 1 minute to GC every 30 minutes. No one will even know. However, you will know whether it took 10 hours or 15 hours.

Go's GC is meant to be simple for users. I think the only option is "how much memory should I be targeting to use?" That makes it dead simple for users. I think the Go authors are right that too many tuning knobs can be a bad thing. People can spend months doing nonsense work. Worst is that knobs are hard to verify that anything is really different. If you're running a web server, was the traffic really the same? What about other things happening on the machine? Wouldn't you rather be writing software instead?

Go's GC is a non-copying GC. One thing this means is that Go's (heap) memory allocation ends up being very different because Go's memory is going to become fragmented. So Go needs to keep a map of free memory and allocations are a bit more expensive. Java (with GCs like G1) can just bump allocate which is insanely cheap. This is because Java is allocating everything contiguously so it just needs to move a pointer. How does that work once something becomes freed? Java's G1 (the default in the latest LTS Java) will copy everything that's still alive to a new portion of memory and the old portion is then just empty. You kind of see this in Go's culture. Web frameworks obsess about not making heap allocations. Libraries often have you pass in pointers to be filled in rather than returning something.

Go misses out on the generational hypothesis. The generational hypothesis is one of the more durable observations we have about programming - that most things that are allocated die really quickly. C# and Java both use generational collectors by default and they've done way better than what came before. C# and Java don't have as-low pause times as Go, but part of that is that they're targeting other things like throughput or heap overhead more.

Go doesn't need GC as much. Go can allocate more on the stack than Java can and, well, Go programmers are sometimes a bit obsessed with stack allocations even when it makes for more complicated code. Having structs means creating something where you can just have contiguous memory rather than allocating separate things for the fields in your object. Go's authors have observed that a lot of their objects that die-young are stack allocated and so while the generational hypothesis holds, it's a bit different. Go has put a good amount of effort into escape analysis to get more stuff stack allocated.

Java has two new algorithms ZGC and Shenandoah which are available in the latest Java. They're pretty impressive and usually get down to sub-millisecond pause times and even 99th percentile pauses of 1-2ms.

Go's new GC was constrained by the fact that "Go also desperately needed short term success in 2015" (Rick Hudson from Google's Go team) and the fact that they wanted their foreign function interface to be simple - if you don't move objects in memory, you don't have to worry about dealing with the indirection you'd need between C/C++ expecting them to be in one place and Go moving them around. Google's going to have a lot of code they want to use without replacing it with Go code and so C/C++ interop is going to be huge in terms of their internal goals (and in terms of what the team targeted regardless of whether it's useful to you). And I think once they had shown off sub-millisecond pause times, they were really hesitant to do something that might introduce things like 5ms pause times. I think they might have also said that Google had an obsession with the long-tail at the time. Especially at Google, there's going to be a very long tail and if that's what people are all talking about and caring about, you end up wanting to target that.

Go has tried other algorithms. They had a request-oriented-collector and that worked well, but it slowed down certain applications that Go programmers care about - namely, the compiler. They tried a non-copying generational GC, but didn't have a lot of success there.

Ultimately, Go wanted fast success and to solve the #1 complaint people had: extreme pause times (pause times that would make JVM developers feel sorry for Go programmers). Going with a copying GC might have offered better performance, but would have meant a lot more work. And Go gets away with some things because more stuff gets stack allocated and Go programmers try to avoid heap allocations which would be more expensive given Go's choices (programming for the GC algorithm).

I don't think that JVM languages have a programming style that lends themselves to an over-reliance on churning through short-term garbage. Well, Clojure probably since I think it does go for the functional/immutable allocate-a-lot style. Maybe Kotlin and Scala if you're creating lots of immutable stuff just to re-allocate/copy when you want to change one field. That doesn't really apply to most Java programs. And I have covered the way that Go potentially leads to more stack allocations. However, I don't think most people know how their Go programs work any more than their JVM programs and this really just seems to be a "I want to dislike Java" kind of thing rather than something about memory.

Java programs tend to start slow because of the JVM and JIT compilation. Java has been focused on throughput more than Go has (at the expense of responsiveness). That is changing with the two latest Java GC algorithms (and even G1 which is really good). Java is also working on modularizing itself so that you won't bring along as much of what you don't need (Jigsaw) and AOT compilation. Saying that Java is slow just isn't really true, but it might feel true - things like startup times and pause times can inform our opinions a lot. There's absolutely no question that Java is a lot faster than Python, but Python can feel faster for simple programs that aren't doing a lot (or are just small amounts of Python doing most of the heavy work in C).

I mean, are you including Android in "all Java apps"?

Java, C#, and Go are all really wonderful languages/platforms (including things hosted on them like Kotlin). They're all around the same performance, but they do have some differences. I think Go should re-visit their GC decisions in the future, especially as ZGC and Shenandoah take shape, but their GC works pretty well. But there are certainly trade-offs being made (and it isn't around language features that make the platform productive for programmers). I think GC is very interesting, but ultimately Java, C#, and Go all have very good GC that offers a good experience.


Nice writeup!

If a programmer really cares for performant batch-processing (not sure it's a direction we're going for anymore but), you'll just reuse your objects and dataspace. Understanding Go in order to minimize GC is pretty trivial and the standard library includes packages and tooling exactly for such purposes. Most likely, there are only a few hotspots needing to be tended to like this.

So this sounds like Golang is optimized for what people love about computing: fast response time. Also providing programmers with basic performance "out of the box", which is another good tradeoff for me.

The tradeoff works best when you make simple designs, not huge behemoths. Spending more time reiterating clever designs, rather than jamming the keyboard until done.


Java is slower than Python for simple short-running programs. When Python is finished Java is still struggling through start-up and class-loading.


Is this actually true? Have you measured this, or seen measurements?


Here are measurements for an extreme case of a short-running simple program:

  $ cat Hello.java 
  class Hello { public static void main(String[] args) { System.out.println("Hello from Java!"); } }

  $ cat hello.py 
  print("Hello from Python!")

  $ time /usr/lib/jvm/java-13-openjdk/bin/java -Xshare:on -XX:+TieredCompilation -XX:TieredStopAtLevel=1 Hello
  Hello from Java!

  real 0m0.102s
  user 0m0.095s
  sys 0m0.025s

  $ time python3 -S hello.py 
  Hello from Python!

  real 0m0.034s
  user 0m0.020s
  sys 0m0.013s
It's a bit faster if you create a custom modular JRE with jlink:

  $ /usr/lib/jvm/java-13-openjdk/bin/jlink --add-modules java.base --output /tmp/jlinked-java13-jre
  $ /tmp/jlinked-java13-jre/bin/java -Xshare:dump
  $ time /tmp/jlinked-java13-jre/bin/java -Xshare:on -XX:+TieredCompilation -XX:TieredStopAtLevel=1 Hello
  Hello from Java!

  real 0m0.087s
  user 0m0.050s
  sys 0m0.035s


Yes, I often write small command-line tools and that's what I found. Profiling seemed to indicate that doing anything at all in Java, such as reading a config file, is fast the second time but super slow the first time.

You can test my assertion simply by writing a "hello world" in Python and Java.


Thanks for the in-depth write-up @mdasen. I certainly learnt a lot about general GC properties and trade-offs.

takes hat off in appreciation


Great reply. Thanks for taking the time to write it.


Nicely written


I would imagine that Go’s GC is worse than the tunable JVM ones, but go isn’t powerful enough that one would ever be tempted to program in an abstraction heavy style.

That is a compliment.

One of the design criteria for Go was to push programmers away from being architecture astronauts whose abstraction heavy style results in having no idea how much work you are hiding in all of the layers. And that design criteria was based on analysis of the actual failure modes of significant software projects.

Therefore the fact that real world Go apps generally manage to avoid that failure more is a testament that they succeeded in this design criteria.


Generics are architecture astronomy?

This is parroting anti-intellectual nonsense.


Generics as done in C++ templates lend themselves to that.

Inheritance as done in most OO systems does likewise.

You will note that the core Go team has a desire to add generics. However they also feel that it is a potential Pandora's box.


Interestingly though, some of the worst excesses of overdesigned architectures in Java happened before Java even had generics, and heavy use of interfaces was a standout feature.

So Go has absolutely everything a true architecture astronaut needs. In my view, the big difference is cultural and has nothing to do with language features.

Also, the lack of generics often forces you to use reflection to avoid code duplication, which makes for very complex, error prone code.

As an example consider the sort package in Go's standard library. Here's what they had to write in order to generically swap two elements in a []interface{}:

https://golang.org/src/internal/reflectlite/swapper.go


I suspect a problem with Java is it's culture and ecosystem evolved in the early 90's when memory, disk, and processor speeds were doubling every few months.

Go in the late 2000s after Amdahl's law had been bitch slapping everyone for 10 years. Network bandwidth increased but laterncy didn't. Processor transistor counts increased but clock speeds didn't. Memory increased but memory bandwidth didn't keep up.


C++ templates are not the gold standard of generics. In fact, they are a glorified text substitution system, totally broken and flawed.

Generics are still absolutely necessary for ANY serious programming language. They are required by the DRY principle. Algorithms must not need be repeatedly implemented for every new data structure. To say that generics are astronaut is simply unprofessional.


>One of the design criteria for Go was to push programmers away from being architecture astronauts whose abstraction heavy style results in having no idea how much work you are hiding in all of the layers.

So they chose to create a language where there's no way to get around creating vast amounts of boilerplate. This boilerplate heavy code style results in having no idea of the true logic you are hiding in all of the boilerplate.


"over reliance on churning through a lot of short-term garbage"

this is handled super efficiently by generational garbage collectors.


Right, you only pay for the memory reachable when the slow / blocking part do the garbage collection runs, so a lean program that churns through small objects can be very responsive.


To my understanding, all of jetbrains products run on the jvm. They work pretty well although I wouldn't characterize them as lean.


Work well? I program professionally in Scala and IntelliJ is fucking terrible. Me and everyone on my team yells “fucking IntelliJ” and restarts the damn thing at least once a day.


(ftf it's chock-full not chalk full.)


> I for one have never seen a JVM desktop

> or business app

Practically every large site you use (Google, Amazon, FB, GitHub, Apple, Twitter, and many more) are using Java/JVM in the backend in some non-trivial capacity.


>I for one have never seen a JVM desktop or business app that worked super well.

And I think none of them work super well.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: