DEV Community

Cover image for Garbage Collection in Java: A Simple Explanation
Marta Kravchuk
Marta Kravchuk

Posted on • Edited on

Garbage Collection in Java: A Simple Explanation

Effective memory management is essential in Java development. Fortunately, Java automates this task through a process called garbage collection (GC), which helps prevent memory leaks. This article explains Java garbage collection in simple terms, covering its core principles, the System.gc() method for suggesting garbage collection, the deprecated finalize() method, and modern ways to manage resources.


Table of Contents

  1. What is Java Garbage Collection?
  2. Suggesting Garbage Collection with System.gc()
  3. Why the Deprecated finalize() Should Be Avoided
  4. Modern Resource Management Techniques

All Java objects are stored in a memory heap. The heap, also called free storage, is a large pool of unused memory that is specifically allocated for your Java program. Depending on the environment you're working in, the heap can be quite large, but its size is always limited. After all, there is no computer with infinite memory. So if you create many objects without cleaning them up, your program could run out of memory and crash.

1. What is Java Garbage Collection?

Garbage collection refers to the process where Java automatically deletes objects that are no longer needed to free up memory, operating silently in the background.

To understand how garbage collection works, it's important to know when an object becomes eligible for removal. In Java, an object is considered eligible for garbage collection when it's no longer accessible to the program and therefore able to be garbage collected. This happens in two primary cases:

  1. The object has no more references pointing to it, indicating that no part of the code is actively using it.
  2. All references to the object have gone out of scope, such as when the object was created inside a method that has finished executing.

2. Suggesting Garbage Collection with System.gc()

Java includes a built-in method to help support garbage collection that can be called at any time: System.gc().

public static void main(String[] args) {
   System.gc();
}
Enter fullscreen mode Exit fullscreen mode

Note: When you call System.gc(), you are simply suggesting to Java that it should perform garbage collection. It doesn’t guarantee that it will actually do this. The JVM may perform garbage collection at that moment, or it might be busy and choose not to. The JVM is free to ignore the request.

The following example demonstrates this more concretely, by creating Cat objects and manipulates references to them to demonstrate how garbage collection eligibility works in Java.

public class Cat { 
   public static void main(String[] args) { 
      Cat one = new Cat(); 
      Cat two = new Cat(); 
      Cat three = one; 
      one = null; 
      Cat four = one; 
      three = null; 
      two = null; 
      two = new Cat(); 
      System.gc(); 
   } 
}
Enter fullscreen mode Exit fullscreen mode

Initially, Cat one and Cat two are created, each referencing a new Cat object. Assigning Cat three = one makes both one and three point to the same Cat object. Setting one = null does not make that Cat object eligible for garbage collection, because the variable three still holds an active reference. Assigning Cat four = one assigns a null reference and does not affect garbage collection eligibility. It's only when both one and three are set to null that the first Cat object becomes eligible for garbage collection, because there are then no more active references pointing to it. Similarly, when two = null is executed, the second Cat object becomes eligible, since two was the only reference to that object. The assignment where two is a new Cat() makes the third object collected when method scope is closed. Finally, the line System.gc() suggests to the JVM that it might be a good time to run the garbage collector. However, it doesn't guarantee that garbage collection will actually occur. Even if garbage collection runs, it simply reclaims the memory.

That is, in short:

  • The first Cat object becomes eligible for garbage collection after three = null because no other variables reference it.
  • The second Cat object becomes eligible for garbage collection after two = null because no other variables reference it.
  • The third Cat object becomes eligible for garbage collection after the method ends.

The System.gc() is just a request, and the JVM may choose to ignore it.

3. Why the Deprecated finalize() Should Be Avoided

The finalize() method in Java was part of the Object class. This function can be confusing and difficult to use. In a few words, the garbage collector may call the finalize() method once — or not at all, depending on whether and when garbage collection occurs. If the garbage collector failed to collect an object and tried again later, there was no second call to finalize(). In fact, it was used as a finalisation mechanism to perform certain actions before the object was destroyed by the garbage collector. However, since Java 9, this method has been deprecated in Object, as reported in the official documentation:

The finalisation mechanism is inherently problematic.

It is important to know that finalize() could be executed zero or once. It could be executed twice.

How the finalize() method worked:

The finalize() method provided a last opportunity to clean up resources that the object hadn't released, such as closing open files or breaking database connections.

protected void finalize() throws Throwable {
   try {
      // Perform cleanup, like releasing resources
   } finally {
      super.finalize();
   }
}
Enter fullscreen mode Exit fullscreen mode

Problems with finalize() method:

  • Unreliability: the method was not always called immediately after the object became unnecessary, which led to delays in freeing resources.
  • Performance: this slowed down the garbage collector as it required additional work to be done.
  • Security: the method could allow unsafe code to be executed while cleaning up objects, which created security risks.

Alternatives:

Instead of finalize(), other resource management techniques are recommended, such as:

  • try-with-resources automatically closes resources after use, avoiding the need for garbage collection participation.
  • Cleaner and PhantomReference provide better control and flexibility for resource cleanup without the issues associated with finalize().

4. Modern Resource Management Techniques

Java includes the try-with-resources statement to automatically close all resources opened in a try clause. This feature, known as automatic resource management, ensures that resources are properly closed without requiring additional code in finally blocks. This is especially useful for managing files, databases, and network connections, where you need to ensure that the resource is closed regardless of the success of the operation or exceptions.

void readFile() {
   try (FileReader file = new FileReader("file.txt")) {
      ...
   } catch (IOException e) {
      System.out.println("An error: " + e.getMessage());
   }
}
Enter fullscreen mode Exit fullscreen mode

Note: Only a try-with-resources statement is permitted to omit both the catch and finally blocks. A traditional try statement must have either or both.

void readFile() throws IOException {
   try (FileReader file = new FileReader("file.txt")) {
        ...
   }
}
Enter fullscreen mode Exit fullscreen mode

When using a try-with-resources statement, you cannot use just any class. Instead, Java requires that the classes you use must implement the AutoCloseable interface. When you use a try-with-resources statement, the Java compiler automatically generates a hidden finally block behind the scenes. This implicit finally block is responsible for calling the close() method on all resources declared in the try-with-resources statement.

The Cleaner and PhantomReference mechanisms provide more flexible ways to perform cleanup actions for objects that are no longer reachable, offering alternatives to the deprecated finalize() method.

  • The Cleaner class was introduced in Java 9 as part of the java.lang.ref package. It provides a mechanism to register cleanup actions that will be executed when an object becomes unreachable.
import java.lang.ref.Cleaner;

class Resource {
    private static final Cleaner cleaner = Cleaner.create();

    private final Cleaner.Cleanable cleanable;

    public Resource() {
        cleanable = cleaner.register(this, new CleanupAction());
        System.out.println("Resource allocated.");
    }

    private static class CleanupAction implements Runnable {
        public void run() {
            System.out.println("Cleanup actions executed.");
        }
    }

// Other methods for the resource...
// If you need to explicitly clean up, you could provide a close method

    public void close() {
        cleanable.clean();
    }
}

public class CleanerExample {
    public static void main(String[] args) {
        Resource resource = new Resource();
// Use the resource. The cleanup action will be called when the resource becomes unreachable
        resource = null; // Make the resource eligible for garbage collection
        System.gc(); // Suggest garbage collection
    }
}
Enter fullscreen mode Exit fullscreen mode
  • PhantomReference provides a mechanism to detect when an object becomes unreachable and to perform cleanup actions before the memory is reclaimed — similar in purpose to finalize(), but more reliable. The key feature of PhantomReference is the use of a ReferenceQueue. When an object becomes unreachable, it is automatically added to this queue.
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

class PhantomReferenceExample {
    public static void main(String[] args) {
        ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
        MyObject obj = new MyObject();
        PhantomReference<MyObject> phantomRef = new PhantomReference<>(obj, queue);

        // Make the object eligible for garbage collection
        obj = null;
        System.gc(); // Suggest garbage collection

        // Check if phantom reference is added to the queue
        PhantomReference<MyObject> ref = (PhantomReference<MyObject>) queue.poll();
        if (ref != null) {
            System.out.println("Phantom reference has been cleared.");
        } else {
            System.out.println("Phantom reference not yet cleared.");
        }
    }
}

class MyObject {
    // Class implementation...
}
Enter fullscreen mode Exit fullscreen mode

Both approaches, Cleaner and PhantomReference, offer a more reliable and controlled way to manage resource cleanup compared to the finalize() method.

Conclusion

This article has provided an overview of how Java automatically frees up memory using the garbage collector. Objects become eligible for removal when they are no longer referenced or go out of scope. Local variables disappear once their block ends, instance fields become unreachable when the object itself is no longer accessible, and static variables live throughout the entire runtime of the program.
Understanding how garbage collector works is essential for performance optimization, as unintentional memory leaks can lead to OutOfMemoryErrors and degraded application performance. Instead of relying on the deprecated finalize() method, Java provides more efficient alternatives: try-with-resources, Cleaner, and PhantomReference — to ensure timely cleanup of resources.


Thanks for reading! Hope you found some useful info in this article. Got any questions or ideas? Drop them in the comments below. Stay tuned here for a new Java series every week! And feel free to connect with me on LinkedIn 😊

Top comments (0)