Garbage Collection in Java – How It Works, finalize(), and Best Practices

Illustration for Garbage Collection in Java – How It Works, finalize(), and Best Practices
By Last updated:

Introduction

Garbage Collection (GC) in Java is an automatic memory management mechanism that removes unused objects from the heap to free up memory. Instead of manually allocating and deallocating memory like in C/C++, Java’s JVM handles it automatically.

Why It Matters

  • Prevents memory leaks by cleaning unused objects.
  • Optimizes heap usage for better performance.
  • Eliminates the need for manual memory deallocation, reducing bugs.

When to Use

You don't explicitly "use" GC in Java—it's always running. However, you can optimize your code to work with the GC, tune its behavior, and understand how it impacts performance.


How Garbage Collection Works in Java

Java objects are created on the heap memory. When no live references point to an object, it becomes eligible for garbage collection.

Core Concepts

  1. Reachability Analysis: GC uses reference chains starting from GC Roots (local variables, static fields, active threads) to find reachable objects.
  2. Generational Heap:
    • Young Generation: Newly created objects; frequent collections.
    • Old Generation (Tenured): Long-lived objects.
    • Permanent/Metaspace: Class metadata.

Garbage Collector Types

Collector Use Case Strength Weakness
Serial GC Single-threaded apps Low overhead Pauses entire app
Parallel GC Multi-core servers High throughput Longer pauses
G1 GC Large heaps Predictable pause times Complex tuning
ZGC/Shenandoah Ultra-low latency Concurrent GC Higher CPU usage

The finalize() Method

finalize() is a special method that the GC calls before reclaiming an object's memory.

@Override
protected void finalize() throws Throwable {
    System.out.println("Object is being garbage collected");
}

Problems with finalize()

  • Unpredictable: No guarantee when or if it will run.
  • Performance overhead: Slows GC because finalizable objects are processed separately.
  • Deprecated in Java 9: Use java.lang.ref.Cleaner or try-with-resources instead.

Real-World Analogy

Think of GC as a housekeeping robot in a hotel:

  • Guests (objects) occupy rooms (memory).
  • When guests check out (references lost), the robot cleans the room.
  • Some guests forget belongings (finalize()), but waiting for them delays the cleaning process.

Common Mistakes & Anti-Patterns

  1. Calling System.gc() manually:

    • Forces GC, causing unnecessary pauses.
    • JVM treats it as a suggestion, not a command.
  2. Relying on finalize() for resource cleanup:

    • Use try-with-resources instead for deterministic cleanup.
  3. Holding unnecessary references:

    • Leads to memory leaks; nullify or limit scope.
  4. Creating too many short-lived objects:

    • Puts pressure on Young Generation; reuse objects where possible.

Performance & Memory Implications

  • GC pause times affect latency-sensitive applications (trading systems, real-time games).
  • Choosing the right GC (e.g., G1 for large heaps, ZGC for low latency) can drastically improve performance.

Tuning Tips

  • Adjust JVM options like -Xms, -Xmx to control heap size.
  • Use -XX:+UseG1GC for balanced pause times.
  • Monitor GC with tools like JVisualVM, GC logs.

Best Practices

  • Prefer short-lived objects for efficiency in Young Generation.
  • Use WeakReference/SoftReference for cache-like structures.
  • Avoid finalize(); use Cleaner or try-with-resources.
  • Profile memory usage regularly with tools.
  • Tune GC only after profiling—don’t guess.

Java Version Relevance

Version Change
Java 8 PermGen removed, Metaspace introduced
Java 9 finalize() deprecated
Java 11+ ZGC introduced
Java 12+ Shenandoah GC added

public class GCDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 1000000; i++) {
            new GCDemo();
        }
        System.gc(); // Suggest GC
    }

    @Override
    protected void finalize() {
        System.out.println("Object cleaned by GC");
    }
}

Conclusion & Key Takeaways

  • GC automates memory management but requires understanding for performance tuning.
  • finalize() is deprecated—use Cleaner or try-with-resources.
  • Monitor, profile, and tune GC only when needed.
  • Choosing the right GC algorithm is critical for large-scale or low-latency apps.

FAQ

  1. What triggers garbage collection in Java?
    Loss of all references to an object makes it eligible; JVM decides when to run GC.

  2. Is System.gc() guaranteed to run GC?
    No, it's only a request.

  3. What replaced finalize() in modern Java?
    java.lang.ref.Cleaner and try-with-resources.

  4. Can I disable GC?
    No, but you can tune it or use low-level memory management in special JVMs.

  5. How to detect memory leaks in Java?
    Use profilers like JVisualVM, YourKit, or Eclipse MAT.

  6. Does GC clean static variables?
    Only when the class is unloaded.

  7. What’s the difference between Soft and Weak References?
    Soft survives low memory, Weak is collected eagerly.

  8. Which GC is best for large heaps?
    G1, ZGC, or Shenandoah.

  9. Does finalize() always run before exit?
    No guarantee; app may terminate before GC.

  10. How does GC affect performance?
    Pause times can affect latency; proper tuning minimizes impact.