Course outline

Java / Advanced / Lesson 16

Profiling, garbage collection, and retention

Investigate performance through comparable workloads and runtime evidence. Separate allocation, retained live data, heap limits, native memory, and collector tradeoffs.

About 5 min readContent reviewed
On this page

Lesson goals

  • Define a performance hypothesis and useful metrics.
  • Explain why reachable objects remain live.
  • Use JFR and GC evidence without inventing benchmark results.

Separate latency, throughput, and resource cost

A performance investigation begins with a measurable complaint: elevated tail latency, lower throughput, CPU saturation, or growing memory under a stated workload. Record request mix, data size, concurrency, runtime version, and resource limits. Comparing a cold first execution with a warmed-up process can confuse class loading and JIT compilation with the business operation's steady-state cost.

Do not time one tiny expression with currentTimeMillis and call it a benchmark. JVM optimization, dead-code elimination, constant folding, warmup, and measurement overhead can dominate. For microbenchmarks use an appropriate dedicated methodology; for service tuning compare representative end-to-end work and preserve correctness. A faster wrong answer is not an optimization, and a higher average throughput can hide unacceptable tail latency.

Understand why garbage collection cannot forget a cache

Garbage collection reclaims unreachable objects according to its implementation and reachability rules. A static map, unbounded queue, listener registry, or thread-local reference can keep data reachable indefinitely. That is a logical retention leak even though no manual free operation exists in Java. Increasing heap may delay the failure while preserving the underlying growth pattern.

The example removes a synthetic entry from an owned map and prints its size before and after. It demonstrates dropping the map's reference, not forcing immediate reclamation or proving a measured heap decrease. Other references may still retain the object. Do not use System.gc as an assertion that a particular object must be collected before the next line.

java Remove one owning reference without claiming GC timing
public class Main {

 public static void main(String[] args) throws Exception {
var cache=new java.util.HashMap<String,byte[]>();
cache.put("sample",new byte[16]);
System.out.println(cache.size());
cache.remove("sample");
System.out.println(cache.size());
 }
}
Expected output
1
0

The map loses its entry. The example makes no statement about immediate reclamation, heap size, or collection count.

Bound retention by a visible policy

A cache needs an entry or weight budget, an eviction or expiry policy, and ownership of cleanup. A count limit alone may be insufficient when values have widely different sizes. Queues and asynchronous submissions also retain task arguments, so overload can resemble a cache leak. Examine the retaining path and workload arrival/completion rates before selecting a collector flag.

The access-order LinkedHashMap example retains two entries and evicts the eldest on a third insertion. It is a small single-threaded policy illustration, not a production concurrent cache library. The printed keys are b and c because a was eldest and no read changed access order. A real design also needs concurrency, size accounting, failure behavior, and an explicit lifecycle.

java A bounded single-threaded retention policy
public class Main {

 public static void main(String[] args) throws Exception {
var cache=new java.util.LinkedHashMap<String,Integer>(4,0.75f,true) {
 protected boolean removeEldestEntry(java.util.Map.Entry<String,Integer> e) { return size()>2; }
};
cache.put("a",1);cache.put("b",2);cache.put("c",3);
System.out.println(cache.keySet());
 }
}
Expected output
[b, c]

Inserting c grows the map beyond two entries, so the eldest entry a is removed. This does not make the map thread-safe.

Correlate JFR, GC logs, and retained objects

JDK Flight Recorder can capture CPU samples, allocation, locks, I/O, and GC-related events according to the recording configuration. Event thresholds and sampling affect what is visible; an absent event is not proof that no short operation occurred. GC logs can relate pause times and heap behavior to the incident window. Collector tuning trades latency, throughput, and memory rather than optimizing every dimension simultaneously.

Heap occupancy after a collection, allocation rate, and total process resident memory are different measures. Native buffers, thread stacks, code cache, and other native structures contribute beyond the Java heap. Heap dumps and recordings may contain sensitive data and can impose operational cost. Use authorized isolated captures or approved production procedures; this course does not attach to existing application processes.

Change one supported hypothesis and verify

If live data grows with unique user keys, investigate unbounded retention before increasing Xmx. If allocation is high but live data stays bounded, investigate allocation sites and GC overhead. If workers are mostly waiting on I/O, a CPU micro-optimization may not address latency. Use comparable before/after windows and keep a rollback path for tuning or code changes.

A useful report states the workload, observed metric, causal hypothesis, change, correctness checks, and outcome with limits. Never fabricate pause durations or universal collector rankings from a tiny teaching program. The examples here produce deterministic structural results only; performance claims require representative measurement on the actual supported runtime and deployment resource model.

Common errors and fixes

OutOfMemoryError despite garbage collection

Why it happens
Reachable live data or another memory budget exceeds available capacity.
How to fix it
Identify retaining paths and resource category before changing limits.
Prevent it next time
Bound caches, queues, task admission, and input sizes.

A single stopwatch run suggested a huge speedup

Why it happens
Warmup or optimizer effects dominated an uncontrolled measurement.
How to fix it
Use appropriate benchmarking and comparable workloads.
Prevent it next time
Record runtime, data, warmup, and metric definitions.

A heap dump was shared publicly

Why it happens
Diagnostic artifacts were treated as harmless metadata.
How to fix it
Restrict and sanitize evidence through the incident process.
Prevent it next time
Classify dumps and recordings as potentially sensitive.

Check your understanding

1. An unbounded static map keeps every request object. Why does GC not solve it?

Show worked solution

The map remains reachable and therefore retains its values. GC cannot infer the business retention deadline. Add a bounded lifecycle and remove unnecessary references, then verify live-data trends under representative traffic.

2. Heap remains stable but process memory grows. What should be considered?

Show worked solution

Investigate native allocations, direct buffers, thread stacks, code cache, and measurement definitions. Do not assume all process memory is controlled by Xmx, and correlate the growth with thread counts, buffers, and workload events.

Key takeaways

  • Reachability determines retention, not business usefulness.
  • Heap, allocation rate, and process memory are different metrics.
  • Performance conclusions require controlled workload evidence.

Official references