A service uses a stable amount of memory. There is no leak, memory usage is flat, and the garbage collector runs constantly and consumes a substantial fraction of available CPU.
Nothing is being retained. Everything is being created — a high rate of short-lived objects that are collected almost immediately. The collector is doing exactly what it should, at a cost that shows up as CPU time nobody attributes to allocation.
What changed in 2026
- Continuous allocation profiling in production spread. Low-overhead sampling made this practical outside development.
- Allocation and heap views got clearly separated. Tooling stopped conflating what is created with what is retained.
- Escape analysis improved. Runtimes eliminating more allocations automatically reduced the need for manual avoidance.
- The GC-pressure framing became standard. Understanding collection cost as a function of allocation rate became common knowledge.
Two different questions
|
Allocation profiling |
Heap profiling |
| Shows |
What is being created |
What is currently retained |
| Finds |
GC pressure, avoidable churn |
Leaks, unexpected retention |
| Typical symptom |
High CPU in collection |
Memory growing over time |
| Actionable on |
Hot paths creating objects |
References held too long |
They answer different questions and are frequently confused.
Allocation profiling attributes allocations to the code that made them. Useful when the collector is busy and memory is stable — the objects are being created and discarded rapidly.
Heap profiling shows what is alive right now and what holds references to it. Useful when memory grows steadily — something is retaining objects it should release.
A steadily-growing heap is a leak. A high allocation rate with flat memory is churn. Different problems, different fixes.
Where churn comes from
The pattern is nearly always allocation inside a loop or a hot path.
Objects created per iteration. A temporary object inside a loop running a million times is a million allocations.
String concatenation in loops. Each concatenation creates a new string; building incrementally in a loop is quadratic in allocation.
Boxing and wrapper objects. Converting primitives to objects in managed runtimes allocates, frequently invisibly.
Defensive copying. Copying a collection before returning it is safe and allocates every call.
Intermediate collections in chained operations. Each stage of a transformation pipeline may materialise a new collection.
Closures capturing variables. In some runtimes, capturing state allocates per call.
The useful lens is rate, not size. A large allocation once is fine. A small allocation a million times is the problem, and it is precisely the one that looks harmless in code review.
Fixing it selectively
Most allocation is irrelevant. Optimising it everywhere makes code worse for no benefit.
Fix allocations that a profiler shows in a hot path, and confirm the improvement with a measurement. The usual techniques: reusing buffers rather than allocating per call, preallocating collections to their known size, avoiding intermediate collections in chains, and using value types where the runtime supports them.
Object pooling is a heavier hammer that reintroduces manual lifetime management, which is the thing garbage collection exists to avoid. Reach for it only where profiling shows a specific allocation dominating and simpler approaches have not worked.
The complementary view matters too: a CPU flame graph will show time spent in collection, which tells you whether allocation is a problem at all before you go looking for it — see flame graphs.
Common mistakes
- Confusing allocation rate with a leak. Flat memory with high churn is not leaking.
- Micro-optimising outside hot paths. Worse code, no benefit.
- Object pooling by default. Reintroduces manual lifetime management.
- Profiling in development only. Production hot paths differ.
- Ignoring the collector's own CPU cost. It appears as unattributed time.
- Not measuring after a change. Runtimes optimise more than you expect.
- Assuming allocation is free because there is no leak. It costs CPU.
FAQ
How do I know if allocation is my problem?
Check what fraction of CPU time goes to garbage collection. If it is meaningful, allocation profiling is worthwhile. If it is small, look elsewhere.
Does this apply to non-garbage-collected languages?
Differently — allocation still costs, and there is no collector. Profiling allocation there is about avoiding allocator overhead and fragmentation rather than collection pressure.
Will the runtime optimise my allocations away?
Sometimes — escape analysis eliminates allocations that do not outlive their scope. It is not guaranteed, and measuring after a change tells you whether the runtime was already handling it.
Should I tune the garbage collector instead?
Worth trying and usually less effective than reducing allocation. Collector tuning changes when the cost is paid; reducing allocation removes it.
Where to go next
For CPU profiling generally, read flame graphs. For the concurrency model that affects allocation patterns, async vs threads and thread pools.