Objects in Memory
References, aliasing, null, and garbage collection
After this lesson, you will be able to:
- Explain where primitives and objects are stored in memory (stack vs. heap)
- Draw accurate memory diagrams showing references from stack to heap
- Predict the behavior of aliased object references
- Identify when
NullPointerExceptionwill occur and write defensive null checks - Trace what happens when objects are passed to methods
- Describe how garbage collection reclaims unreachable objects
The Question That Breaks Beginners
Look at this code and predict the output before reading further:
Student alice = new Student("Alice", 3.8, 1001);
Student bob = alice;
bob.setGpa(4.0);
System.out.println(alice.getGpa());
If you guessed 3.8, you are wrong. The answer is 4.0. If that surprises you, this lesson will explain exactly why. The key is understanding what variables actually store when they hold objects.
From CSCD 110: In Python, the same thing happens:
a = [1, 2, 3]; b = a; b[0] = 99changesa[0]too, because lists are mutable objects andb = acopies the reference, not the list. Java works the same way for all objects, but makes it more visible because you must think about types.
Stack vs. Heap: Two Regions of Memory
Java uses two distinct regions of memory. Understanding the difference is essential.
The stack is where local variables and method call frames live. It is fast, small, and automatically cleaned up when a method returns. Each method call creates a new frame on the stack; when the method finishes, that frame is destroyed.
The heap is where objects live. It is large, slower, and managed by the garbage collector. Objects persist on the heap as long as at least one reference points to them.
Here is the rule:
Primitives (int, double, boolean, char) → value stored directly on the stack
Objects (Student, String, arrays) → object on the heap, reference on the stack
Memory Diagram: Primitives vs. Objects
After this code runs:
int age = 25;
double gpa = 3.8;
Student alice = new Student("Alice", 3.8, 1001);
Memory looks like this:
STACK HEAP
┌──────────────┐ ┌──────────────────────┐
│ age: 25 │ │ │
├──────────────┤ │ @5024: Student │
│ gpa: 3.8 │ │ name ──► "Alice" │
├──────────────┤ │ gpa = 3.8 │
│ alice: @5024 ─┼──────────►│ id = 1001 │
└──────────────┘ │ │
└──────────────────────┘
age and gpa hold their actual values directly on the stack. alice holds a reference — a memory address (@5024) — that points to the Student object on the heap. The variable alice is not the student. It is an arrow pointing to the student.
Key Insight: Primitives store values. Object variables store references (arrows). This single distinction explains aliasing,
==vs..equals(), null pointer exceptions, and how methods interact with objects.
References and Aliasing
A reference is a memory address — an arrow pointing to an object on the heap. When you assign one object variable to another, you copy the reference, not the object:
Student alice = new Student("Alice", 3.8, 1001);
Student bob = alice; // bob now points to the SAME object
STACK HEAP
┌──────────────┐ ┌──────────────────────┐
│ alice: @5024 ─┼─────┐ │ │
├──────────────┤ ├────►│ @5024: Student │
│ bob: @5024 ─┼─────┘ │ name ──► "Alice" │
└──────────────┘ │ gpa = 3.8 │
│ id = 1001 │
└──────────────────────┘
Both alice and bob hold the same address. They are two names for the same object. This is called aliasing.
Now the opening puzzle makes sense:
Student alice = new Student("Alice", 3.8, 1001);
Student bob = alice; // alias — same object
bob.setGpa(4.0); // modifies the shared object
System.out.println(alice.getGpa()); // 4.0
There is only one Student object. Both variables point to it. Changing it through bob changes what alice sees, because they are the same object.
Common Pitfall:
Student bob = alice;does NOT create a copy of Alice. It creates a second reference to the same object. To make an actual independent copy, you must create a new object:Student bob = new Student(alice.getName(), alice.getGpa(), alice.getId());
After this code runs, how many Student objects exist on the heap?
Student a = new Student("A", 3.0, 1);
Student b = new Student("B", 3.5, 2);
Student c = a;
== vs. .equals(): References vs. Content
Now you can understand a critical distinction. The == operator compares references (memory addresses), not content:
Student alice1 = new Student("Alice", 3.8, 1001);
Student alice2 = new Student("Alice", 3.8, 1001);
System.out.println(alice1 == alice2); // false — different objects
System.out.println(alice1.equals(alice2)); // true — same content (if equals is overridden)
STACK HEAP
┌───────────────┐ ┌──────────────────────┐
│ alice1: @5024 ─┼─────────►│ @5024: Student │
├───────────────┤ │ name ──► "Alice" │
│ alice2: @5048 ─┼───┐ │ gpa = 3.8 │
└───────────────┘ │ │ id = 1001 │
│ ├──────────────────────┤
└─────►│ @5048: Student │
│ name ──► "Alice" │
│ gpa = 3.8 │
│ id = 1001 │
└──────────────────────┘
Two separate objects with identical data. == returns false because the arrows point to different locations. .equals() returns true because the content matches (assuming you overrode equals to compare by ID or fields).
The same applies to String:
String s1 = new String("Hello");
String s2 = new String("Hello");
System.out.println(s1 == s2); // false — different objects
System.out.println(s1.equals(s2)); // true — same text
The Trick: For objects, always use
.equals()to compare content. The==operator only tells you whether two variables point to the same object in memory, not whether they represent the same data.
Null: The Absence of a Reference
What if an object variable does not point to anything? Java uses the special value null:
Student alice = null; // alice points to nothing
STACK HEAP
┌──────────────┐ ┌──────────────────────┐
│ alice: null │ │ (nothing here) │
└──────────────┘ └──────────────────────┘
null means “no object.” The variable exists on the stack but its arrow points nowhere.
If you try to follow a null reference — calling a method or accessing a field — Java throws a NullPointerException:
Student alice = null;
System.out.println(alice.getGpa()); // NullPointerException!
This is one of the most common runtime errors in Java. The fix is a null check:
if (alice != null) {
System.out.println(alice.getGpa());
} else {
System.out.println("No student assigned.");
}
A common pattern with arrays of objects:
Student[] roster = new Student[5];
// All 5 slots are null by default!
roster[0] = new Student("Alice", 3.8, 1001);
// roster[1] through roster[4] are still null
for (int i = 0; i < roster.length; i++) {
if (roster[i] != null) {
System.out.println(roster[i]);
}
}
When you create an object array with new Student[5], Java allocates 5 reference slots, all initialized to null. No Student objects exist yet. You must create them individually with new.
What happens when this code runs?
String[] words = new String[3];
System.out.println(words[0].length());
Passing Objects to Methods
When you pass a primitive to a method, the method receives a copy of the value. The original variable cannot change:
public static void tryToChange(int x) {
x = 99; // changes the local copy only
}
int num = 5;
tryToChange(num);
System.out.println(num); // still 5
When you pass an object to a method, the method receives a copy of the reference. The reference is copied, but it still points to the same object on the heap:
public static void giveRaise(final Student s) {
s.setGpa(4.0); // modifies the SHARED object
}
Student alice = new Student("Alice", 3.8, 1001);
giveRaise(alice);
System.out.println(alice.getGpa()); // 4.0 — the object changed!
main's stack frame giveRaise's frame HEAP
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ alice: @5024 ─┼────┐ │ s: @5024 ─┼──►│ @5024: Student │
└──────────────┘ └───►│ │ │ gpa: 3.8→4.0 │
└──────────────┘ └──────────────────┘
Both alice (in main) and s (in giveRaise) point to the same heap object. When giveRaise modifies the object through s, the change is visible through alice because there is only one object.
However, the method cannot reassign the caller’s variable:
public static void tryToReplace(Student s) {
s = new Student("Bob", 3.0, 1002); // reassigns LOCAL copy of reference
}
Student alice = new Student("Alice", 3.8, 1001);
tryToReplace(alice);
System.out.println(alice.getName()); // still "Alice"
main's stack frame tryToReplace's frame HEAP
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ alice: @5024 ─┼────────►│ │ │ @5024: Student │
└──────────────┘ │ s: @5048 ─┼──►│ name: "Alice" │
└──────────────┘ ├──────────────────┤
│ @5048: Student │
│ name: "Bob" │
└──────────────────┘
Inside tryToReplace, s is a local copy of the reference. Reassigning s to point to a new object does not affect alice in main. The original reference is unchanged.
Key Insight: Java is always pass-by-value. For primitives, the value is the data. For objects, the value is the reference (the arrow). The method gets its own copy of the arrow, which still points to the same object. The method can follow the arrow and modify the object, but it cannot change where the caller’s arrow points.
What does this code print?
public static void reset(Student s) { s = null; }
Student alice = new Student("Alice", 3.8, 1001);
reset(alice);
System.out.println(alice.getName());
Garbage Collection
When no variable in the program points to an object, that object is unreachable. Java’s garbage collector automatically reclaims the memory:
Student alice = new Student("Alice", 3.8, 1001);
alice = null; // the Student object is now unreachable
// Garbage collector will eventually free that memory
STACK HEAP
┌──────────────┐ ┌──────────────────────┐
│ alice: null │ │ @5024: Student │ ← no references!
└──────────────┘ │ (garbage) │
└──────────────────────┘
Another example:
Student alice = new Student("Alice", 3.8, 1001);
alice = new Student("Bob", 3.5, 1002);
// The original "Alice" Student object is now garbage
After the second assignment, alice points to the new “Bob” object. Nobody points to the “Alice” object anymore, so it is eligible for garbage collection.
You do not need to manually free memory in Java. The garbage collector handles it. This is one of Java’s advantages over languages like C and C++, where you must call free() or delete yourself. In CSCD 240, you will manage memory manually and appreciate what the garbage collector does for you.
Complete Example: Tracing Exercise
Trace through this code and draw the memory diagram at each step. Then check your answer:
public class MemoryTrace {
public static void main(final String[] args) {
Student alice = new Student("Alice", 3.8, 1001); // Step 1
Student bob = new Student("Bob", 3.2, 1002); // Step 2
Student charlie = alice; // Step 3
alice = bob; // Step 4
System.out.println("alice: " + alice);
System.out.println("bob: " + bob);
System.out.println("charlie: " + charlie);
}
}
Step 1: Create Alice
STACK HEAP
┌────────────────┐ ┌────────────────────┐
│ alice: @1000 ─┼─────►│ @1000: Student │
└────────────────┘ │ name: "Alice" │
│ gpa: 3.8 │
└────────────────────┘
Step 2: Create Bob
STACK HEAP
┌────────────────┐ ┌────────────────────┐
│ alice: @1000 ─┼─────►│ @1000: Student │
├────────────────┤ │ name: "Alice" │
│ bob: @2000 ─┼──┐ │ gpa: 3.8 │
└────────────────┘ │ ├────────────────────┤
└─►│ @2000: Student │
│ name: "Bob" │
│ gpa: 3.2 │
└────────────────────┘
Step 3: charlie = alice (aliasing)
STACK HEAP
┌────────────────┐ ┌────────────────────┐
│ alice: @1000 ─┼──┐ │ @1000: Student │
├────────────────┤ ├──►│ name: "Alice" │
│ bob: @2000 ─┼──┤ │ gpa: 3.8 │
├────────────────┤ │ ├────────────────────┤
│ charlie: @1000 ─┼──┘ │ @2000: Student │
└────────────────┘ └───►│ name: "Bob" │
│ gpa: 3.2 │
└────────────────────┘
Wait — bob’s arrow is wrong in the diagram above. Let me fix that. alice and charlie both point to @1000. bob points to @2000:
STACK HEAP
┌────────────────┐ ┌────────────────────┐
│ alice: @1000 ─┼─┐ │ @1000: Student │
├────────────────┤ ├───►│ name: "Alice" │
│ bob: @2000 ─┼──┐ │ gpa: 3.8 │
├────────────────┤ │ ├────────────────────┤
│ charlie: @1000 ─┼─┘ │ @2000: Student │
└────────────────┘ └──►│ name: "Bob" │
│ gpa: 3.2 │
└────────────────────┘
Step 4: alice = bob (reassign alice)
STACK HEAP
┌────────────────┐ ┌────────────────────┐
│ alice: @2000 ─┼──┐ │ @1000: Student │
├────────────────┤ ├──►│ name: "Alice" │ ◄── charlie points here
│ bob: @2000 ─┼──┘ │ gpa: 3.8 │
├────────────────┤ ├────────────────────┤
│ charlie: @1000 ─┼─────►│ @2000: Student │ ◄── alice AND bob point here
└────────────────┘ │ name: "Bob" │
│ gpa: 3.2 │
└────────────────────┘
Wait — the arrows crossed. Let me be precise:
alicenow points to@2000(Bob’s object)bobstill points to@2000(Bob’s object)charliestill points to@1000(Alice’s object)
Output:
alice: Bob (ID: 1002, GPA: 3.2)
bob: Bob (ID: 1002, GPA: 3.2)
charlie: Alice (ID: 1001, GPA: 3.8)
Both objects are still reachable — one through charlie, the other through alice and bob. Nothing is garbage collected.
In the tracing example above, if we add the line charlie = null; after Step 4, is the "Alice" Student object eligible for garbage collection?
Quick Reference
| Concept | Rule |
|---|---|
| Primitives | Stored directly on the stack |
| Objects | Stored on the heap, referenced from the stack |
= with objects |
Copies the reference, not the object |
| Aliasing | Two variables pointing to the same object |
== on objects |
Compares references (memory addresses) |
.equals() |
Compares content (if overridden) |
null |
A reference that points to nothing |
NullPointerException |
Calling a method on a null reference |
| Pass-by-value | Primitives: copy of value. Objects: copy of reference |
| Garbage collection | Automatic cleanup of unreachable objects |
Summary
Java organizes memory into two regions: the stack (for primitives and references) and the heap (for objects). Object variables do not store objects — they store references that point to objects on the heap.
Assigning one object variable to another copies the reference, creating an alias. Both variables point to the same object, so changes through one are visible through the other. The == operator compares references; .equals() compares content.
null means a reference points to nothing. Following a null reference throws NullPointerException — check for null before using any object that might not be initialized. When no references point to an object, the garbage collector reclaims it automatically.
When you pass an object to a method, the method receives a copy of the reference. It can modify the object (both caller and method share the same heap object), but it cannot reassign the caller’s variable.
These concepts are not just theory. Every bug involving unexpected state changes, null pointer crashes, or mysterious side effects traces back to how references work. In CSCD 211 and CSCD 240, you will build on this foundation extensively.