RU | EN | DE

JIT Compiler (Just-In-Time)

What it is and how it works

  • JVM first interprets bytecode. Hot spots (frequently called methods/loops) are profiled, and the JIT compiler compiles them into native code.
  • Tiered Compilation (default): C1 (fast, fewer optimizations) → C2 (slower compilation, aggressive optimizations). OSR (On-Stack Replacement) is possible – “re-hosting” a hot loop onto compiled code directly in the middle of execution.
  • Key optimizations: inlining (inline calls), escape analysis (field/scalarization, stack placement), narrowly specialized intrinsics (System.arraycopy, Math.*), loop unrolling, vectorization.

How to observe and manage

-XX:+PrintCompilation
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
-XX:CompileThreshold=10000  # "hotness" threshold
-XX:TieredStopAtLevel=1..4  # limit tiered levels
-XX:+DoEscapeAnalysis  # usually enabled

Typical errors

  • Compares performance of “cold” code without warm-up.
  • Writes microbenchmarks without JMH ⇒ measures JIT/GC, not code.
  • Pessimizes JIT: frequent virtual calls, non-final classes/methods, excessive abstraction hinder inlining.

Best practices for JIT

  • Warm up code before measurements (or use JMH).
  • Make key hot methods small and “inline-friendly”.
  • Avoid unnecessary boxing/unboxing in hot paths, especially in Stream with auto-boxing.
  • Cache computations, remove polymorphism where inlining is important (sealed/final).

JVM Memory: Heap, Stack, Metaspace (+ other areas)

Memory Map

  • Heap — objects. Divided into Young (Eden + Survivor S0/S1) and Old. Fast allocation via TLAB (Thread-Local Allocation Buffer) and “bump-the-pointer”.
  • Stack — call frames for each thread: local variables, references, return address.
  • Metaspace — class metadata (replacement for PermGen in Java 8). Grows in native memory. Controlled by -XX:MaxMetaspaceSize.
  • Code Cache — compiled JIT code (-XX:ReservedCodeCacheSize).
  • Direct memory (off-heap NIO) — managed by the OS (-XX:MaxDirectMemorySize).

Quick Size Setup

-Xms2g -Xmx2g
-XX:MaxMetaspaceSize=512m
-XX:ReservedCodeCacheSize=256m

Garbage Collectors (very briefly)

  • G1 (default in 17+) — regional, predictable pauses, good default.
  • ZGC / Shenandoah — low pauses, large heaps, advanced (if minimal latency is needed).
  • General concepts: Stop-the-World, safepoints, card table, write barriers.

Typical errors

  • Small heap ⇒ frequent GC pauses, OutOfMemoryError.
  • Leaks through static collections/caches or listeners (refs are not released).
  • Does not consider off-heap (DirectByteBuffer, native driver memory) and blames the heap.

Best practices for memory

  • Start with G1, ensure the heap size is reasonable (minimum = maximum for stability).
  • Monitor metaspace and direct memory.
  • Profile allocations; reduce short-lived objects in hot code.
  • For caches — soft/weak references as needed, limit size.

Diagnostic Tools

1. jconsole (simple telemetry via JMX)

  • Launch: jconsole → select process (local) or connect via JMX.
  • What to look for:
    • Memory: heap/metaspace graphs, Perform GC button (for diagnostics only).
    • Threads: thread states, Detect Deadlock.
    • MBeans: application/pool parameters (if exposed).
  • When useful: first approximation – see heap growth, GC count, deadlock in one click.

2. jvisualvm (GUI profiler/dump analysis)

  • Launch: jvisualvm (or VisualVM as a separate installation).
  • Features:
    • Sampler CPU/Memory (less overhead) and Profiler (more accurate, but heavier).
    • Open heap dump (*.hprof), view Top Consumers (who eats memory), inspect reachability graph.
    • Plugins: VisualGC (heap generation graph).
  • When useful: find leaks, hot methods, confirm hypotheses before/after fixes.

3. jmap (heap snapshot and histogram)

Основные команды:

jmap -histo <PID>  # top classes by count/size
jmap -histo:live <PID>  # only live objects (after GC)
jmap -dump:format=b,file=heap.hprof <PID>  # heap dump
  • Usage: see what the heap is filled with now; download dump and open in VisualVM/YourKit/JProfiler.

4. jstack (thread dump)

jstack -l <PID>  # thread dump with monitors
jstack -m <PID>  # with native code stack

What to look for:

  • Threads in BLOCKED (monitor/lock), WAITING/TIMED_WAITING (waiting), RUNNABLE (CPU).
  • Cyclic dependencies (deadlock): usually a section “Found one Java-level deadlock” at the end of the dump.
  • “Hot thread”: thread that is constantly in RUNNABLE with the same stack trace.

Useful addition (not requested, but must-have): jcmd <PID> VM.native_memory summary – native/metaspace summary; jcmd <PID> GC.heap_info | GC.class_histogram | Thread.print.

Mini-Cases (code → how to diagnose)

1. Memory leak through a static list

public class Leak {
  private static final List<byte[]> bag = new ArrayList<>();
  public static void main(String[] args) throws Exception {
  while (true) {
    bag.add(new byte[1_000_000]); // 1 MB
    Thread.sleep(50);
  }
}

Diagnosis:

  1. jconsole → Memory grows incrementally, GC doesn’t help → suspicion of retention.
  2. jmap -histo:live <PID> → many byte[].
  3. jmap -dump:format=b,file=heap.hprof <PID> → open in VisualVM, find GC Roots → see static field Leak.bag. Fix: limit/clean the cache, use WeakReference/SoftReference if necessary.

2. Deadlock due to lock order

class Locks {
  private final Object a = new Object();
  private final Object b = new Object();
 
  void m1() { synchronized (a) { sleep(100); synchronized (b) {} } }
  void m2() { synchronized (b) { sleep(100); synchronized (a) {} } }
}

Diagnosis:

  • jconsole → Threads → Detect Deadlock.
  • jstack -l <PID> → section “Found one Java-level deadlock” with list of threads and monitor owners. Fix: strict lock order (always a then b), or tryLock/timeouts, or larger lock granularity.

3. High CPU on one thread

Symptom: 100% cores. Diagnosis:

  • top/htop → PID → tid of thread with high CPU.
  • jstack <PID> several times in a row → the same stack trace in RUNNABLE ⇒ hot spot.
  • Then jvisualvm CPU Sampler/Profiler for confirmation.

Check-list for problem searching (quick)

  1. Memory: jconsole graphs → jmap -histo:live → heap dump → VisualVM.
  2. Pauses/GC: enable GC logs (-Xlog:gc* in 17+), check collector/heap size.
  3. CPU: jstack several times → hot method → jvisualvm Sampler.
  4. Locks: jconsole Detect Deadlock → jstack -l.
  5. Native/Metaspace: jcmd VM.native_memory summary, -XX:MaxMetaspaceSize.

Best practices (summary)

  • For production: G1, adequate -Xms = -Xmx, GC logs (-Xlog:gc*:file=gc.log).
  • For analysis: VisualVM/jconsole for initial symptoms, jmap/jstack for facts.
  • Reduce allocation in hot spots, avoid unnecessary boxing and temporary objects.
  • Design locks: order, timeouts, Lock/StampedLock for read access.
  • Cover microbenchmarks with JMH, not custom timers.
  • Monitor Metaspace/Code Cache/Direct memory – these are also causes of OOME.
  • For caches — limits and weak/soft references where appropriate.