Lifecycle of a Java Program — Find the Bug¶
Practice finding and fixing bugs in Java code related to the program lifecycle. Each exercise contains buggy code — your job is to find the bug, explain why it happens, and fix it.
How to Use¶
- Read the buggy code carefully
- Try to find the bug without looking at the hint
- Write the fix yourself before checking the solution
- Understand why the bug happens — not just how to fix it
Difficulty Levels¶
| Level | Description |
|---|---|
| 🟢 | Easy — Common beginner mistakes, wrong commands, missing methods |
| 🟡 | Medium — Static initialization issues, classloading subtleties, lifecycle ordering |
| 🔴 | Hard — ClassLoader leaks, JIT deoptimization traps, shutdown hook races |
Bug 1: Wrong Class Name in Command 🟢¶
What the code should do: Print "Hello, World!" when run.
// File: HelloWorld.java
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Expected output:
Actual output:
Hint
Look at the file name and the class name — do they match?Bug Explanation
**Bug:** The file is named `HelloWorld.java` but the public class is named `Main`. In Java, the public class name must match the file name. **Why it happens:** Java requires this convention so the ClassLoader can find classes by name. **Impact:** Compilation fails — `javac` refuses to compile.Fixed Code
**What changed:** Renamed file from `HelloWorld.java` to `Main.java` to match the public class name.Bug 2: Running with .class Extension 🟢¶
What the code should do: Print "Program running" when executed.
// File: Main.java
public class Main {
public static void main(String[] args) {
System.out.println("Program running");
}
}
Expected output:
Actual output:
Error: Could not find or load main class Main.class
Caused by: java.lang.ClassNotFoundException: Main.class
Hint
The `java` command expects a class name, not a file name.Bug Explanation
**Bug:** Running `java Main.class` tells the JVM to look for a class named `Main.class` (with a literal dot and "class" as part of the name). The JVM then looks for `Main/class.class`, which does not exist. **Why it happens:** The `java` command takes a fully qualified class name, not a file path. The JVM appends `.class` internally. **Impact:** `ClassNotFoundException` — program does not run.Fixed Code
**What changed:** Removed `.class` from the `java` command.Bug 3: Missing main Method Signature 🟢¶
What the code should do: Print "Application started".
public class Main {
public void main(String[] args) {
System.out.println("Application started");
}
}
Expected output:
Actual output:
Error: Main method is not static in class Main, please define the main method as:
public static void main(String[] args)
Hint
The JVM requires a very specific signature for the entry point method.Bug Explanation
**Bug:** The `main` method is missing the `static` keyword. The JVM requires `public static void main(String[] args)` exactly. **Why it happens:** The JVM needs to call `main` without creating an instance of the class. Only `static` methods can be called without an object. **Impact:** JVM reports the error and does not execute the program.Fixed Code
**What changed:** Added `static` to the `main` method declaration.Bug 4: Static Initialization Order Bug 🟡¶
What the code should do: Print "Max connections: 200" (MULTIPLIER * BASE).
public class Main {
static final int MAX_CONNECTIONS = MULTIPLIER * BASE;
static final int BASE = 100;
static final int MULTIPLIER = 2;
public static void main(String[] args) {
System.out.println("Max connections: " + MAX_CONNECTIONS);
}
}
Expected output:
Actual output:
Hint
Static fields are initialized in the order they appear in the source code. What are the values of `MULTIPLIER` and `BASE` when `MAX_CONNECTIONS` is computed?Bug Explanation
**Bug:** `MAX_CONNECTIONS` is computed before `BASE` and `MULTIPLIER` are initialized. At that point, both have their default `int` value of `0`, so `MAX_CONNECTIONS = 0 * 0 = 0`. **Why it happens:** JLS 12.4.2 — static fields are initialized in textual order. `static final` fields with compile-time constant values (literals) are exceptions, but here `MAX_CONNECTIONS` depends on runtime computation. **Impact:** Wrong configuration value — could cause application to reject all connections.Fixed Code
public class Main {
static final int BASE = 100; // Initialize first
static final int MULTIPLIER = 2; // Initialize second
static final int MAX_CONNECTIONS = MULTIPLIER * BASE; // Now = 2 * 100 = 200
public static void main(String[] args) {
System.out.println("Max connections: " + MAX_CONNECTIONS);
}
}
Bug 5: Static Initializer Kills the Class 🟡¶
What the code should do: Load configuration from environment variable with a fallback default.
public class Main {
static final int PORT;
static {
PORT = Integer.parseInt(System.getenv("APP_PORT"));
}
public static void main(String[] args) {
System.out.println("Server starting on port: " + PORT);
}
}
Expected output (when APP_PORT is not set):
Actual output:
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.NumberFormatException: null
Hint
What happens when `System.getenv("APP_PORT")` returns `null`? And what happens to the class after the static initializer fails?Bug Explanation
**Bug:** When `APP_PORT` is not set, `System.getenv()` returns `null`, and `Integer.parseInt(null)` throws `NumberFormatException`. This is wrapped in `ExceptionInInitializerError`, and the class is permanently marked as failed. **Why it happens:** Static initializers run during class loading. Any exception permanently poisons the class. **Impact:** The class cannot be used — any future reference throws `NoClassDefFoundError`.Fixed Code
public class Main {
static final int PORT;
static {
String portStr = System.getenv("APP_PORT");
if (portStr != null && !portStr.isEmpty()) {
PORT = Integer.parseInt(portStr);
} else {
PORT = 8080; // Fallback default
}
}
public static void main(String[] args) {
System.out.println("Server starting on port: " + PORT);
}
}
Bug 6: Stale Class Files 🟡¶
What the code should do: Print the updated message after code change.
// Version 1: Main.java
public class Main {
public static void main(String[] args) {
System.out.println("Version 1");
}
}
Developer compiles and runs:
Then developer changes the code:
// Version 2: Main.java (updated)
public class Main {
public static void main(String[] args) {
System.out.println("Version 2 — with new features!");
}
}
Developer runs again WITHOUT recompiling:
Expected output:
Actual output:
Hint
The JVM runs `.class` files, not `.java` files. Did the developer rebuild?Bug Explanation
**Bug:** The developer edited `Main.java` but forgot to run `javac` again. The JVM still uses the old `Main.class`. **Why it happens:** `.class` files are not automatically regenerated when `.java` files change. You must recompile explicitly (or use a build tool like Maven/Gradle that handles this). **Impact:** Running an outdated version of the code — can cause confusion and "it works on my machine" issues.Fixed Code
**What changed:** Added the `javac` recompilation step before running.Bug 7: Version Mismatch Between Compile and Runtime 🟡¶
What the code should do: Run a program compiled with Java 21 on a Java 17 runtime.
// Compiled with JDK 21
public class Main {
public static void main(String[] args) {
System.out.println("Running on Java " + Runtime.version());
}
}
Expected output:
Actual output:
Error: LinkageError: class Main has been compiled by a more recent version of the Java Runtime
(class file version 65.0), this version of the Java Runtime only recognizes class file versions up to 61.0
Hint
Each Java version has a class file version number. What is the version for Java 17 and Java 21?Bug Explanation
**Bug:** The `.class` file compiled with JDK 21 has class file version 65. JDK 17 only supports up to version 61. The JVM refuses to load the class. **Why it happens:** Java maintains backward compatibility (new JVM runs old classes) but NOT forward compatibility (old JVM cannot run new classes). **Impact:** `UnsupportedClassVersionError` — application cannot start.Fixed Code
**What changed:** Either compile with `--release 17` to target the older runtime, or upgrade the runtime to match the compiler version.Bug 8: Shutdown Hook Race Condition 🔴¶
What the code should do: Save the counter value to a file during shutdown.
import java.io.FileWriter;
import java.io.IOException;
public class Main {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// Bug: counter may not have the final value
try (FileWriter fw = new FileWriter("counter.txt")) {
fw.write("Final count: " + counter);
System.out.println("Saved counter: " + counter);
} catch (IOException e) {
System.err.println("Failed to save: " + e.getMessage());
}
}));
for (int i = 0; i < 100; i++) {
counter++;
System.out.println("Count: " + counter);
Thread.sleep(100);
}
}
}
Expected output (when Ctrl+C pressed at count 50):
Actual output:
Hint
The shutdown hook runs in a separate thread. Is `counter` safely visible across threads? What about the race between the main loop and the hook?Bug Explanation
**Bug:** Two issues: 1. **Visibility:** `counter` is not `volatile`, so the shutdown hook thread may see a stale value from its CPU cache. 2. **Atomicity:** The main thread may be in the middle of `counter++` when the hook reads it. **Why it happens:** The JMM (Java Memory Model) does not guarantee visibility of non-volatile writes across threads without synchronization. **Impact:** The saved counter value may be stale or inconsistent.Fixed Code
import java.io.FileWriter;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
static final AtomicInteger counter = new AtomicInteger(0);
static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
running = false;
// Small delay to let main thread finish current iteration
try { Thread.sleep(50); } catch (InterruptedException e) { }
int finalCount = counter.get(); // Atomic read
try (FileWriter fw = new FileWriter("counter.txt")) {
fw.write("Final count: " + finalCount);
System.out.println("Saved counter: " + finalCount);
} catch (IOException e) {
System.err.println("Failed to save: " + e.getMessage());
}
}));
while (running && counter.get() < 100) {
counter.incrementAndGet(); // Atomic increment
System.out.println("Count: " + counter.get());
Thread.sleep(100);
}
}
}
Bug 9: ClassLoader Leak via ThreadLocal 🔴¶
What the code should do: Load a plugin class, use it, then unload it (ClassLoader should be GC'd).
import java.lang.ref.WeakReference;
public class Main {
static ThreadLocal<Object> cache = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
WeakReference<ClassLoader> clRef = loadAndUsePlugin();
// Try to GC the ClassLoader
System.gc();
Thread.sleep(100);
if (clRef.get() == null) {
System.out.println("ClassLoader was garbage collected (no leak)");
} else {
System.out.println("ClassLoader LEAKED! Still in memory.");
}
}
static WeakReference<ClassLoader> loadAndUsePlugin() throws Exception {
// Simulate a custom ClassLoader
ClassLoader pluginCL = new ClassLoader(Main.class.getClassLoader()) {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name); // Delegate to parent
}
};
Class<?> stringClass = pluginCL.loadClass("java.lang.String");
Object instance = "plugin-data";
// Bug: Store object loaded by plugin's classloader in ThreadLocal
cache.set(instance);
return new WeakReference<>(pluginCL);
}
}
Expected output:
Actual output:
Hint
`ThreadLocal` stores values in a map attached to the current thread. If the value references an object loaded by the plugin ClassLoader, the ClassLoader cannot be GC'd.Bug Explanation
**Bug:** The `ThreadLocal` holds a reference to an object that (in a real scenario) would be loaded by the plugin ClassLoader. The ThreadLocal's internal map is attached to the thread, which is a GC root. This prevents the plugin ClassLoader from being garbage collected. **Why it happens:** `ThreadLocal` values are stored in `Thread.threadLocals` (a `ThreadLocalMap`). As long as the thread is alive and the ThreadLocal is reachable, the value is retained. **Impact:** `OutOfMemoryError: Metaspace` after repeated plugin load/unload cycles. **How to detect:** Heap dump analysis with Eclipse MAT → find GC root path for the ClassLoader.Fixed Code
import java.lang.ref.WeakReference;
public class Main {
static ThreadLocal<Object> cache = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
WeakReference<ClassLoader> clRef = loadAndUsePlugin();
// Clean up ThreadLocal before expecting GC
cache.remove(); // ← Critical fix: remove ThreadLocal value
System.gc();
Thread.sleep(100);
if (clRef.get() == null) {
System.out.println("ClassLoader was garbage collected (no leak)");
} else {
System.out.println("ClassLoader LEAKED! Still in memory.");
}
}
static WeakReference<ClassLoader> loadAndUsePlugin() throws Exception {
ClassLoader pluginCL = new ClassLoader(Main.class.getClassLoader()) {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
}
};
Class<?> stringClass = pluginCL.loadClass("java.lang.String");
Object instance = "plugin-data";
cache.set(instance);
return new WeakReference<>(pluginCL);
}
}
Bug 10: System.exit() Bypasses Finally Block 🔴¶
What the code should do: Always close the resource, even when exiting early.
import java.io.FileWriter;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
FileWriter writer = null;
try {
writer = new FileWriter("output.txt");
writer.write("Processing data...\n");
// Simulate a fatal error condition
boolean fatalError = true;
if (fatalError) {
System.out.println("Fatal error detected. Exiting.");
System.exit(1); // Bug: finally block NEVER runs!
}
writer.write("Processing complete.\n");
} catch (IOException e) {
System.err.println("IO Error: " + e.getMessage());
} finally {
System.out.println("Finally: closing writer...");
try {
if (writer != null) {
writer.flush();
writer.close();
}
} catch (IOException e) {
System.err.println("Error closing writer: " + e.getMessage());
}
}
}
}
Expected output:
Actual output:
The finally block never executes, and the file may have unflushed data.
Hint
`System.exit()` initiates JVM shutdown immediately. It does NOT unwind the call stack or execute `finally` blocks. It does run shutdown hooks, though.Bug Explanation
**Bug:** `System.exit(1)` terminates the JVM immediately without executing `finally` blocks. The file writer is never flushed or closed, potentially losing data. **Why it happens:** `System.exit()` calls `Runtime.getRuntime().exit()`, which initiates the shutdown sequence (runs hooks, runs finalizers) but does NOT return to the calling method. The `finally` block is on the call stack, which is never unwound. **Impact:** Resource leak, data loss (unflushed buffers). **JLS reference:** JLS 14.20.2 — `try-finally` only executes if control reaches it normally or via exception. `System.exit()` bypasses this.Fixed Code
import java.io.FileWriter;
import java.io.IOException;
public class Main {
static FileWriter writer;
public static void main(String[] args) {
// Register shutdown hook for cleanup
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Shutdown hook: closing writer...");
try {
if (writer != null) {
writer.flush();
writer.close();
}
} catch (IOException e) {
System.err.println("Error closing writer: " + e.getMessage());
}
}));
try {
writer = new FileWriter("output.txt");
writer.write("Processing data...\n");
boolean fatalError = true;
if (fatalError) {
System.out.println("Fatal error detected. Exiting.");
System.exit(1); // Now the shutdown hook handles cleanup
}
writer.write("Processing complete.\n");
} catch (IOException e) {
System.err.println("IO Error: " + e.getMessage());
}
}
}
Bug 11: Circular Static Dependency Deadlock 🔴¶
What the code should do: Initialize two classes that reference each other.
// File: A.java
public class A {
static final int VALUE;
static {
System.out.println("A: initializing...");
VALUE = B.VALUE + 1; // Triggers B initialization
System.out.println("A: VALUE = " + VALUE);
}
}
// File: B.java
public class B {
static final int VALUE;
static {
System.out.println("B: initializing...");
VALUE = A.VALUE + 1; // Triggers A initialization
System.out.println("B: VALUE = " + VALUE);
}
}
// File: Main.java
public class Main {
public static void main(String[] args) {
System.out.println("A.VALUE = " + A.VALUE);
System.out.println("B.VALUE = " + B.VALUE);
}
}
Expected output:
Actual output:
Wait — the output looks correct? No! B.VALUE is 1 because when B reads A.VALUE, class A is still being initialized (the JVM detects the circular initialization and returns the default value 0 for A.VALUE). So B.VALUE = 0 + 1 = 1, then A.VALUE = 1 + 1 = 2.
If you expected both to be 2, the circular dependency silently produces wrong results.
Hint
When class A triggers B's initialization, and B tries to read A.VALUE, what value does it see? Class A is not finished initializing yet.Bug Explanation
**Bug:** Circular static dependency. When A is initializing and triggers B, B reads `A.VALUE` which is still `0` (default int value, because A hasn't finished its `Fixed Code
// Break the circular dependency
public class Constants {
static final int BASE_VALUE = 1;
}
public class A {
static final int VALUE = Constants.BASE_VALUE + 1; // 2
}
public class B {
static final int VALUE = Constants.BASE_VALUE + 1; // 2
}
public class Main {
public static void main(String[] args) {
System.out.println("A.VALUE = " + A.VALUE); // 2
System.out.println("B.VALUE = " + B.VALUE); // 2
}
}
Score Card¶
Track your progress:
| Bug | Difficulty | Found without hint? | Understood why? | Fixed correctly? |
|---|---|---|---|---|
| 1 | 🟢 | ☐ | ☐ | ☐ |
| 2 | 🟢 | ☐ | ☐ | ☐ |
| 3 | 🟢 | ☐ | ☐ | ☐ |
| 4 | 🟡 | ☐ | ☐ | ☐ |
| 5 | 🟡 | ☐ | ☐ | ☐ |
| 6 | 🟡 | ☐ | ☐ | ☐ |
| 7 | 🟡 | ☐ | ☐ | ☐ |
| 8 | 🔴 | ☐ | ☐ | ☐ |
| 9 | 🔴 | ☐ | ☐ | ☐ |
| 10 | 🔴 | ☐ | ☐ | ☐ |
| 11 | 🔴 | ☐ | ☐ | ☐ |
Rating:¶
- 11/11 without hints → Senior-level Java debugging skills
- 8-10/11 → Solid middle-level understanding
- 5-7/11 → Good junior, keep practicing
- < 5/11 → Review the lifecycle fundamentals first