Java notes
Allocation Hotspot Hunting with JFR and async-profiler
Why collection-heavy code hurts your tail latency, and how to measure before optimizing
Step 1: find allocation spikes with JFR
- Which classes are allocated the most by bytes (not by count)?
- Which call paths create them?
- Does the allocation rate rise during the slow periods?
Step 2: corroborate with async-profiler
# Allocation profile for 30 seconds, written as an HTML flame graph
asprof -e alloc -d 30 -f alloc.html <pid>
Step 3: understand why boxing and collections allocate
HashSet<Integer>andHashMap<Integer, ...>box every primitive into anIntegerobject (small values are cached; larger ones are not) and allocate a node per entry.- Temporary lists and streams created per request.
- String concatenation and formatting in hot paths.
Step 4: prove an alternative with JMH
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
public class SetBenchmark {
@Param({"1000", "100000"})
int size;
int[] values;
@Setup
public void setup() {
Random r = new Random(42);
values = new int[size];
for (int i = 0; i < size; i++) values[i] = r.nextInt();
}
@Benchmark
public void boxedHashSet(Blackhole bh) {
Set<Integer> set = new HashSet<>();
for (int v : values) set.add(v);
int hits = 0;
for (int v : values) if (set.contains(v)) hits++;
bh.consume(hits);
}
@Benchmark
public void primitiveSet(Blackhole bh) {
IntOpenHashSet set = new IntOpenHashSet();
for (int v : values) set.add(v);
int hits = 0;
for (int v : values) if (set.contains(v)) hits++;
bh.consume(hits);
}
}
java -jar target/benchmarks.jar -prof gc
Step 5: validate in the real service
Failure modes
- Optimizing the wrong layer. A faster set does nothing if the allocation comes from JSON parsing upstream.
- Benchmarking unrealistic data. Use sizes and distributions that match production.
- Trading readability for nothing. Adopt a specialized library only where the profile justifies it.
- Forgetting dependencies. A new library adds maintenance and upgrade cost.
Takeaways
- Profile allocation with two tools before touching code.
- Boxing and per-entry nodes in collections are common sources of garbage.
- Prove a fix in JMH with
-prof gc, then confirm with a production-like recording.