Compared to Python's, all of them are beyond perfect. And 99.9% of the time you don't even need to use anything but the default.
I somehow understand the situation less after reading this.
Is Python's GC bad, or are there cyclic reference issues? Is it possible to detect cyclic references perfectly? What does beyond perfect mean? If we have 7 and 0.1% of the time you need one of the 6 that is non-default, how do we choose? Is the understated version of "Compared to Python's, all of them are beyond perfect" "I think Java's are great"? If not, what about Python's impl makes it so lackluster to any of 7 of Java's?
Yes. The GCs in Java, .NET, V8, and Go do it.
> If we have 7 and 0.1% of the time you need one of the 6 that is non-default, how do we choose?
Java's GC are optimised for different workloads and environments, and when the choice matters, they're easy to choose among:
1. Parallel GC: Maximal throughput when latency doesn't matter (batch processing).
2. Serial GC: Very small machines.
3. ZGC: low latency (<<1ms maximal pause, i.e. effectively pauseless)
4. G1 (the current default): A balanced mix of throughput and latency.
These are all the standard GCs (the seven you mentioned include a GC similar to Go's that was removed years ago, an "no op" GC for benchmarking hidden behind a development flag, and alternative implementations by different companies to some of the ones above).
It's possible that either Serial or Parallel will be removed when G1 is able to fully replace them.
Now, why do users need options? Because Java runs most of the world's finance, manufacturing, shipping and logistics, telecommunication, travel, healthcare, retail, defence, and government. We're talking large, complex software that handles huge workloads, and the needs vary. What works well enough for a CLI dev tool or a simple website is often not good enough to handle the world's credit card transaction processing or mobile phone networks.
> If not, what about Python's impl makes it so lackluster to any of 7 of Java's?
Java's GCs are moving collectors, which offer advantages not just compared to Python's GC but to all memory management strategies. Memory management (even in C) imposes a CPU/RAM tradeoff. Moving collectors (used in Java, .NET, and V8) give you a knob for controlling the tradeoff, i.e. they're able to convert RAM to CPU (i.e. use RAM chips as a hardware accelerator) and vice-versa.
Unless you're being pedantic and including reference counting without cycle detection as GC, if your GC has cyclic reference issues, your GC is bad.
> Is it possible to detect cyclic references perfectly?
Yes? That's the entire point of tracing GC. You have some set of root objects that you start with (globals, objects on thread stacks, etc.) and then you mark every object that's reachable from them. Anything that's not reachable is garbage, even if there are cycles within them.
Both can be true. The first can even be wholly or partly due to the second.
On addition, the way it does it via RC causes fragmentation, poor locality for caches, and general slowness for mass allocations. And it's one-size-fits-all.
Java has a much larger selection to pick to finetune specific use cases, which each being far greater for that use case. And the default no-need-to-think one (G1 iirc), is already faster and better than Python's.
Memory allocator: tcmalloc, jemalloc, they are concerned with fetching (and releasing) pages of memory from the OS and allocating objects for the program
GC is only responsible for saying to the memory allocator "this object is no longer used"
(please stay focused on java)
This is simply not true. Not only are Java's GCs responsible for allocation (which they do simply by bumping a pointer, similar to stack allocation), unlike Python's refcounting collector or Go's nonmoving tracing collector, they have no free operation of any kind and never free objects. Moving collectors don't even know when an object is freed. The way they work is that they compact the live objects, and because the dead objects are invisible to them, they happen to write over them when compacting the live ones. Refcounting collectors and nonmoving tracing collectors do use a free-list-based allocator that they use for allocation and deallocation, but moving collectors work completely differently.
Moving collectors can be so efficient that in the eighties, there was a famous paper about them called "Garbage Collection Can Be Faster Than Stack Allocation" [1] showing that the cost of managing an object's lifetime with a moving collector can, in principle, be less than a single machine instruction. That's why the cost of heap memory management in Java (and other runtimes that employ moving collectors) cannot be compared to the cost of memory management in languages using free lists, be it C or Python. Their operation is just too different (e.g. in Java, assigning a value to an object field or setting an array cell could sometimes be more expensive than allocating a brand new object/array).
While the JDK's don't work quite like this, a simple way to picture it is as a a contiguous memory buffer, say, 100MB large. The GC compacts the live objects to the bottom of that buffer. At that point they do know where the end of the used memory is. Say that after compaction, the live objects occupy the bottom 20MB of the buffer (overwriting any dead objects that may have been there). At that point, the GC can choose to uncommit the top 30MB of the buffer and return it to the OS.
With malloc/free there's also this separation. A free operation marks the memory of a freed object in a data structure called a free list. If all the objects in a certain page have been freed, the allocator can choose to uncommit it and return it to the OS (although many modern allocators choose not to do this promptly).
The operation of moving collectors and where there cost is is completely different from free-list approaches. With free list approaches there's some work done to allocate an object and some work done to free it. With moving collectors, there's very little work to allocate an object (typically, just a pointer bump) and no work to free it; there is, however work to keep the object alive. That's why, especially with generational collectors, moving collectors do very little work to manage the memory of objects that don't live for long.
This is why, when working in Java, it's important not to think about the heap as we do in C. For short-lived objects, the cost of allocatng and "deallocating" them is often not significantly higher than the cost of allocating and deallocating an object on the stack in C. On the other hand, mutating an existing long-lived object could sometimes require some bookkeeping work by the GC, and could be much more costly than allocating (and "deallocating") a new object. That's why Java programmers are discouraged from pooling objects to "help the GC", something that Go developers often do when they run into issues with their non-moving, non-generational collector.
No, you're missing the fact that the allocation of memory and the GC go hand in hand, because you need it so for optimizations. They are designed together to cooperate in modern runtimes.
Not sure what you mean by this, as this has nothing to do with GC, and Java has had a multi-tier optimising compiler for 15 years now.
> that nobody else have, so fixes are available
Go has much worse problems with GC than Java does these days, and nobody else is able to achieve similar performance in large programs with heavy workloads. So everyone else lives with less sophisticated compilers and memory management simply by accepting worse performance.
Your last phrase is probably is joke ? If not, please share reproductible numbers: some things per watt, some things per MB of memory.