I’ve never worked with a language that had a precise GC that wasn’t also a nightmare to run in production. Java’s the obvious example of a language with an unmanageable GC. (Yes, they’re claiming the next GC will work, but that was the top line feature in the 1.4 marketing back in the late 1990’s, and I simply don’t believe such claims after 25+ years and over a dozen major releases that failed to deliver it.
Go is supposedly a counterexample. I haven’t used it enough to offer an opinion, but I have heard of companies rewriting Go services simply to avoid GC at runtime.
This is such a weird take. Java's been everywhere in prod in the industry for decades from backends running millions of qps to HFT to game clients.
IME, the most common GC problems I see are:
- incredibly basic settings are wrong, like the max heap size
- the app was written with some very poor behavior, e.g. unreal amounts of allocation or tons of allocation thrashing in tenured heap.
There aren't even a lot of GCs knobs to "manage" anymore compared to a decade ago [0]. Don't treat your GC as a black-box machine with infinitely available resources, and you'll be fine.
Counterpoint: the problem with Java isn't the GC, but the language. If every struct you instantiated in C required a malloc/free, that would be really slow too.
Java's memory management doesn't work like malloc/free at all, and that's why it's fast [1]. Even disregarding scalar replacement that means many `new` don't allocate anything on the heap, nearly all allocations are a pointer bump and objects that die don't require some active deallocation. With tracing collectors it's keeping objects alive for a long time that requires some computational work, not deallocating them (as is the case with refcounting collectors).
An extremely simplified description of how OpenJDK's GCs work is like this: every allocation bumps the "heap end" pointer, and when memory runs out, the GC scans the objects reachable from some set of "roots", resets the allocation pointer to the start of the heap, and copies the live objects to the allocation pointer, bumping it along the way (and in the process, overwriting the dead objects). In practice, the scanning and the compaction may be done concurrently with the application, there is not just one allocation pointer (to avoid contention), and not the entire object graph needs to be scanned to free memory.
The cost of Java's good GC is virtually entirely in memory footprint (there may be some speed costs to GC in some cases, but they're offset by GC's speed benefits).
[1]: Java may indeed be slower than C on some workloads due to memory layout, but that's because of data locality, not GC. The upcoming inlined value objects will take care of that.
Go is supposedly a counterexample. I haven’t used it enough to offer an opinion, but I have heard of companies rewriting Go services simply to avoid GC at runtime.