Senior 5 min · March 06, 2026

Java Inner Class Memory Leak — Runnable Kept Session Alive

Heap grows after peak? In MAT, OuterClass$1 instances have large retained size.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Four types: static nested, non-static inner, local, anonymous
  • Static nested: no outer instance reference, safe, use for Builders and Nodes
  • Non-static inner: hidden reference to outer instance, use for Iterators and Views
  • Local and anonymous: method-scoped, captured variables must be effectively final
  • Performance: static nested adds zero overhead; inner class adds one hidden reference field
  • Production pitfall: non-static inner passed to long-lived object keeps outer alive, causing memory leaks
Plain-English First

Imagine a car. The car has an engine, and that engine has a fuel injector inside it. The fuel injector only makes sense in the context of the engine — you'd never buy a fuel injector at a grocery store. That's exactly what a nested class is: a class that lives inside another class because it genuinely belongs there. It's not laziness; it's the right address for that piece of logic.

Every Java codebase beyond 'Hello World' eventually grows classes that are tightly coupled — a Node that only exists to serve a LinkedList, a Comparator that only ever sorts one type of object, a callback that fires exactly once in a UI event. When you shove these into separate top-level files, you scatter related logic across your project and expose internals that were never meant to be public. This is the gap nested and inner classes were designed to fill.

Java gives you four flavours of nested class: static nested classes, non-static inner classes, local classes, and anonymous classes. Each one solves a slightly different coupling problem. Choosing the wrong one — or reaching for a top-level class when a nested one is right — leads to either over-exposed APIs or unnecessarily tangled code. Understanding the four types isn't just trivia; it's the difference between a design that reads like a story and one that reads like a ransom note.

By the end of this article you'll know exactly which nested class type to reach for in a given situation, why each type has the access rules it does, how to avoid the memory-leak trap that catches most developers, and how to answer the interview questions that trip up even experienced Java developers.

Static Nested Classes — The Logical Grouping Tool

A static nested class is a class declared inside another class with the static keyword. The word 'static' here means exactly what it means on a static method: no implicit reference to an enclosing instance. The nested class is associated with the outer type, not with any particular outer object.

This makes static nested classes the safest and most common kind. You use them when a class conceptually belongs to another class but doesn't need to read or write the outer class's instance fields. Think of a Builder inside a HttpRequest, or a Node inside a LinkedList. Neither needs access to the outer instance — they just logically live there.

Because there's no hidden reference to an outer object, static nested class instances are lightweight and can be instantiated independently: new Outer.Nested(). They don't hold onto the outer object, which means no surprise memory leaks. When in doubt between static and non-static, always start with static and only drop the keyword when you genuinely need outer instance access.

HttpRequest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.thecodeforge.nestedclasses;

public class HttpRequest {

    private final String url;
    private final String method;
    private final int timeoutSeconds;

    // Private constructor forces callers to use the Builder
    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.timeoutSeconds = builder.timeoutSeconds;
    }

    public String getUrl()           { return url; }
    public String getMethod()        { return method; }
    public int    getTimeoutSeconds(){ return timeoutSeconds; }

    @Override
    public String toString() {
        return method + " " + url + " (timeout=" + timeoutSeconds + "s)";
    }

    // Static nested class — belongs to HttpRequest conceptually,
    // but needs NO access to any HttpRequest instance while building.
    public static class Builder {\n\n        private String url    = \"\";\n        private String method = \"GET\";     // sensible default\n        private int    timeoutSeconds = 30; // sensible default\n\n        public Builder url(String url) {\n            this.url = url;\n            return this;  // enables method chaining\n        }\n\n        public Builder method(String method) {\n            this.method = method;\n            return this;\n        }\n\n        public Builder timeoutSeconds(int seconds) {\n            this.timeoutSeconds = seconds;\n            return this;\n        }\n\n        // Creates the outer-class instance using 'this' Builder\n        public HttpRequest build() {\n            if (url.isBlank()) {\n                throw new IllegalStateException(\"URL must not be blank\");\n            }\n            return new HttpRequest(this); // passes itself to the private constructor\n        }\n    }\n\n    public static void main(String[] args) {\n        // No HttpRequest instance needed to create a Builder\n        HttpRequest request = new HttpRequest.Builder()\n                .url(\"https://api.thecodeforge.io/articles\")\n                .method(\"POST\")\n                .timeoutSeconds(10)\n                .build();\n\n        System.out.println(request);\n    }\n}",
        "output": "POST https://api.thecodeforge.io/articles (timeout=10s)"
      }

Non-Static Inner Classes — When You Genuinely Need the Outer Instance

Drop the static keyword and you get a non-static inner class, commonly called just an 'inner class'. The compiler silently adds a hidden field — this$0 — that holds a reference to the enclosing outer instance. Every inner class object is permanently tethered to one specific outer object.

This hidden reference is why inner classes can access all outer instance fields and methods directly, even private ones. It's also why you can only create an inner class object through an existing outer instance: outerInstance.new Inner().

The classic real-world use case is iterators. An ArrayList's iterator needs to read the list's private elementData array and track modCount to detect concurrent modification. It can't do that from a static context — it needs the live outer instance. So Java's own standard library uses a non-static inner class for ArrayList.Itr. You should reach for a non-static inner class when your nested type is inherently a view of or operation on a specific outer instance.

NumberRange.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package io.thecodeforge.nestedclasses;

import java.util.Iterator;
import java.util.NoSuchElementException;

// A simple inclusive integer range that can be iterated
public class NumberRange implements Iterable<Integer> {

    private final int start;
    private final int end;

    public NumberRange(int start, int end) {\n        if (start > end) {\n            throw new IllegalArgumentException(\"start must be <= end\");\n        }\n        this.start = start;\n        this.end   = end;\n    }\n\n    @Override\n    public Iterator<Integer> iterator() {\n        // Returns a new RangeIterator tied to THIS NumberRange instance\n        return new RangeIterator();\n    }\n\n    // Non-static inner class — it needs to read 'start' and 'end'\n    // from the enclosing NumberRange instance. Making this static\n    // would require passing start/end explicitly; inner class reads them for free.\n    private class RangeIterator implements Iterator<Integer> {\n\n        private int current = start; // directly reads outer instance field\n\n        @Override\n        public boolean hasNext() {\n            return current <= end;  // reads outer instance field 'end'\n        }\n\n        @Override\n        public Integer next() {\n            if (!hasNext()) {\n                throw new NoSuchElementException(\n                    \"No more values in range [\" + start + \", \" + end + \"]\"\n                );\n            }\n            return current++;\n        }\n    }\n\n    public static void main(String[] args) {\n        NumberRange range = new NumberRange(1, 5);\n\n        // Enhanced for-loop uses the iterator() method under the hood\n        for (int number : range) {\n            System.out.print(number + \" \");\n        }\n        System.out.println();\n\n        // Each call to iterator() creates a fresh, independent RangeIterator\n        Iterator<Integer> it1 = range.iterator();\n        Iterator<Integer> it2 = range.iterator();\n        System.out.println(\"it1 first: \" + it1.next()); // advances it1 only\n        System.out.println(\"it2 first: \" + it2.next()); // it2 still starts at 1\n    }\n}",
        "output": "1 2 3 4 5 \nit1 first: 1\nit2 first: 1"
      }

Local and Anonymous Classes — Inline Logic for One-Time Use

Local classes are declared inside a method body. They can access local variables from the enclosing method, but only if those variables are effectively final — meaning the compiler would accept the final keyword on them even if you didn't type it. Local classes are rare in modern Java because lambdas cover most of their use cases more concisely, but they shine when you need a multi-method implementation in a single place and only that place.

Anonymous classes are local classes without a name. You declare and instantiate them in one expression: new SomeInterface() { ... }. Before Java 8 lambdas, anonymous classes were everywhere — every Swing event listener, every Runnable passed to a Thread. They're still useful today when you need to implement an interface with multiple methods and the logic is short enough to be readable inline.

The critical rule for both: captured local variables must be effectively final. Change a captured variable after capturing it and the compiler will refuse to compile. This isn't a bug — it prevents a whole class of data-race conditions by making the contract explicit.

SortingDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package io.thecodeforge.nestedclasses;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class SortingDemo {

    public static void main(String[] args) {

        List<String> cities = Arrays.asList(
            "Tokyo", "Berlin", "São Paulo", "Lagos", "Melbourne"
        );

        // ── LOCAL CLASS EXAMPLE ──────────────────────────────────────────
        // Suppose we want a Comparator that sorts by string length first,
        // then alphabetically. This logic is only needed in this method.
        final boolean ascending = true; // effectively final — captured below

        class LengthThenAlphaComparator implements Comparator<String> {\n\n            @Override\n            public int compare(String a, String b) {\n                // Uses 'ascending' from the enclosing method scope\n                int lengthDiff = Integer.compare(a.length(), b.length());\n                if (lengthDiff != 0) {\n                    return ascending ? lengthDiff : -lengthDiff;\n                }
                return a.compareTo(b); // alphabetical tiebreak
            }
        }

        List<String> citiesCopy = new java.util.ArrayList<>(cities);
        citiesCopy.sort(new LengthThenAlphaComparator());
        System.out.println("Local class sort:     " + citiesCopy);

        // ── ANONYMOUS CLASS EXAMPLE ──────────────────────────────────────
        // Same comparator, but written inline as an anonymous class.
        // Useful when you won't reuse the name anywhere in this method.
        citiesCopy = new java.util.ArrayList<>(cities);
        citiesCopy.sort(new Comparator<String>() {
            @Override
            public int compare(String a, String b) {\n                // Sorts purely by length descending — longest city name first\n                return Integer.compare(b.length(), a.length());\n            }
        });
        System.out.println("Anonymous class sort: " + citiesCopy);

        // ── LAMBDA (for contrast) ────────────────────────────────────────
        // A lambda replaces a single-abstract-method anonymous class.
        // Clean, concise. Use lambdas over anonymous classes for SAM interfaces.
        citiesCopy = new java.util.ArrayList<>(cities);
        citiesCopy.sort((a, b) -> a.compareTo(b));
        System.out.println("Lambda sort:          " + citiesCopy);
    }
}
Output
Local class sort: [Lagos, Tokyo, Berlin, Melbourne, São Paulo]
Anonymous class sort: [São Paulo, Melbourne, Berlin, Tokyo, Lagos]
Lambda sort: [Berlin, Lagos, Melbourne, São Paulo, Tokyo]
Interview Gold: Anonymous Class vs Lambda
Lambdas replace anonymous classes ONLY for single-abstract-method (SAM) interfaces. If the interface has multiple abstract methods (like Comparator before Java 8 or a custom multi-method interface), you must use an anonymous class or a local/inner class. Also, this inside a lambda refers to the enclosing class; this inside an anonymous class refers to the anonymous class itself — interviewers love this distinction.
Production Insight
Local and anonymous classes are method-scoped, so they are short-lived.
But if you accidentally capture a large object (like a database connection) in an anonymous class passed to a thread pool, that object stays reachable until the task completes.
Always keep captured variables small and focused.
Key Takeaway
Local and anonymous classes capture effectively-final variables.
Use them for short-lived, method-specific logic.
Prefer lambdas for SAM interfaces; use anonymous classes only when you need multiple methods or override from a class.

Memory and Performance Implications: What Breaks in Production

Nested classes are not free. Each type has distinct memory and performance characteristics that matter in production. The biggest hidden cost is the non-static inner class's implicit this$0 reference. It adds one extra object reference per inner instance. That's small — but when you create millions of inner class instances (e.g., iterators in a high-throughput system), the retained heap and GC pressure add up.

Anonymous classes also generate new .class files at compile time. For each anonymous class, the compiler creates a new class file named Outer$1.class, Outer$2.class, etc. In large codebases, this can bloat the JAR size and classloading overhead. Lambdas avoid this because they use invokedynamic — no separate class file is generated.

Local classes are the least common but have similar overhead to anonymous classes. The hidden reference to the enclosing method's stack frame variables can prevent those variables from being garbage collected until the local class instance is collected.

The production rule: static nested classes are cost-free. Every other type introduces some overhead. Use them intentionally, not habitually.

Another subtle performance trap: when a non-static inner class accesses private outer fields, the compiler generates synthetic accessor methods (access$000, etc.) if the inner class is in a different compilation unit? Actually, inner classes have direct access to private fields because they are in the same top-level class. The JVM allows this via synthetic accessors only for nested classes that are in separate compilation units? No — inner classes are in the same .class file but separate inner class files. The compiler creates synthetic accessors for private field access from inner classes to outer classes. This adds a method call overhead. For hot code paths, this can be measurable. Static nested classes that access private outer static fields also require synthetic accessors.

MemoryCostExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package io.thecodeforge.nestedclasses;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.IntStream;

public class MemoryCostExample {

    private final int id;

    public MemoryCostExample(int id) {
        this.id = id;
    }

    // Non-static inner class — each instance holds a reference to outer
    public class InnerIterator implements Iterator<Integer> {
        private int current = 0;
        @Override
        public boolean hasNext() { return current < id; } // accesses outer id
        @Override
        public Integer next() { return current++; }
    }

    public Iterator<Integer> getInnerIterator() {
        return new InnerIterator();
    }

    public static void main(String[] args) throws InterruptedException {
        List<Iterator<Integer>> iterators = new ArrayList<>();

        // Simulate high-throughput creation of inner class instances
        IntStream.range(0, 1_000_000).forEach(i -> {\n            MemoryCostExample outer = new MemoryCostExample(i % 100);
            // Each InnerIterator holds a reference to its outer instance
            // Outer instances are not shared, so 1M outer + 1M inner instances
            iterators.add(outer.getInnerIterator());
        });

        System.out.println("Created " + iterators.size() + " inner class instances.");
        // Now all outer instances are reachable because inner instances hold them
        // This is a memory leak: outer instances cannot be GC'd
        // Even if we null out the objects in iterators later, the outer refs kept
        // Fix: use static nested class and pass only needed data

        // Wait for GC to show they are still alive
        System.gc();
        Thread.sleep(1000);
        System.out.println("After GC, iterators still size: " + iterators.size());
    }
}
Output
Created 1000000 inner class instances.
After GC, iterators still size: 1000000
(Heap usage will be high due to retained outer instances)
The 'Hidden Leash' Model
  • Static nested classes have no leash — the dog is independent.
  • Non-static inner classes always have a leash. If the dog escapes to a long-lived component, the owner is forever trapped.
  • Anonymous classes that capture outer instance methods also have a leash (the implicit this).
  • Lambdas do NOT have a leash unless they reference an instance method of the enclosing class.
  • The fix: make the dog static and pass the owner's particulars as a note.
Production Insight
Non-static inner classes add one extra reference (8 bytes on 64-bit JVM) per instance.
Anonymous classes generate a new .class file each — can bloat JAR size and classloading.
Synthetic accessors for private field access add method call overhead in hot paths.
Rule: profile with -XX:+UnlockDiagnosticVMOptions -XX:+DisplayVMOutput to see synthetic accessor calls.
Key Takeaway
Memory: static nested = zero overhead; non-static = 8+ bytes per instance.
Performance: synthetic accessors add hidden method calls.
Production rule: default to static. Only use non-static when the design demand is clear.

Choosing the Right Nested Class — A Decision You'll Make Every Week

The four types aren't equally useful. In practice, static nested classes account for the majority of real-world nested class usage, anonymous classes show up occasionally pre-Java-8 codebases, and local classes are rare. The decision tree is simpler than most tutorials suggest.

Ask yourself: does this class need access to the outer instance's fields or methods? If no — use a static nested class. If yes — ask whether this class is used in only one method. If in only one method with one or two methods to implement — consider a local class (or a lambda if SAM). If it's a one-shot implementation with no meaningful name — use an anonymous class.

There's also a soft rule around visibility. Static nested classes that you intend other packages to use should be public. Iterators, Builders, and other classes that implement your outer class's contracts but shouldn't be referenced directly should be private. The outer class's name acts as a natural namespace: Map.Entry, Thread.State, HttpRequest.Builder — all statically nested, all clearly 'belonging to' their outer type.

BankAccount.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package io.thecodeforge.nestedclasses;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// Demonstrates static nested class (Transaction) and
// non-static inner class (TransactionView) in one cohesive example
public class BankAccount {

    private final String accountNumber;
    private double balancePence; // stored in pence to avoid floating-point drift
    private final List<Transaction> history = new ArrayList<>();

    public BankAccount(String accountNumber, double openingBalancePounds) {\n        this.accountNumber = accountNumber;\n        this.balancePence  = Math.round(openingBalancePounds * 100);\n    }

    public void deposit(double amountPounds) {
        long amountPence = Math.round(amountPounds * 100);
        balancePence += amountPence;
        // Transaction is a value object — no need to know which account created it
        history.add(new Transaction("DEPOSIT", amountPence, balancePence));
    }

    public void withdraw(double amountPounds) {
        long amountPence = Math.round(amountPounds * 100);
        if (amountPence > balancePence) {
            throw new IllegalStateException("Insufficient funds");
        }
        balancePence -= amountPence;
        history.add(new Transaction("WITHDRAWAL", amountPence, balancePence));
    }

    // Returns a read-only view tied to THIS account instance
    public TransactionView getView() {
        return new TransactionView(); // creates inner class instance via outer instance
    }

    // ── STATIC NESTED CLASS ─────────────────────────────────────────────────
    // Transaction is a pure value/data object. It records what happened.
    // It doesn't need to call any method on BankAccount, so it's static.
    public static class Transaction {
        private final String          type;
        private final long            amountPence;
        private final long            balanceAfterPence;
        private final LocalDateTime   timestamp;

        private Transaction(String type, long amountPence, long balanceAfterPence) {\n            this.type              = type;\n            this.amountPence       = amountPence;\n            this.balanceAfterPence = balanceAfterPence;\n            this.timestamp         = LocalDateTime.now();\n        }

        @Override
        public String toString() {
            return String.format("%-12s £%6.2f  (balance: £%.2f)",
                type, amountPence / 100.0, balanceAfterPence / 100.0);
        }
    }

    // ── NON-STATIC INNER CLASS ──────────────────────────────────────────────
    // TransactionView is a *view of this specific account*.
    // It reads 'accountNumber', 'balancePence', and 'history' from the
    // enclosing BankAccount instance — it genuinely needs the outer instance.
    public class TransactionView {

        public void printStatement() {
            // Directly accesses outer instance's private fields — no getters needed
            System.out.println("Account: " + accountNumber);
            System.out.printf ("Balance: £%.2f%n", balancePence / 100.0);
            System.out.println("──────────────────────────────────────────");
            List<Transaction> snapshot = Collections.unmodifiableList(history);
            if (snapshot.isEmpty()) {
                System.out.println("No transactions yet.");
            } else {
                snapshot.forEach(System.out::println);
            }
        }
    }

    public static void main(String[] args) {
        BankAccount account = new BankAccount("GB29-NWBK-1234", 500.00);
        account.deposit(150.75);
        account.withdraw(42.00);
        account.deposit(10.00);

        // Getting a view — inner class instance is created through the outer instance
        TransactionView view = account.getView();
        view.printStatement();

        // A Transaction can be used standalone — it's a static nested class
        BankAccount.Transaction sampleTx =
            new BankAccount.Transaction("REFUND", 500, 65075); // only possible if public
        // Note: constructor is private here, so this line would not compile.
        // Shown to illustrate the syntax for public static nested classes.
    }
}
Output
Account: GB29-NWBK-1234
Balance: £618.75
──────────────────────────────────────────
DEPOSIT £150.75 (balance: £650.75)
WITHDRAWAL £ 42.00 (balance: £608.75)
DEPOSIT £ 10.00 (balance: £618.75)
Pro Tip: Syntax for Accessing Outer Members from Inner Class
If an inner class has a field with the same name as an outer class field, use OuterClassName.this.fieldName to explicitly reference the outer instance's version. For example: BankAccount.this.balancePence. This avoids silent shadowing bugs that compile fine but return the wrong value.
Production Insight
Choosing the wrong nested class type is a design debt that accumulates over time.
Non-static inner classes in public APIs leak implementation details because they expose outer instance methods.
Rule: make nested classes private static by default; only increase visibility and remove static when the API demands it.
Key Takeaway
Decision tree: need outer instance access? No -> static nested. Yes -> method-scoped? Yes -> local/anonymous. Otherwise inner class.
Visibility: private static nested for internal, public static nested for reusable components.
Remember: static nested classes are the safest bet in 90% of cases.
● Production incidentPOST-MORTEMseverity: high

The Anonymous Runnable That Held an Entire Session Alive

Symptom
App server heap grows continuously. After a few hours, full GC runs every few seconds. Eventually OutOfMemoryError: Java heap space. Heap dump shows millions of UserSession instances.
Assumption
Assumed user sessions timed out after 30 minutes and were garbage collected. The scheduled cleanup thread was supposed to remove expired sessions.
Root cause
A non-static inner class (actually an anonymous class) implementing Runnable was passed to a ScheduledExecutorService. The Runnable held an implicit reference to the outer UserSession object. Even after the session expired and all other references were gone, the executor's scheduled task kept the session alive. The task never checked if the session was still valid.
Fix
Changed the Runnable to a static nested class that accepted the UserSession as a parameter. Also added a check inside the task to skip execution if the session was already expired. Ensured the executor did not hold a strong reference to the session beyond the task's execution.
Key lesson
  • Never pass a non-static inner class instance to a long-lived executor or cache
  • Use static nested classes for any callback that outlives the enclosing method
  • Always verify the lifecycle of objects captured by inner classes passed to background threads
Production debug guideHow to identify, confirm, and fix hidden outer references in production heap dumps4 entries
Symptom · 01
Heap grows over time, especially after periods of high user activity. Old generation fills up and GC cannot reclaim objects.
Fix
Take a heap dump (jmap -dump:live,format=b,file=heap.hprof <pid>). Load in Eclipse MAT or VisualVM. Run the 'Leak Suspects' report. Look for objects that should be collected but have an unusually large retained set.
Symptom · 02
In Eclipse MAT, you see a non-static inner class instance (e.g., com.example.OuterClass$1, com.example.OuterClass$InnerClass) with a retained size larger than expected.
Fix
Right-click the instance -> 'Merge Shortest Paths to GC Roots' -> 'Exclude Weak References'. You'll see a path like 'OuterClass$InnerClass -> this$0 -> OuterClass'. The hidden field this$0 is the culprit — it's the reference from the inner class back to the outer.
Symptom · 03
The outer class instance is alive only because of inner class references — no other references in the application hold it.
Fix
Verify that the inner class is truly non-static. If it doesn't access any outer instance members, add the static keyword to the nested class. If it does need outer access, refactor: pass the required data via constructor parameters so the nested class can be static.
Symptom · 04
Memory leak occurs in an anonymous class used in a callback or scheduled task.
Fix
Convert the anonymous class to a static nested class. Use constructor parameters to supply necessary data. Remove the implicit outer reference. If the anonymous class is inside a lambda, note that lambdas do not capture this unless they reference the enclosing object's instance methods.
★ Inner Class Memory Leak DiagnosisQuick commands and steps to identify and fix inner class memory leaks in production.
Immediate action
Commands
jmap -dump:live,format=b,file=leak.hprof $(pgrep -f your-app)
eclipse-mat Leak.hprof (automated leak suspect analysis)
Fix now
Add static keyword to nested class if possible, or replace anonymous class with a static nested class.
Object retained by inner class is not GC'd+
Immediate action
In MAT, select suspect inner class instance, run 'Path to GC Roots' excluding weak refs
Commands
In MAT: Click 'Java Basics' -> 'Thread Overview' & 'Merge Shortest Paths to GC Roots'
Check if this$0 field points to outer class instance
Fix now
Refactor inner class to static, pass required data via constructor
Nested Class Types Comparison
Feature / AspectStatic Nested ClassNon-Static Inner ClassLocal ClassAnonymous Class
Declared insideOuter class bodyOuter class bodyMethod bodyMethod body / expression
Has static keywordYesNoNo (implicitly non-static)No (implicitly non-static)
Needs outer instance to instantiateNo — new Outer.Nested()Yes — outerRef.new Inner()No (created in scope)No (created in scope)
Can access outer instance membersNo (compile error)Yes — directlyYes — if effectively finalYes — if effectively final
Can have its own static membersYes (Java 16+: always; pre-16: only static final)No (pre Java 16)NoNo
Memory leak riskNoneYes — holds outer referenceLow (method-scoped)Low (method-scoped)
Can implement interfacesYesYesYesYes — exactly one
Can extend a classYesYesYesYes — exactly one
Has a reusable nameYesYesYes (within method)No
Best forBuilders, Nodes, EntriesIterators, ViewsOne-off multi-method logicOne-shot callbacks
Modern alternativeLambda (if SAM)Lambda (if SAM)
Compiled to separate .class fileYes (Outer$Nested.class)Yes (Outer$Inner.class)Yes (Outer$1LocalClass.class)Yes (Outer$1.class, Outer$2.class)
Synthetic accessors generatedOnly for private static field accessYes, for private outer instance field accessYes, for captured variablesYes, for captured variables

Key takeaways

1
Static nested classes have no hidden reference to the outer instance
they're safe to pass around long-lived objects and produce zero memory-leak risk from nesting.
2
Non-static inner classes carry a silent this$0 reference to their enclosing outer instance
every inner class object keeps its outer object alive for as long as the inner object is reachable.
3
Local and anonymous classes can only capture local variables that are effectively final
if you need mutable state inside them, use a mutable container like AtomicInteger or a single-element array as a workaround.
4
In modern Java (8+), lambdas replace anonymous classes for any single-abstract-method interface, but if the interface has multiple abstract methods you still need an anonymous or inner class
and this means different things in each.
5
Config
consistently use public static class for builders and value objects. Keep iterators and views as private non-static inner classes. Avoid anonymous classes for callbacks that outlive the enclosing method — use static factory methods instead.

Common mistakes to avoid

4 patterns
×

Using a non-static inner class when static would do

Symptom
Memory profiler shows outer objects accumulating and never being GC'd, even though you discarded all references to them. Heap dump reveals inner class instances holding references via this$0.
Fix
Add the static keyword to the nested class. If the compiler then complains about accessing an outer field, pass that field explicitly via the nested class constructor instead of relying on the hidden reference.
×

Trying to instantiate a non-static inner class from a static context

Symptom
Compile error: 'No enclosing instance of type Outer is accessible'.
Fix
Either create an outer instance first (Outer o = new Outer(); o.new Inner();) or, if you don't actually need the outer instance, make the inner class static.
×

Mutating a local variable after capturing it in an anonymous class or lambda

Symptom
Compile error: 'Variable used in anonymous class should be effectively final' (Java 8+) or 'local variable is accessed from within inner class; needs to be declared final' (pre-Java 8).
Fix
If you need a mutable counter inside an anonymous class, use a single-element array (int[] count = {0};) or an AtomicInteger — both are effectively-final references to a mutable container.
×

Passing an anonymous class that captures `this` to a long-lived executor

Symptom
Memory leak: outer object not GC'd because the anonymous class's implicit reference to the enclosing instance keeps it alive.
Fix
Convert the anonymous class to a static nested class that takes only the required data as constructor parameters. Avoid capturing this unintentionally.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between a static nested class and a non-static in...
Q02SENIOR
Why can an anonymous class or local class only capture effectively final...
Q03SENIOR
If `this` inside a lambda and `this` inside an anonymous class both appe...
Q01 of 03JUNIOR

What is the difference between a static nested class and a non-static inner class in Java, and when would you choose one over the other?

ANSWER
A static nested class is declared with the static keyword. It has no implicit reference to an outer instance, so it can be instantiated independently: new Outer.Nested(). It can only access static members of the outer class. A non-static inner class has an implicit this$0 reference to an outer instance, so it can access all outer instance members, including private ones. You must instantiate it through an outer instance: outer.new Inner(). Choose static nested when the nested class does not need access to outer instance fields — e.g., a Builder, a Node in a data structure, a value object. A non-static inner is appropriate when the nested class needs to operate on a specific outer instance — e.g., an iterator that needs to read the outer collection's internal array. In practice, default to static; only drop the keyword when you genuinely need outer instance access.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Can a static nested class in Java access the private members of its outer class?
02
What does 'effectively final' mean in the context of Java inner classes?
03
Why does Java use a non-static inner class for ArrayList's iterator instead of a separate top-level class?
04
How can I avoid memory leaks when using inner classes in Android or UI event handlers?
🔥

That's OOP Concepts. Mark it forged?

5 min read · try the examples if you haven't

Previous
instanceof Operator in Java
16 / 16 · OOP Concepts
Next
Strings in Java