allocation and TLABs
new is usually just a pointer bump. here's why it's so fast.
- chapter
- chapter 14.02
- reading time
- 8 min read
each thread gets a private chunk of eden called a TLAB (thread-local allocation buffer). allocating an object is just bumping that thread's top pointer by the object's size: no lock, no CAS, a handful of instructions.
that's why new costs a couple of nanoseconds in java, and why allocating short-lived objects is way cheaper than people think.
the picture
you know the folklore: "allocation is expensive, reuse your objects, pool everything." that was true-ish in 1998. in a modern JVM, new Point(1, 2) is cheaper than most method calls you'd write to avoid it.
think of eden (the part of the heap where new objects are born) as one long roll of paper. if every thread had to grab the roll to tear off a strip, they'd be fighting over it all day. so instead each thread tears off a big piece once, keeps it on its own desk, and cuts little strips from that whenever it needs one. the big piece is the TLAB. cutting a strip is moving a pencil mark to the right (bumping a pointer). only when your piece runs out do you go back to the shared roll.
eden is empty and no thread has a TLAB yet. press allocate.
new byte[big] too.see the ratio? nearly everything is the fast path. the shared pointer (the only place threads could fight) gets touched once per TLAB, not once per object.
why new is so cheap
here's what the JIT inlines at every new Point(x, y), written out as pseudo-code. no function call, no lock:
obj = thread.tlab.top // where the object goes
newTop = obj + 24 // a Point is 24 bytes, known at compile time
if (newTop > thread.tlab.end) // doesn't fit? rare
goto slowPath
thread.tlab.top = newTop // claim it. nobody else uses this TLAB
obj.header = <unlocked, age 0> // mark word
obj.klass = Point // class pointer
obj.x = 0; obj.y = 0 // zero the fields (why an unset int field is 0)
// ...then the constructor runsa load, an add, a compare, a store, and writing the header. that's the whole allocation. the object's size is a constant, the TLAB belongs to the current thread, and the thread pointer is sitting in a register.
the only reason this works is that eden is one contiguous, empty block. after a young GC, every survivor has been copied out, so the space is clean and you can hand it out in order. that's the deal: bump allocation is cheap because the collector compacts.
i measured it (jdk 25, an arm mac). a loop doing ring[i & 1023] = new Point(i, i) 200 million times, GC included:
new Point(i, i), lower is better. with four threads and no TLABs, every allocation is a CAS on one shared pointer and they spend their time fighting over it. measured with a plain loop, not JMH, but a 100× gap doesn't need error bars.public class Alloc {
record Point(int x, int y) {}
static final Object[] ring = new Object[1024];
public static void main(String[] args) {
for (int round = 0; round < 5; round++) {
long n = 200_000_000L;
long t0 = System.nanoTime();
for (int i = 0; i < n; i++) ring[i & 1023] = new Point(i, i);
long t1 = System.nanoTime();
System.out.printf("%.2f ns per new Point%n", (t1 - t0) / (double) n);
}
}
}the ring matters: it makes each Point escape, so the JIT can't optimize the allocation away (more on that below). run it with -XX:-UseTLAB to see the slow version.
when it doesn't fit
the fast path fails maybe once every few thousand objects. then it gets more expensive, one step at a time:
hover a stage.
TLABs aren't a fixed size either. the JVM resizes each thread's TLAB based on how much that thread allocates between GCs, so a busy thread gets a big one and an idle thread doesn't hog eden. you can watch it happen with -Xlog:gc+tlab=trace. here's a slice of a real run (three threads making byte[64]s, jdk 25, G1):
TLAB: fill thread: 0x00000079590a6800 [id: 43267] desired_size: 512KB slow allocs: 0 refill waste: 8192B ...
TLAB: fill thread: 0x00000079590a7000 [id: 43011] desired_size: 512KB slow allocs: 0 refill waste: 8192B ...and the summary it prints at every GC (-Xlog:gc+tlab=debug):
GC(3) TLAB totals: thrds: 3 refills: 304 max: 118 slow allocs: 0 max 0 waste: 0.2% ...
GC(3) Pause Young (Normal) (G1 Evacuation Pause) 153M->1M(256M) 0.408msread it: each thread's TLAB was 512 KB. a TLAB gets retired when less than 8 KB (1/64 of it) is left, otherwise the odd object goes outside it. 304 refills between two GCs, 0.2% of eden wasted on fillers. and then the GC: 153 MB of eden in, 1 MB survived, 0.4 ms. that last line is generational collection in miniature: almost nothing survives.
big objects skip all of this
an object bigger than a TLAB can't go in one, so it takes the CAS path into eden. in G1 (the default collector) it goes further: anything at least half a region is humongous and gets its own run of contiguous regions, outside eden entirely. regions are 1 to 32 MB depending on heap size (1 MB for a 256 MB heap, 4 MB for 6 GB).
the cheapest allocation is the one that never happens+
if the JIT can prove an object never escapes the method (never stored in a field, never returned, never passed somewhere that keeps it), C2's escape analysis can replace it with its fields in registers. no TLAB bump, no header, no garbage. that's why benchmarks of "allocation in a loop" often measure nothing, and why the measurement above stores every Point into an array. see JIT optimizations.
- each thread allocates in its own TLAB, a private slice of eden.
newis a pointer bump: check, add, store, write the header. no locks.- the shared eden pointer is only touched once per TLAB, with a CAS.
- bump allocation only works because the GC keeps eden empty and contiguous.
- big objects skip TLABs. in G1, half a region or more is humongous.