Command Palette

Search for a command to run...

PHASE 4Beginner ~30 min· topic 12 of 12

Topic 4.12

Object Lifetime, References and null

In one line

An object lives as long as some chain of references from the running program can reach it; after that the garbage collector may reclaim it, at a time you don't control. null means "no object", and using it as if it were one throws NullPointerException.

Think of it like this

Helium balloons at a fair. Each balloon is held by a string, and a child holds the string. As long as someone holds a string, the balloon stays. Let go of every string, and the balloon floats away and a cleaner eventually collects it. In Java, objects are the balloons, references are the strings, and the garbage collector is the cleaner. An empty hand holding no string is null.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Lifetime
The time from when an object is created until it can no longer be used and its memory can be reclaimed.
Garbage collector (GC)
The part of the JVM that finds objects nobody can reach any more and reuses their memory automatically.
GC root
A starting point the garbage collector trusts is alive: local variables in running methods, static fields, live threads.
Reachable
An object is reachable if you can get to it by following references from a GC root. Reachable objects are kept.
Eligible for GC
No longer reachable, so the garbage collector is allowed to reclaim it whenever it chooses.
`null`
A reference value that points to no object at all.
`NullPointerException` (NPE)
The exception thrown when code tries to use null as if it were an object.
Memory leak (in Java)
Objects that stay reachable, so they can't be collected, even though the program will never use them again.

Step by step

01Birth: new puts an object on the heap

Every object starts with new (or something that calls it, like a factory method or a literal such as "hi"). The reference you get back is the only way to reach it.

Objects can outlive the method that created them. A factory method creates an object in its own frame and returns the reference; the frame is popped, but the object lives on because the caller now holds a reference.

Main.javawhole filejava
static Dog adopt(String name) {
    Dog d = new Dog(name);   // d is a local: dies when adopt returns
    return d;                // but the Dog object survives: the caller holds it
}

02Life: reachable from a root

The GC starts from the roots and follows every reference, marking what it can reach. Everything marked stays. Everything else is garbage.

In the picture, main's local team reaches the Team, which reaches two players. The old Player that nothing points to any more is unreachable, and so is the cycle of two nodes pointing only at each other.

Life: reachable from a rootdiagram
Rendering diagram…

03Death: unreachable, then collected later

An object becomes unreachable when the last reference to it goes away: a variable is reassigned (d = new Dog("Max")), set to null, or goes out of scope when its method returns, or the object holding the reference becomes unreachable itself.

Being unreachable doesn't mean it's freed now. The GC runs when it decides to, usually when an allocation area fills up. System.gc() is only a hint and can be disabled (-XX:+DisableExplicitGC). Programs must never depend on when collection happens.

That's why Java has no destructors. To release non-memory resources (files, sockets, database connections) you close them explicitly with try-with-resources (Phase 8), not when the object dies. finalize() was deprecated in Java 9 and deprecated for removal in Java 18 (JEP 421).

04null: a reference to nothing

Dog d = null; means d exists but refers to no object. Fields and array elements of reference type start as null. Local variables don't start as anything: reading one before assigning is a compile error (variable d might not have been initialized).

Using null as an object fails at run time. These all throw NullPointerException: calling a method on it, reading or writing a field, getting an array's length or element, unboxing a null Integer into an int, throw null, and synchronized (null).

Some things are fine with null: printing it or concatenating it ("x" + null is "xnull"), comparing with ==, instanceof (always false), and passing it as an argument.

05Read a helpful NullPointerException

Since Java 14 (enabled by default in 15), the JVM analyses the bytecode that failed and explains which part was null: Cannot invoke "String.trim()" because the return value of "Main.find(String)" is null.

For local variables, the message shows their names only if the class was compiled with -g (debug info). Without it you see "<local1>". Field names, static names and method names always appear.

terminal
$ java Main
── expected output ──
Exception in thread "main" java.lang.NullPointerException: Cannot read field "name" because "<local1>.owner" is null
at Main.main(Main.java:6)

06Defend against null on purpose

Fail fast at the boundary: this.owner = Objects.requireNonNull(owner, "owner"); throws immediately, with a clear message, when the bad value arrives, instead of an NPE far away later.

Compare safely: "admin".equals(role) and Objects.equals(a, b) never throw. Use defaults: Objects.requireNonNullElse(name, "guest") (Java 9).

Don't create nulls: return List.of() instead of null for "no results", and Optional<T> for a single value that may be absent (Phase 11). Many teams also use @Nullable/@NonNull annotations with static analysis tools.

07Leaks: reachable but useless

A static map used as a cache is a GC root forever. If entries are added per request and never removed, memory grows until java.lang.OutOfMemoryError: Java heap space.

Other classics: event listeners registered and never unregistered, collections that hold sessions after logout, and ThreadLocal values in pooled threads. The fix is always to break the reference: bound the cache (with eviction), unregister listeners, call ThreadLocal.remove(). Heap dumps (jcmd <pid> GC.heap_dump) and tools like Eclipse MAT show which root keeps the objects alive (Phase 13).

Try it yourself

  1. 1

    Predict each NPE message

    In "Helpful NullPointerException messages", add a case int[] scores = null; scores[0] = 1; inside a try/catch. Predict the message before running (hint: it mentions storing to an int array and "<local1>" or similar).

  2. 2

    Turn a late failure into an early one

    Write a class Account whose constructor stores an owner string, and a method ownerInitial() returning owner.charAt(0). Create new Account(null) and call the method: where does it fail? Then add Objects.requireNonNull(owner, "owner") to the constructor and compare.

  3. 3

    See an OutOfMemoryError locally

    On your machine (not the browser), run a loop that adds 1 MB arrays to a static list forever, with a small heap. Watch it fail, then explain which root kept the arrays alive.

    terminal
    $ java -Xmx64m Leak.java
    ── expected output ──
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at Leak.main(Leak.java:7)

Code & diagrams

Helpful NullPointerException messages New tab

Static fields and methods are named in the message. Local variables need javac -g to appear by name.

Sign in to run this example in your browser.

Expected output

1: Cannot invoke "String.length()" because "Main.name" is null
2: Cannot invoke "String.trim()" because the return value of "Main.find(String)" is null
3: Cannot read field "name" because "Main.owner.pet" is null
4: name must not be null
What is and isn't safe with null New tab
Sign in to run this example in your browser.

Expected output

concat:     role=null
== null:    true
instanceof: false
literal first equals: false
Objects.equals:       true
default:    guest
empty list instead of null: 0
unboxing null throws NullPointerException
Objects outlive the method that made them New tab

This is also the shape of the classic leak: a static collection that is only ever added to.

Sign in to run this example in your browser.

Expected output

registry: [Rex, Bella]
first dog still reachable: Rex
registry after clear: []

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Call a method on a value that might be null

Write static String find(String key) { return null; } and then find("x").trim(); without a check.

terminal
$ java Main
── what you'll see ──
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.trim()" because the return value of "Main.find(String)" is null
at Main.main(Main.java:6)

Break #2

Use a local variable before giving it a value

Write Dog d; d.bark();.

terminal
$ javac Main.java
── what you'll see ──
Main.java:2: error: variable d might not have been initialized
public class Main { public static void main(String[] args) { Dog d; d.bark(); } }
^
1 error

Myth vs fact

Myth

Setting a variable to null frees the object immediately.

Fact

It only removes one reference. The object becomes eligible for collection if no other references remain, and the GC frees it whenever it decides.

Myth

System.gc() forces garbage collection.

Fact

It's a request the JVM may ignore (and -XX:+DisableExplicitGC turns it off). Correct programs never depend on it.

Myth

Java can't have memory leaks because it has a garbage collector.

Fact

The GC frees only unreachable objects. Anything still referenced, like entries in a static map nobody cleans, stays forever.

Myth

Objects that reference each other in a cycle are never collected.

Fact

That's a problem for reference counting. Java's tracing collectors start from roots, so an unreachable cycle is collected like any other garbage.

When it breaks

A static HashMap used as a cache grows on every request

What you see

Heap usage climbs steadily over days, GC runs more and more often, latency rises, and finally the service dies with java.lang.OutOfMemoryError: Java heap space. Restarting "fixes" it until it happens again.

Fix & prevent

Take a heap dump (jcmd <pid> GC.heap_dump file.hprof) and find the dominant map and its GC root. Replace it with a bounded cache with eviction and expiry (Caffeine, or LinkedHashMap with removeEldestEntry). Alert on old-generation usage after GC.

NullPointerException deep inside a service from a missing config value

What you see

Requests fail with a 500 and an NPE stack trace far from where the null entered (for example a missing environment variable read at startup and used much later).

Fix & prevent

Validate configuration at startup with Objects.requireNonNull and clear messages, so the process fails fast before taking traffic. Use helpful NPE messages (on by default since Java 15) to find the exact null expression.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Reachability has strengths: strong (normal references), soft (SoftReference, cleared under memory pressure), weak (WeakReference, cleared at the next GC once only weakly reachable; used by WeakHashMap), and phantom (PhantomReference, for post-mortem cleanup; what java.lang.ref.Cleaner, Java 9, uses as the replacement for finalize).

  • ▸

    The JIT can treat a local variable as dead after its last use, even if it's still in scope, so an object can become unreachable while its method is still running. That's why Reference.reachabilityFence(obj) (Java 9) exists for code that relies on an object staying alive (for example around native resources).

  • ▸

    Most objects die young (the generational hypothesis), so collectors like G1 (default since Java 9) and generational ZGC (Java 21) collect the young generation often and cheaply. Allocation-heavy code is usually fine; long-lived caches and leaks are what hurt. Phase 13 covers GC tuning.

  • ▸

    Helpful NPE messages are computed lazily (only when getMessage() is called) from the bytecode, so they cost nothing on the normal path. Turn them off with -XX:-ShowCodeDetailsInExceptionMessages if messages could reveal sensitive code details in logs.

Remember this

  1. 1

    An object is born with new and lives on the heap. It stays alive as long as it's reachable: there is a path of references to it starting from a GC root. Roots include local variables and parameters in active stack frames, static fields of loaded classes, live threads, and references held by native code.

  2. 2

    When no path from any root reaches an object, it's unreachable and becomes eligible for garbage collection. The JVM decides when to collect; it might be milliseconds later, much later, or never if the program ends first. You can't free an object yourself, and there are no destructors in Java.

  3. 3

    Java uses tracing collection, not reference counting. Objects that point to each other in a cycle (A points to B, B points to A) are still collected once nothing outside the cycle reaches them. Setting fields to null to "help the GC" is almost never needed; letting variables go out of scope does the job.

  4. 4

    null is a special reference value meaning "refers to no object". It's the default for reference fields and array elements. Calling a method, reading a field, indexing an array, or unboxing through null throws NullPointerException. Since Java 14 (default on since 15), the message says exactly which expression was null (JEP 358, helpful NullPointerExceptions).

  5. 5

    Handle null deliberately: validate inputs with Objects.requireNonNull(x, "x") so the failure happens early and with a clear message; use Objects.equals(a, b) and "literal".equals(x) for null-safe comparisons; return empty collections instead of null; and for "maybe a value" return types, use Optional (Phase 11).

  6. 6

    Java can still leak memory: a leak here is an object that's reachable but never used again, such as entries piling up in a static map used as a cache, listeners that are registered but never removed, or a ThreadLocal in a thread pool. The GC can't free what you still reference.

Explain it without notes

01

When does an object become eligible for garbage collection? What are GC roots?

02

Why doesn't Java have destructors, and how do you release resources like files?

03

List the operations that throw NullPointerException and three that are safe with null.

04

How can a garbage-collected Java program still leak memory? Give two examples and fixes.

Practice

01

Write a class Profile whose constructor requires a non-null email (with Objects.requireNonNull and a message) and allows a null nickname, with a method displayName() that returns the nickname or, if it's null, the part of the email before @. Show both cases and a rejected null email.

02

Write a method static int safeLength(String s) that returns 0 for null, and print it for null, "" and "java".

03

Create a tiny cycle: two Node objects whose next fields point at each other, print a.next.next == a, then set both local variables to null. Write in a comment whether the cycle can be collected and why.

Trade-offs

  • ↔

    Automatic garbage collection removes whole classes of bugs (double free, dangling pointers) at the cost of some CPU, memory headroom and, with some collectors, pauses. Modern collectors (G1, ZGC, Shenandoah) keep pauses short; you trade a little throughput for that.

  • ↔

    Returning null for "absent" is cheap and familiar but invites NPEs far from the source. Optional, empty collections and fail-fast checks cost a little ceremony and make absence explicit.

  • ↔

    Caching objects in long-lived structures speeds repeated work but extends their lifetime, possibly forever. Every cache needs a size limit or expiry, or it's a leak waiting to happen.

Done when you can

  • Done when you can explain reachability, GC roots and "eligible for collection".

  • Done when you know why Java has no destructors and how resources are released instead.

  • Done when you can read a helpful NPE message and find the null expression.

  • Done when you use requireNonNull, null-safe comparisons and empty results instead of returning null.

  • Done when you can describe two ways Java programs leak memory and how to fix them.