Skip to main content

Overview

Debugging OpenJDK requires understanding both Java and native code debugging techniques. This guide covers tools and approaches for debugging the JVM itself, as well as Java applications running on it.

Build Configuration for Debugging

Before debugging, configure your build for debugging:

Debug Builds

# Most debugging information, slowest runtime
bash configure --with-debug-level=slowdebug
make images
LevelAssertionsOptimizationDebug SymbolsUse Case
slowdebugYesNone (-O0)FullDeep debugging
fastdebugYesSome (-O2)FullDaily development
releaseNoFull (-O3)OptionalPerformance testing
For most development work, fastdebug provides the best balance. Use slowdebug only when you need to debug optimized-away variables or complex issues.

Java-Level Debugging

Using JDB (Java Debugger)

JDB is the command-line debugger included with the JDK:
# Start application in debug mode
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 MyApp

# In another terminal, attach jdb
jdb -attach 5005
1

Set Breakpoints

# Break at specific method
stop in java.lang.String.hashCode

# Break at line number
stop at MyClass:42

# Break on exception
catch java.lang.NullPointerException
2

Control Execution

run                 # Start/resume execution
step                # Step into
next                # Step over
step up             # Step out
cont                # Continue to next breakpoint
3

Inspect State

print myVariable    # Print variable value
dump myObject       # Dump object state
locals              # List local variables
where               # Print stack trace
threads             # List all threads

IDE Debugging

Most IDEs provide superior debugging experiences:
<!-- Configure Remote Debug Configuration -->
Host: localhost
Port: 5005
Command line args for remote JVM:
  -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

<!-- Import OpenJDK project -->
File → New → Project from Existing Sources
Select jdk/ directory
Import as Maven/Gradle project

Useful JVM Debug Flags

# Print loaded classes
java -verbose:class MyApp

# Print JIT compilation
java -XX:+PrintCompilation MyApp

# Trace method calls
java -XX:+TraceClassLoading -XX:+TraceClassUnloading MyApp

# GC debugging
java -Xlog:gc*=debug:file=gc.log MyApp

# Print all JVM flags
java -XX:+PrintFlagsFinal -version

Native Code Debugging

Using GDB (Linux/macOS)

GDB is the primary debugger for native HotSpot code:
1

Start GDB

# Debug the JVM
gdb --args build/linux-x64-server-fastdebug/jdk/bin/java MyApp

# Or attach to running process
gdb -p <pid>

# With core dump
gdb build/linux-x64/jdk/bin/java core.12345
2

Set Breakpoints

# Break at C++ function
break JavaThread::run

# Break at source location
break thread.cpp:1234

# Conditional breakpoint
break JavaThread::run if thread_id == 5

# Break on memory access
watch my_variable
3

Navigate Code

run                 # Start program
continue            # Continue execution
next                # Step over (same level)
step                # Step into
finish              # Step out of current function
until <line>        # Continue until line
4

Inspect State

# Print variables
print thread->_name
print /x 0x7fff12345678        # Print as hex
print *this                     # Print current object

# Print formatted
p/x variable      # Hexadecimal
p/d variable      # Decimal
p/t variable      # Binary
p/c variable      # Character

# Stack traces
backtrace         # Full stack
bt 10             # Top 10 frames
frame 3           # Switch to frame 3
info locals       # Local variables
info args         # Function arguments

GDB Helper Scripts

OpenJDK includes GDB helper scripts:
# Load HotSpot GDB helpers
gdb -x src/hotspot/share/gc/z/zDebug.gdb

# Or in GDB prompt
(gdb) source src/hotspot/share/gc/z/zDebug.gdb

# Custom helper for thread inspection
(gdb) define print-java-stack
  call JavaThread::print_stack_on($arg0)
end

# Use it
(gdb) print-java-stack thread

LLDB (macOS)

LLDB is the debugger for macOS:
# Start LLDB
lldb -- build/macosx-x64-server-fastdebug/jdk/bin/java MyApp

# Common commands
(lldb) breakpoint set --name JavaThread::run
(lldb) breakpoint set --file thread.cpp --line 1234
(lldb) run
(lldb) continue
(lldb) next
(lldb) step
(lldb) frame variable
(lldb) bt
GDBLLDBDescription
breakbreakpoint setSet breakpoint
runprocess launchStart program
continuecontinueResume execution
backtracethread backtraceStack trace
printframe variablePrint variables
info localsframe variableLocal variables

Visual Studio Debugger (Windows)

# Start JVM
start build\windows-x64-server-fastdebug\jdk\bin\java.exe MyApp

# In Visual Studio:
# Debug → Attach to Process → java.exe
# Set breakpoints in C++ source files

Advanced Debugging Techniques

Debugging JIT Compiled Code

# Disable JIT compilation
java -Xint MyApp

# Compile only specific methods
java -XX:CompileCommand=compileonly,MyClass::myMethod MyApp

# Print compilation details
java -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining MyApp

# Print generated assembly
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly MyApp

# Disable background compilation
java -Xbatch MyApp

Debugging Garbage Collection

# Detailed GC logging
java -Xlog:gc*=debug:file=gc.log:time,level,tags MyApp

# Verify heap consistency
java -XX:+VerifyBeforeGC -XX:+VerifyAfterGC MyApp

# Trace object allocation
java -XX:+TraceObjectAllocation MyApp

# GC stress testing
java -XX:+UnlockDiagnosticVMOptions -XX:+FullGCAtParallelMark MyApp
[0.123s][info][gc] GC(0) Pause Young (Normal) 12M->3M(128M) 2.345ms

Breaking down the log:
- [0.123s] - Time since JVM start
- [info][gc] - Log level and tag
- GC(0) - GC event number
- Pause Young - Type of collection
- 12M->3M(128M) - Before size -> After size (Total heap)
- 2.345ms - Pause time

Memory Leak Detection

1

Enable Native Memory Tracking

java -XX:NativeMemoryTracking=detail MyApp

# Check memory usage
jcmd <pid> VM.native_memory summary
jcmd <pid> VM.native_memory detail

# Create baseline
jcmd <pid> VM.native_memory baseline

# Check diff
jcmd <pid> VM.native_memory summary.diff
2

Heap Dumps

# Generate heap dump
jcmd <pid> GC.heap_dump /tmp/heap.hprof

# Or on OutOfMemoryError
java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/tmp/heap.hprof MyApp

# Analyze with jhat
jhat /tmp/heap.hprof
# Browse to http://localhost:7000
3

Valgrind (Linux)

# Check for memory leaks in native code
valgrind --leak-check=full \
         --show-leak-kinds=all \
         --track-origins=yes \
         build/linux-x64/jdk/bin/java MyApp

Crash Dump Analysis

When the JVM crashes, it generates a crash dump (hs_err_pidXXXX.log):
# Crash dump sections:
# 1. Summary - Exception type, address, thread
# 2. Thread state - Registers, stack
# 3. Native frames - C/C++ stack trace
# 4. Java frames - Java call stack (if available)
# 5. VM state - Heap info, GC state, etc.
# 6. System info - OS, CPU, memory

# Key things to check:
# - Problematic frame (near crash location)
# - Signal type (SIGSEGV, SIGBUS, etc.)
# - Thread state at crash
# - Recent VM operations
# Analyze core dump with debugger
gdb build/linux-x64/jdk/bin/java core.12345

# Get stack trace
(gdb) thread apply all bt

# Check specific frames
(gdb) frame 5
(gdb) info locals

# Print Java stack if possible
(gdb) call JavaThread::print_stack_on(thread)

Debugging Tools

jcmd - JVM Diagnostic Command

# List available commands
jcmd <pid> help

# Thread dump
jcmd <pid> Thread.print

# Class histogram
jcmd <pid> GC.class_histogram

# VM flags
jcmd <pid> VM.flags
jcmd <pid> VM.system_properties

# Compiler statistics
jcmd <pid> Compiler.codecache
jcmd <pid> Compiler.codelist

# Flight Recorder
jcmd <pid> JFR.start duration=60s filename=/tmp/recording.jfr
jcmd <pid> JFR.dump filename=/tmp/snapshot.jfr
jcmd <pid> JFR.stop

jstack - Thread Dump

# Get thread dump
jstack <pid>

# With locks
jstack -l <pid>

# Force dump (if hung)
jstack -F <pid>

# Analyzing deadlocks
jstack <pid> | grep -A 10 "deadlock"

jmap - Memory Map

# Heap summary
jmap -heap <pid>

# Heap histogram
jmap -histo <pid>
jmap -histo:live <pid>  # Only live objects

# Dump heap
jmap -dump:format=b,file=/tmp/heap.bin <pid>

Java Flight Recorder (JFR)

# Start with JVM
java -XX:StartFlightRecording=duration=60s,filename=/tmp/recording.jfr MyApp

# Or during runtime
jcmd <pid> JFR.start name=MyRecording duration=60s
jcmd <pid> JFR.dump name=MyRecording filename=/tmp/recording.jfr
jcmd <pid> JFR.stop name=MyRecording

Logging and Diagnostics

Unified Logging Framework

The -Xlog option provides extensive diagnostic information:
# Basic logging syntax
java -Xlog:<tags>*=<level>:<output>:<decorators>

# Examples:
# All GC logs to file
java -Xlog:gc*=debug:file=gc.log MyApp

# Class loading
java -Xlog:class+load=info:stdout MyApp

# Method compilation
java -Xlog:jit+compilation=debug:file=jit.log MyApp

# Everything (very verbose!)
java -Xlog:all=debug:file=all.log MyApp

# Multiple outputs
java -Xlog:gc=debug:stdout -Xlog:gc=trace:file=gc.log MyApp
  • gc - Garbage collection
  • class - Class loading/unloading
  • jit, compilation - JIT compilation
  • thread - Thread operations
  • os - Operating system interactions
  • vmop - VM operations
  • safepoint - Safepoint operations
  • exceptions - Exception handling

Assertion and Debugging Checks

// In HotSpot C++ code:

// Runtime checks (always enabled in debug builds)
assert(ptr != nullptr, "pointer must not be null");
guarantee(heap->is_initialized(), "heap not initialized");

// Should never reach
ShouldNotReachHere();
Unimplemented();

// Development-only code
#ifdef ASSERT
  verify_object_state();
#endif

// Trace execution
log_debug(gc)() << "Performing GC, heap size: " << heap_size;

Debugging Specific Scenarios

Debugging Deadlocks

# Get thread dump with lock info
jstack -l <pid> > threads.txt

# Look for "Found one Java-level deadlock"
grep -A 20 "deadlock" threads.txt

# In GDB, examine lock holders
(gdb) call Thread::print_all_threads()

Debugging Performance Issues

1

Profile with JFR

java -XX:StartFlightRecording=settings=profile,duration=30s,filename=profile.jfr MyApp
# Analyze in JMC to find hotspots
2

Check for Compilation Issues

java -XX:+PrintCompilation \
     -XX:+UnlockDiagnosticVMOptions \
     -XX:+LogCompilation \
     -XX:LogFile=compilation.log MyApp
3

Analyze GC Overhead

java -Xlog:gc*=debug:file=gc.log:time,uptimemillis,level MyApp
# Check pause times and frequency

Debugging Native Crashes

# 1. Reproduce with debug build
bash configure --with-debug-level=slowdebug
make images

# 2. Run under debugger
gdb --args build/linux-x64-slowdebug/jdk/bin/java MyApp
(gdb) run
# When crash occurs, examine state
(gdb) bt
(gdb) info registers
(gdb) frame 0
(gdb) info locals

# 3. Check for common issues
# - Null pointer dereference
# - Buffer overflow
# - Use after free
# - Stack overflow

Best Practices

1

Use Appropriate Build

  • Development: fastdebug
  • Deep debugging: slowdebug
  • Performance testing: release
2

Enable Assertions

Always run tests and development with assertions enabled (-ea).
3

Start Simple

Begin with high-level tools (jcmd, jstack) before diving into debuggers.
4

Isolate the Problem

Create minimal test cases that reproduce the issue reliably.
5

Use Logging Liberally

Add diagnostic logging before resorting to interactive debugging.

Resources

Documentation

Tools

  • jcmd - Diagnostic command tool (included with JDK)
  • jstack - Thread dump tool
  • jmap - Heap analysis tool
  • JFR/JMC - Profiling and monitoring
  • GDB/LLDB - Native debuggers
  • Valgrind - Memory debugging (Linux)
  • VisualVM - All-in-one monitoring tool
Effective debugging often requires patience and methodical investigation. Start with the simplest tools and escalate to more complex debugging techniques only when necessary.

Build docs developers (and LLMs) love