Aside, I'm curious how first-class support for value types and Go-style stack allocation via escape analysis changes the value proposition of the generational hypothesis. If we hypothesize that short-lived objects are local to a single function scope (and thus eligible for stack allocation either explicitly via a value type or heuristically via escape analysis) then it might completely upend the generational hypothesis and make it so that relatively more long-lived objects are getting heap-allocated. Surely someone's done some studies on this?
You can craft a workload which violates the hypothesis by only allocating objects that live for a long time but both JVM and .NET GC implementations are still much faster designs than Go's GC which prioritizes small memory footprint and consistent latency on low allocation traffic (though as of .NET 9, SRV GC puts much more priority on this, making similar tradeoffs).
Would Java's moves towards "integrity by default" mean that this could be ruled out in more cases?
I'm not privy to the exact APIs that OpenJDK exposes but in .NET the main limitation around escape analysis that spans multiple methods is the fact that CoreCLR has re-JIT API which allows to perform a multitude of actions like attaching a profiler or a debugger to a live application and forcing the runtime to deoptimize a particular method for debugging, or modifying the implementation and re-JITting the result. Debug codegen is very different especially around GC liveness tracking and escape analysis that builds on top of it - it means that even debug code would have to uphold stack-allocated nature of such object in some way, complicating the design significantly. In addition to that, debuggers and other instrumentation may observe object references that would have otherwise not escaped local scope.
This creates an unfortunate situation where the above almost never happens in production, but ignoring it and "just stack-allocating anyway" would lead to disastrous breakage for all sorts of existing instrumentation. Because Go does not have to deal with this constraint, it can perform interproc escape analysis without risk - whether a pointer escapes or not can be statically proven. For NativeAOT, .NET could approach this problem in the same way, but paraphrasing compiler team: "We would like to avoid optimizations only available for one of the target types be it JIT or AOT, and only supporting AOT would not benefit the majority of the .NET users".
There is, however, recognition that more comprehensive interproc analysis could be very beneficial, including the EA which is why it is planned to work on it in .NET 10:
- https://github.com/dotnet/runtime/issues/108931 IPA framework
- https://github.com/dotnet/runtime/issues/104936 Stack allocation enhancements
Profiling and debugging would be separate considerations -- I'm really not sure what limitations those impose on the JVM JIT.
I don't have numbers at hand, but I remember the JDK Expert Group talking about this extensively in the past and why they deferred bringing Value Types for such a long time. They hoped complex enough EA can get rid of indirections and heap allocations but it just wasn't powerful enough, even with all advances throughout the years.
Heap allocation is what requires requesting memory from an allocator.
Well, variables cannot be forced to stack specifically. They are placed in the "local scope". And that would usually be either stack or CPU registers - thinking in stack only is a somewhat flawed mental model.
Both C# and F# complicate this by supporting closures, iterator and async methods which capture variables placing them in a state machine box / display class instead which would be located on the heap, unless stack-allocated by escape analysis (unlikely because these usually cross method boundaries).
However, .NET has `ref structs` (or, in F#, [<Struct; IsByRefLike>] types) which are subject to lifetime analysis and can never be placed on the heap directly or otherwise.
According to https://github.com/golang/go/discussions/70257#discussioncom...
> the weak generational hypothesis does not hold up well with respect to heap-allocated memory in many real-world Go programs (think 60-70% young object mortality vs. the 95% typically expected)
Essentially the stack is a form of younggen. It is not as complete (as there are things which must be heap allocated) but because it is, it reduces the benefits of a generational GC… without having much impact on its costs and complexity.
Depending on work load, that competition can be sufficient to make a generational GC net negative.
The generational hypothesis is about object lifetime, and that doesn't change.
It does change the relevance of the generational hypothesis to garbage collection.
> Surely someone's done some studies on this?
The go team has, and that's why go doesn't have a generational GC. The complexity of adding generational support, especially in a mutation-based language (so needing memory barriers and the like) was found not to benefit when a significant fraction of the newborn objects don't even reach the youngen. See https://github.com/golang/go/discussions/70257#discussioncom... from the current discussion of adding opt-in ad-hoc support for memory regions.
The idea of mark/scavenge is pretty cool for page-based allocation and deallocation.
Also real-time or maybe latency-intolerant systems, which basically can't block for garbage collection. You could probably keep scavenging and stay ahead of things without impacting things in a black and white way.
It’s Oracle.