Measuring Performance

Measure First

"Premature optimization is the root of all evil." Most slowness in real apps comes from a few places: database queries (N+1, missing indexes), network calls, and bad algorithms (O(n²) loops). Find them with measurements, not guesses.

ToolUse
JMH (Java Microbenchmark Harness)correct micro-benchmarks (handles JIT warm-up, dead-code elimination)
Java Flight Recorder + Mission ControlCPU hot spots, allocations, locks in production
async-profilerflame graphs with very low overhead
Spring Boot Actuator + Micrometerrequest latency percentiles (p95, p99)
APM (New Relic, Datadog, Elastic)slow endpoints, slow SQL in production

A JMH Benchmark

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 3) @Measurement(iterations = 5) @Fork(1)
@State(Scope.Benchmark)
public class ConcatBenchmark {
    @Param({"100", "1000"}) int n;

    @Benchmark
    public String plusInLoop() {
        String s = "";
        for (int i = 0; i < n; i++) s += i;
        return s;                        // return the result so the JIT cannot remove the work
    }

    @Benchmark
    public String stringBuilder() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) sb.append(i);
        return sb.toString();
    }
}

Example

java -jar target/benchmarks.jar ConcatBenchmark

Example output

Benchmark                       (n)  Mode  Cnt    Score   Error  Units
ConcatBenchmark.plusInLoop      100  avgt    5    2.913 ± 0.120  us/op
ConcatBenchmark.plusInLoop     1000  avgt    5  187.402 ± 6.551  us/op
ConcatBenchmark.stringBuilder   100  avgt    5    0.611 ± 0.020  us/op
ConcatBenchmark.stringBuilder  1000  avgt    5    6.884 ± 0.233  us/op

Big-O Still Matters

Example

Checking membership in a List is O(n); in a HashSet it is O(1). With 20,000 ids, the list version does about 200 million comparisons; the set version about 20,000 lookups. (The program counts work instead of printing timings, so the output is stable.)

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> blockedList = IntStream.range(0, 20_000).map(i -> i * 2).boxed().toList();
        Set<Integer> blockedSet = new HashSet<>(blockedList);
        List<Integer> incoming = IntStream.range(0, 20_000).boxed().toList();

        long listHits = incoming.stream().filter(blockedList::contains).count();   // O(n * m)
        long setHits = incoming.stream().filter(blockedSet::contains).count();     // O(n)

        System.out.println("Blocked (list): " + listHits);
        System.out.println("Blocked (set):  " + setHits);
        System.out.println("Worst-case comparisons with list: " + (long) incoming.size() * blockedList.size());
        System.out.println("Hash lookups with set: " + incoming.size());
    }
}

Output

Blocked (list): 10000
Blocked (set):  10000
Worst-case comparisons with list: 400000000
Hash lookups with set: 20000