Pass by Value / Pass by Reference — Optimization¶
Twelve before/after exercises focused on argument passing performance.
Optimization 1 — Avoid boxing in hot paths¶
Before:
public void process(Integer count) { ... }
for (int i = 0; i < 1_000_000; i++) {
process(i); // boxes each iteration (uncached for large i)
}
After:
public void process(int count) { ... }
for (int i = 0; i < 1_000_000; i++) {
process(i); // no boxing
}
Why: boxing int to Integer for values outside [-128, 127] allocates. Hot loops can be GC-pressure dominated.
Optimization 2 — Primitive specializations¶
Before:
After:
Why: primitive streams (IntStream, LongStream, DoubleStream) avoid boxing.
Optimization 3 — Records for multiple returns¶
Before:
Awkward + mutable.
After:
JIT-friendly, immutable, escape-analysis-eligible.
Optimization 4 — Defensive copy with List.copyOf¶
Before:
After:
Why: List.copyOf returns the input directly if already immutable. Otherwise creates immutable list. Fewer allocations.
Optimization 5 — Avoid varargs in hot paths¶
Before:
After: specific overloads:
public void log(Object a) { ... }
public void log(Object a, Object b) { ... }
public void log(Object a, Object b, Object c) { ... }
public void log(Object... args) { ... } // fallback
JIT picks the fixed-arity overload, no array allocation.
Optimization 6 — Final parameters help readability¶
Before:
After:
final prevents reassignment; introduces clarity. No runtime effect.
Optimization 7 — Pass arrays for ABI-friendly data¶
When passing many primitives, an array can be more ABI-friendly than many separate primitives:
Before:
double computeFromMany(double a, double b, double c, double d, double e, double f, double g, double h);
8 args may not fit in registers; some spill to stack.
After (when args are uniform):
Pass one pointer; the loop can vectorize.
Trade-off: array allocation vs argument register pressure. Profile.
Optimization 8 — Records and escape analysis¶
public double distance(Point a, Point b) {
return Math.hypot(a.x() - b.x(), a.y() - b.y());
}
double d = distance(new Point(1, 1), new Point(2, 2));
If Point is final and the args don't escape, C2 scalarizes — no allocation. The two new Point calls become register operations.
Verify with -XX:+PrintEliminateAllocations.
Optimization 9 — Avoid mutable inputs as method args¶
Before:
Caller's list mutated. Surprising. Hard for JIT to prove non-escape.
After:
public List<Item> processed(List<Item> items) {
return items.stream().filter(i -> !i.expired()).toList();
}
Functional, return-based, JIT-friendlier.
Optimization 10 — MethodHandle for typed reflection¶
Before (reflection):
Method m = obj.getClass().getMethod("compute", int.class);
m.invoke(obj, 5); // ~100x slower than direct
After:
private static final MethodHandle COMPUTE = MethodHandles.lookup()
.findVirtual(I.class, "compute", MethodType.methodType(int.class, int.class));
int result = (int) COMPUTE.invokeExact((I) obj, 5);
MethodHandle.invokeExact can be JIT-inlined.
Optimization 11 — Argument-eager evaluation¶
If an argument is expensive, evaluate it once into a local:
Before:
After:
Saves one expensive call.
Optimization 12 — Avoid lambda allocation in hot loops¶
Before:
Wait — that's not right syntactically. The point: capturing lambdas may allocate.
After: lift non-changing lambdas out of loops:
Function<X, Y> f = item -> doSomething(item); // outside loop
for (var item : items) {
list.add(f.apply(item));
}
For non-capturing lambdas, the JIT caches automatically. For capturing, lift if possible.
Tools cheat sheet¶
| Tool | Purpose |
|---|---|
-XX:+PrintEliminateAllocations | EA decisions |
async-profiler -e alloc | Allocation flame graph |
-XX:+PrintInlining | Inlining of method calls |
jol-cli | Object layout |
jmh | Microbenchmark argument cost |
When to apply¶
- Hot inner loops with many calls
- Allocation profile shows boxing/varargs/lambda allocations
- Tight numeric kernels
- High-throughput services
When not to¶
- Cold paths (config loading, startup)
- Code clarity matters more than tiny speedup
- Already JIT-optimized (verify with PrintInlining)
Memorize this: Java's pass-by-value is fast. The JIT often passes args via registers, eliminates intermediate allocations via escape analysis, and inlines monomorphic calls. Avoid boxing, varargs allocation, and capturing lambdas in hot paths. Use records for multi-return; immutable types for parameters.