Also the nature of pauses is different. A GC pauses everything. A malloc/free call only blocks the thread that called them. Threads that never allocate anything are never disturbed.
Here is a what I found:
"The ZGC design strives to deliver a max pause time of a few milliseconds with marginal throughput loss." [1]
"We now have an objective of 500 microseconds stop the world pause per GC cycle." [2]
Of course, those GCs have different tradeoffs in terms of max pause time, app throughput, memory fragmention/usage, etc.
[1] Deep Dive into ZGC: A Modern Garbage Collector in OpenJDK , September 2022, https://dl.acm.org/doi/fullHtml/10.1145/3538532
[2] Getting to Go: The Journey of Go's Garbage Collector, July 2018, https://go.dev/blog/ismmkeynote
Of course if you allocate arenas and reuse that cleverly you can beat any GC, but that’s far from obvious nowadays (e.g. I remember a reddit thread where naive Java beat naive Rust in some parsing to AST program (can’t find it currently). The reason was the speed of object allocation).