Then again, malloc() itself is usually not deterministic in a sense that it may take an arbitrary amount of time to make an allocation: many implementations shift work from free() to malloc() to get better asymptotic time complexity.
Also pauses from malloc/free are orders of magnitude smaller than pauses caused by tracing GC. Low pause tracing GC creators make it a big news when they manage to get pauses below 1 ms, while malloc/free operate in range of tens or maybe hundreds nanoseconds.
Tracing GC can pause a fragment of code that does not make any allocations. Refcounting can't. Refcounting has only local effects - you pay for it where you use it. But when you add tracing GC, its problems spread everywhere.
Most modern implementations of refcounting end up having a cycle collector, anyway, which also means you can end up unexpectedly paused, removing another touted advantage.
There are always trade-offs, so you can't really call any of it solved or make absolute claims. In general, GC implementations (including initially-refcounting) tend to converge over time in both performance and features.
No, it doesn't in general. Delayed destruction can be used selectively only for these object graphs that could cause large batches of deallocations, and only once you find by profiling that you really have a problem. Most object graphs are not like that, so don't penalize the average case for the edge case you might even not have at all. You can still use synchronous deallocation on the objects where immediate deallocation is needed. Secondly, I bet delayed deallocation on a background thread happens a lot sooner than waiting for the next GC to happen and to call the finalizers (which may never happen). With delayed deallocation the system knows how much there is to deallocate. With tracing GC there is no such knowledge until tracing is over.
> Most modern implementations of refcounting end up having a cycle collector, anyway, which also means you can end up unexpectedly paused, removing another touted advantage.
That's why Rust doesn't have a default cycle collector. Reference cycles are IMHO a non-issue in practice. There is no point in paying for cycle-collection by unpredictability of the whole program, when it helps only with edge-cases.
> In general, GC implementations (including initially-refcounting) tend to converge over time in both performance and features.
I'd frame it differently: all-purpose one-size-fits-all memory management solutions are almost universally suboptimal and easily beaten by solutions which give more control in programmer's hands. Rust is about providing that control about fine details and giving you a set of handy tools. The toolset might be slightly harder to learn initially due to borrow checker and lifetime rules, but over time I didn't notice any productivity hit from that (actually quite contrary, but this is a different topic).
And somehow, even without me doing any heavy optimisations, my Rust programs tend to use 100x less memory and perform way faster than my Java programs. How can it be explained, when I have much more Java experience than Rust experience?
Any double-linked structure makes it very much an issue. And before you reply with "who uses linked lists anyway", consider the publisher/listener pattern: a publisher has the list with its listeners, a listener usually knows what publisher(s) it's subscribed to.
2. A lot of stuff that need doubly-linked lists is enclosed in libraries anyways, so you don't need to worry about it.
3. Reference cycles are ugly even in GCed environments and generally should be avoided. They make programs hard to understand and follow.
Really? Using a lookup table to annotate objects externally instead of stuffing annotations inside the objects themselves makes programs harder to understand and follow? No, it's having objects with hundreds of fields that get gradually filled by different places in code too makes programs harder to understand, because it's unclear what data is available where and when. Having children in tree-like structures have pointers to their parents makes programs harder to understand and follow? Again, no: when traversing a tree one can do without to-parent pointer available in the children by stuffing it in the traversal context, but that too makes the program harder to understand.
What I meant was you should structure the program in a way to avoid circular dependencies, instead of pretending to not having them by using indexes/keys.
> Having children in tree-like structures have pointers to their parents makes programs harder to understand and follow
Yes. It is not a tree anymore. It is a graph, and even not a simple DAG. More state = harder to follow and more ways it can go wrong. For example pointers could not properly match (the parent pointer doesn't point to the correct parent). Circular dependencies also make it impossible to initialize objects without temporarily inconsistent states.
BTW I've seen real memory leaks in GCed (traced) programs because of improper use of such back-references to parent. It is really easy to screw up such structures e.g. when copying, by forgetting to update some of these references and letting them point to the old structure, keeping it in memory.
And what's even worse are circular dependencies between different components in a program. I worked on big codebases where figuring out the proper initialization order was a huge PITA because of reference cycles. And failures to get it right manifested with subtle null pointer exceptions. I'd really love the language forbid them and forced developers to strive for simpler designs.
In at least two senses:
1. Memory usage: with reference counting, at any given moment, the amount of memory used is the minimum necessary for all live references.
2. Timing: with reference counting, the only moment when memory will be released (and their corresponding destructors called) is when a reference count is decremented.
> Any refcount decrement may or may not be the last one and that may or may not cause a huge graph of objects being free()d which (even without destructors) takes unpredictable amount of time.
That's actually predictable: it will always happen precisely at the last refcount decrement for that object, and will release only that object and other objects owned exclusively by that object, and nothing else. The size of that "huge graph of objects" is bounded, which makes the time taken to release it predictable.
By the same line of reasoning, since the total size of the heap is bounded, it makes the time taken to sweep through it predictable.