Phase 5 · Professional DevelopmentModule 38~38 min read

Performance Optimization

Measure, then optimize: algorithms, memory, I/O, caching, and JVM tuning.

What you'll learn

Fast software matters — but so does not wasting effort optimizing the wrong thing. This module teaches a disciplined approach: measure, target the real bottleneck, and know which changes actually move the needle.

By the end you'll be able to:

  • Measure before optimizing — and know why
  • Prioritise the changes that matter most
  • Reduce memory pressure and string overhead
  • Optimise collections, I/O, and database access
  • Use caching, benchmark with JMH, and understand JVM tuning

Measure first

The cardinal rule of performance: measure, don't guess. Developers are famously bad at predicting bottlenecks — the slow part is rarely where you think. Profile the running program (Module 30), find the actual hotspot, fix it, then measure again to confirm you helped.

Premature optimization

Donald Knuth's famous line: "Premature optimization is the root of all evil." Write clear, correct code first. Optimise only what a profiler proves is slow — and only when the speed actually matters. Clever, unmeasured tweaks usually just add bugs and complexity.

Algorithmic wins come first

When performance does matter, your algorithm and data structure choices dwarf everything else. Turning an O(n²) loop into O(n log n), or swapping a linear search for a HashMap lookup, can make code thousands of times faster — no micro-optimization comes close:

Where to look, in order of impact
1. Algorithm & data structureBiggest wins — O(n²) → O(n log n) beats any micro-tweak
2. I/O & databaseNetwork and disk are orders of magnitude slower than CPU
3. Memory & allocationFewer objects, less GC pressure
4. Micro-optimizationsLast resort — small, often invisible gains

Memory & strings

Creating fewer objects means less work for the garbage collector (Module 27). The classic example is string building: concatenating with + in a loop creates a new immutable String every iteration — quadratic work. A StringBuilder reuses one buffer:

Strings.java
int n = 100_000;

// ✗ O(n²): each += builds a brand-new String
String s = "";
for (int i = 0; i < n; i++) s += i;

// ✓ O(n): one mutable buffer, appended in place
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) sb.append(i);
String result = sb.toString();

Note

Other wins: reuse objects where practical, prefer primitives over boxed wrappers in hot loops (Module 15), and size collections up front (new ArrayList<>(expectedSize)) to avoid repeated resizing.

Collections & I/O

Choosing the right collection (Module 14) is a performance decision: HashMap lookups are O(1) vs O(n) for scanning a list. And I/O — disk and network — is orders of magnitude slower than CPU work, so it's often the real bottleneck. Buffer your I/O (Module 19), batch database queries instead of looping (avoid the "N+1 query" problem), and fetch only the data you need.

Caching

The fastest work is work you don't repeat. Caching stores the result of an expensive operation so subsequent calls are instant. Map.computeIfAbsent is a clean way to cache in memory; at scale, a dedicated cache like Redis serves many servers:

Caching.java
import java.util.*;

Map<Integer, Long> cache = new HashMap<>();

long expensive(int key) {
    return cache.computeIfAbsent(key, k -> {
        // ...slow computation runs only once per key...
        return (long) k * k;
    });
}

Watch out

Caching adds a hard problem: invalidation — keeping the cache in sync when the underlying data changes. A stale cache serves wrong answers. Cache only what's worth it, and have a clear expiry/refresh strategy.

Benchmarking & JVM tuning

Micro-benchmarking Java is deceptively hard because of JIT warm-up (Module 27) — naive timing gives wrong numbers. The standard tool is JMH (Java Microbenchmark Harness), which handles warm-up and statistical rigor for you. At the deployment level, JVM tuning — heap size (-Xmx), garbage-collector choice — can matter for high-performance services, but only after your code and algorithms are sound.

Recap & quick check

Key takeaways

  • Measure with a profiler before optimizing — the bottleneck is rarely where you'd guess.
  • Algorithm and data-structure choices give the biggest wins by far.
  • Reduce allocations: use StringBuilder, primitives in hot loops, and pre-sized collections.
  • I/O and database access are usually slower than CPU — buffer, batch, and fetch only what you need.
  • Cache expensive results (mind invalidation); benchmark with JMH; tune the JVM last.

Quick check

1. What's the first step before optimizing?

2. Which change typically gives the biggest performance win?

3. Why use StringBuilder in a loop instead of +=?

4. What's the main risk of caching?

5. Which tool is designed for reliable Java micro-benchmarks?

Great — you can now make code fast the disciplined way. Next up: Module 39 — Packaging, Deployment & Distribution.