Java Objects, Nested Classes and Garbage Collection (1Z0-831)
An object's life cycle in Java runs from creation with new, through use while some live code can reach it, to eligibility for garbage collection once it is unreachable — and Oracle 1Z0-831 objective 3.1 asks you to trace that cycle in code and to create objects of every kind of nested class. This lesson separates references from objects, shows how aliasing and reassignment differ (including inside method calls), covers static nested, inner, local and anonymous classes with their exact instantiation syntax, and teaches how to decide which objects are eligible for collection at a given line.
On this page10 sections
- Objects versus references: what new actually creates
- Aliasing: changing an object versus reassigning a reference
- Passing references to methods: pass-by-value of the reference
- The four kinds of nested class
- Instantiating inner and static nested classes
- Shadowing and Outer.this
- Local and anonymous classes: capture and the meaning of this
- The object life cycle and garbage-collection eligibility
- System.gc() and when memory is actually reclaimed
- Inner objects keep their outer object alive
- Distinguish declaring a reference from creating an object, and count the objects a snippet creates
- Trace aliasing, reference reassignment and pass-by-value of references through method calls
- Instantiate static nested, inner, local and anonymous class objects with the correct syntax
- Resolve shadowed names with this and Outer.this, and explain what this means in anonymous classes and lambdas
- Apply the effectively final rule to variables captured by local and anonymous classes
- Determine which objects are eligible for garbage collection at a given line, including cycles, arrays and inner-class links
- Explain what System.gc() does and does not guarantee
Objects versus references: what new actually creates
An object exists only when an expression such as new Account() runs; declaring a variable creates a reference, never an object. A reference is a value that either points to one object or is null. new allocates the object, sets every field to its default (0, false, '\u0000' or null), runs the constructor chain and returns a reference to the result.
class Account {
int balance;
String owner;
}
public class Main {
public static void main(String[] args) {
Account a; // a reference, no object
Account b = new Account(); // one object
a = b; // still one object
System.out.println(b.balance + " " + b.owner);
System.out.println(a == b);
}
}This prints 0 null and then true. Only one Account object exists: a and b are two references to it, and == on references compares identity (the same object), not contents.
- Count objects by counting
newexpressions that actually run (plus array creations and array initializers), never by counting variables.Lamp l1, l2 = new Lamp();declares two references and creates one object. - A reference field that nothing assigns stays
null. Calling a method through it (order.customer.name()whencustomerwas never set) compiles, then throwsNullPointerExceptionat runtime. - A local variable gets no default: reading it before it is definitely assigned is a compile error, not a
null.
Aliasing: changing an object versus reassigning a reference
When two variables hold the same reference they are aliases (say Tank t2 = t1;): a change made through either one changes the single shared object, while assigning a new reference to one variable changes only that variable. Keep the two operations apart when you trace code:
| Statement | What changes | Seen through the alias? |
|---|---|---|
t2.level = 40; | A field of the object t2 points to | Yes — it is the same object |
t1 = new Tank(); | Only the variable t1; it now points to a new object | No — t2 still points to the old one |
t1 = null; | Only the variable t1 | No — the object survives while t2 refers to it |
A reliable way to trace is to draw each object as a box and each variable as an arrow. Field assignments write inside a box; variable assignments move an arrow. Anything you compute afterwards reads the box the arrow currently points to.
Passing references to methods: pass-by-value of the reference
Java passes every argument by value, and for an object type the value copied is the reference. The method's parameter therefore starts as another alias of the caller's object: it can mutate that object, but reassigning the parameter only reseats the method's own copy and never reaches the caller's variable.
class Wallet {
int coins;
}
public class Main {
static void spend(Wallet w) {
w.coins -= 3; // reaches the caller's object
w = new Wallet(); // reseats only the parameter
w.coins = 50; // lands on the new object
}
public static void main(String[] args) {
Wallet mine = new Wallet();
mine.coins = 20;
Wallet shared = mine; // alias: same object
spend(shared);
System.out.println(mine.coins + " " + shared.coins);
}
}The program prints 17 17. Inside spend, w.coins -= 3 lands on the one object that mine and shared both point to; after w = new Wallet() the parameter points to a brand-new object, so w.coins = 50 changes something the caller never sees. Two general rules follow:
- Assignments to parameters never travel back. However a method reassigns or exchanges its parameters, the caller's variables still refer to exactly the objects they referred to before the call.
- A field write goes to whatever object the parameter refers to at that moment. Trace each parameter's arrow line by line: if it currently points at one of the caller's objects, the write is visible to the caller; if it points at an object the method created, it is not.
The four kinds of nested class
A nested class is any class declared inside another class or block, and Java has four kinds that differ in whether they need an enclosing instance and where they can be created. A static nested class is declared with static at member level; every other nested class is an inner class — a non-static member class, a local class or an anonymous class.
| Kind | Declared | Enclosing instance? | Created with |
|---|---|---|---|
| Static nested class | Member level, with static | None | new Outer.Nested() (or new Nested() inside Outer) |
| Inner (member) class | Member level, no static | Exactly one, fixed for life | outerRef.new Inner(); just new Inner() inside an instance method of Outer |
| Local class | Inside a method, constructor or block | Yes when declared in an instance context | new Local(), only within its block, after the declaration |
| Anonymous class | In a new expression with a body | Yes when created in an instance context | new Type(args) { … } — declared and instantiated at once |
Member records, enums and interfaces are implicitly static. A record declared inside a class is a static nested type even without the keyword, so its methods cannot read the outer class's instance fields (non-static variable … cannot be referenced from a static context). Local records, enums and interfaces are implicitly static too, so they cannot capture the enclosing method's local variables.
Instantiating inner and static nested classes
An inner class object can only be created with an enclosing object to bind to, while a static nested class object needs none. Outside the outer class, or from a static method, you supply the enclosing instance with the qualified form outerRef.new Inner(); the type is written Outer.Inner.
class Garage{
private int opens;
private static int visits = 10;
class Door { // needs a Garage instance
void open() { opens++; }
}
static class Inspector { // no Garage instance
String report(Garage g) {
return g.opens + "/" + visits;
}
}
}
public class Main {
public static void main(String[] args) {
Garage g = new Garage();
Garage.Door d = g.new Door();
d.open();
d.open();
Garage.Inspector i = new Garage.Inspector();
System.out.println(i.report(g));
}
}This prints 2/10: two calls through the Door bound to g raised g's private opens field, and the static nested Inspector read it through the explicit reference it was given. The legal and illegal forms are near-twins, so learn them precisely:
new Outer().new Inner()andref.new Inner()compile — any expression of typeOutercan be the enclosing instance.new Outer.Inner()only names the type more fully; it does not supply an enclosing instance. In astaticmethod it fails exactly likenew Inner(): non-static variable this cannot be referenced from a static context.new Inner()orthis.new Inner()inside astaticmethod fails: non-static variable this cannot be referenced from a static context. Inside an instance method, plainnew Inner()works because it meansthis.new Inner().new ref.Inner()is not the syntax — the compiler readsrefas a package or type name.new Outer().new Nested()for a static nested class fails (qualified new of static class); usenew Outer.Nested().
The binding is permanent: every inner object created from one outer object reads and writes that outer object's fields, so two inner objects made from the same outer object share its state, while one made from another outer object does not. A static nested class reaches the outer class's static members directly (even private ones) but its instance members only through an explicit reference. Since Java 16 an inner class may also declare static members; a static field in an inner class is one variable shared across every enclosing instance.
Shadowing and Outer.this
When an inner class declares a field with the same name as one in its enclosing class, the nearest declaration wins and Outer.this.name reaches the shadowed one. Inside an inner class, a simple name resolves to the innermost declaration: a parameter or local first, then the inner class's own field, then each enclosing class outwards. this always means the current (innermost) object; ClassName.this means the enclosing instance of that class.
class Shop {
String label = "shop";
class Shelf {
String label = "shelf";
class Slot {
String label = "slot";
String where(String label) {
return Shop.this.label + "/" + Shelf.this.label
+ "/" + this.label + "/" + label;
}
}
}
}
public class Main {
public static void main(String[] args) {
Shop shop = new Shop();
Shop.Shelf.Slot slot = shop.new Shelf().new Slot();
System.out.println(slot.where("tag"));
}
}This prints shop/shelf/slot/tag. Each level of nesting has its own enclosing object, so with two levels Shop.this and Shelf.this are different objects, and the bare name label is the parameter because it is the innermost declaration. ClassName.this is a reference to the real enclosing object, not a copy taken when the inner object was created: whatever value that object's field holds at the moment of the read is what you get.
Local and anonymous classes: capture and the meaning of this
A local or anonymous class may read a local variable or parameter of its enclosing method only if that variable is final or effectively final — assigned once and never reassigned anywhere, before or after the class. Reassigning a captured variable is a compile error: local variables referenced from an inner class must be final or effectively final. Lambdas follow the same rule.
- Loop variables. A classic
forindex is reassigned by its update expression, so it is not effectively final and cannot be captured; declaring itfinaljust moves the error to the update. A variable declared inside the loop body and assigned once is a new, effectively final variable on each iteration, and so is the variable of an enhancedfor— both can be captured. - An anonymous class can extend an abstract class:
new Base(args) { … }passes the arguments to the superclass constructor and must implement its abstract methods. It cannot declare a constructor of its own; an anonymous class implementing an interface takes no arguments.
import java.util.function.Supplier;
class Radio {
String tag = "radio";
void tune() {
String suffix = "!"; // effectively final
Supplier<String> anon = new Supplier<>() {
String tag = "dial";
public String get() {
return this.tag + "," + Radio.this.tag + suffix;
}
};
Supplier<String> lambda = () -> this.tag + suffix;
System.out.println(anon.get());
System.out.println(lambda.get());
}
}
public class Main {
public static void main(String[] args) {
new Radio().tune();
}
}This prints dial,radio! and then radio!. Inside an anonymous class body, this is the anonymous object itself (so this.tag is its own field, and Radio.this.tag reaches the outer one); inside a lambda, this is the enclosing Radio instance, because a lambda does not introduce a new object scope. The same split applies to an overridden toString() printed through this.
The object life cycle and garbage-collection eligibility
An object is eligible for garbage collection as soon as no live thread can reach it through any chain of references from a root. Roots are the local variables and parameters of methods that are still running, static fields of loaded classes and live threads. An object's life cycle is therefore: created by new, used while reachable, eligible once unreachable, and reclaimed at some later time the JVM chooses.
class Wagon {
Wagon coupled;
}
public class Main {
public static void main(String[] args) {
Wagon front = new Wagon(); // wagon 1
Wagon rear = new Wagon(); // wagon 2
front.coupled = rear;
rear.coupled = front; // a cycle
Wagon spare = new Wagon(); // wagon 3
spare.coupled = new Wagon(); // wagon 4
rear = null;
front = null; // 1 and 2: unreachable island
spare = spare.coupled; // 3: unreachable; 4: held
System.out.println("parked");
}
}When parked is printed, wagons 1, 2 and 3 are eligible and wagon 4 is not (variable spare now holds it). Rules for tracing:
- Only references from reachable things count. Wagons 1 and 2 point at each other, but nothing reachable points at either — an island of isolation, collectable however many references it holds internally. Add one incoming reference from a live variable or object and the whole island is reachable again.
- Setting one reference to
nullmakes an object eligible only if it was the last path to it. - Arrays are objects. Count the array itself, and an element is eligible once neither the array (if reachable) nor any variable refers to it.
- Method exits end local references, but an object a method stored in a
staticfield or returned to a caller who kept it stays reachable.
System.gc() and when memory is actually reclaimed
System.gc() (the same as Runtime.getRuntime().gc()) only suggests that the JVM expend effort recycling unused objects; it guarantees nothing. The Java SE API documentation says there is no guarantee it reclaims any particular objects or amount of space, or completes at any particular time — before the call returns or ever. Two consequences follow:
- Eligibility and reclamation are different events. Your code decides eligibility by dropping references; the JVM decides if and when to reclaim. A reference cycle that is unreachable is eligible whether or not any collection ever runs.
- Never rely on collection for program logic.
Object.finalize()has been deprecated for removal since Java 18; release resources explicitly, for example with try-with-resources.
Inner objects keep their outer object alive
An inner class object is tied to its enclosing instance, so as long as the inner object is reachable, the outer object it uses is reachable too. Storing an inner object in a long-lived place — a static field, a listener list, a cache — therefore keeps the whole outer object (and everything it references, such as a large buffer) out of reach of the collector, even after every direct reference to the outer object is gone.
Under the hood the compiler stores the enclosing instance in a hidden final field (shown by javap -p as this$0). Since JDK 18, javac omits that field when the inner class never uses its enclosing instance (serializable inner classes excepted), so an inner class that touches no outer member no longer pins its outer object. Reason about code that does use outer members (as exam snippets do) as holding a reference. When an inner class does not need the outer object at all, declare it static: a static nested class has no enclosing instance and cannot cause this kind of leak.
Tip. Objective 3.1 is examined almost entirely through short, complete programs. Expect to read code and state what it prints, whether it compiles, or whether it throws — for example a call through a reference field that was never assigned, or an inner class created from a static context. Instantiation items list near-identical expressions (outerRef.new Inner(), new Outer.Inner(), new Outer.Nested(), this.new Inner()) and ask which compile. Shadowing items mix parameters, this.x and Outer.this.x across one or two levels of nesting. Garbage-collection items give a sequence of assignments and ask which objects, or how many, are eligible at a marked line — with cycles, arrays, static fields, returned objects and inner-to-outer links as the twists — or ask what System.gc() guarantees. Capture items reassign a local after an anonymous or local class uses it, or capture a loop index.
- A variable holds a reference, not an object; only new (or an array creation) creates objects, and unassigned reference fields are null.
- Mutating through an alias changes the shared object; reassigning a variable changes only that variable.
- Java passes references by value: a method can mutate the caller's object but can never reseat the caller's variable.
- Inner class objects need an enclosing instance: outerRef.new Inner(); static nested classes use new Outer.Nested().
- Member and local records, enums and interfaces are implicitly static and cannot use outer instance fields or captured locals.
- Outer.this.name reaches a shadowed field of an enclosing instance; this in an anonymous class is the anonymous object, in a lambda the enclosing one.
- Local and anonymous classes (and lambdas) may only capture final or effectively final locals.
- An object is eligible once no live thread can reach it from a root; unreachable cycles are eligible too.
- System.gc() is only a request — eligibility is guaranteed by your references, reclamation is up to the JVM.
- A reachable inner object that uses its outer object keeps that outer object alive; prefer static nested classes when the link is not needed.
Frequently asked questions
Is Java pass-by-value or pass-by-reference?
Java is always pass-by-value. For object types the value copied into the parameter is the reference, so a method can change the object both variables point to, but assigning a new object to the parameter changes only the method's copy and the caller's variable keeps pointing to the original object.
How do you create an instance of an inner class from a static method?
You must supply an enclosing instance with the qualified form outerRef.new Inner(), for example new Outer().new Inner(), and the variable type is Outer.Inner. Writing new Inner() or this.new Inner() in a static method fails to compile because there is no this. A static nested class needs no instance: new Outer.Nested().
What is the difference between a static nested class and an inner class in Java?
An inner class (non-static member, local or anonymous class) is bound to an instance of its enclosing class and can use that instance's fields directly. A static nested class has no enclosing instance: it is created with new Outer.Nested(), sees the outer class's static members directly and reaches instance members only through an explicit reference. Member records, enums and interfaces are always static.
When is an object eligible for garbage collection in Java?
An object is eligible when no live thread can reach it through any chain of references starting from a root: a local variable or parameter of a running method, a static field or a live thread. Objects that only reference each other in a cycle are eligible too, because references from unreachable objects do not count.
Does System.gc() force garbage collection?
No. System.gc() suggests that the JVM spend effort reclaiming unused objects, but the Java SE API documentation gives no guarantee about which objects are reclaimed, how much memory is freed or when the work completes. Making an object unreachable is what makes it eligible; the JVM alone decides when it is reclaimed.
Why must variables used in an anonymous class be effectively final?
A local or anonymous class captures a copy of each local variable it reads, so Java requires that the variable is final or effectively final — assigned once and never changed — to keep the copy and the original identical. Reassigning a captured variable anywhere in the method is a compile error; the same rule applies to lambdas.
Can an inner class object cause a memory leak?
Yes. An inner class object that uses its enclosing instance holds a hidden reference to it, so storing the inner object somewhere long-lived, such as a static field or listener list, keeps the outer object and everything it references reachable. Declaring the nested class static removes that link.
Source
This lesson covers the "Applying Object-Oriented Principles in Java Programs" domain of the official 1Z0-831 exam guide. Vendors revise their guides — check the source for the current version.
- Java SE 25 Developer Professional (1Z0-831) exam page and topics — Oracle University
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.