Objects Deep Dive | Size, Call by Value vs Reference | Shallow vs Deep Copy
Understanding how objects behave inside the Java Virtual Machine (JVM) memory architecture is fundamental to writing performant, bug-free, enterprise-grade applications. This chapter dives deep into the internal layout of Java objects, memory footprint calculation, the reality of parameter passing in Java, and object copying techniques (Shallow vs. Deep Copy).
1. The Mental Model: Objects in Java Memory
To understand Java objects, you must first separate a reference variable from the actual object instance.
In Java memory management:
- Stack Memory: Stores method execution frames, primitive local variables, and object reference variables (pointers/addresses).
- Heap Memory: Stores actual object instances, complete with their instance variables and object metadata.
When you write Person p1 = new Person("Alice", 25);, p1 is not the object. p1 is a reference variable stored on the stack that holds the 64-bit (or 32-bit compressed) memory address pointing to the Person object residing in the heap.
2. Internal Structure of a Java Object
Every Java object allocated on the heap consists of three core components:
Real-Life Analogy
Think of a Java object as a commercial shipping parcel:
- Object Header: The shipping label containing tracking info, lock state, and handling instructions.
- Instance Data: The actual items inside the box (your payload variables).
- Padding: Extra styrofoam packing peanuts added to make the box fit standard shipping pallet sizes (8-byte grid alignment).
Component Breakdown
- Mark Word (8 bytes on 64-bit JVM): Stores runtime metadata including:
- Identity HashCode
- GC (Garbage Collection) age tags
- Locking state & synchronization flags
- Biased locking thread ID
- Klass Word / Metadata Pointer: Points to the class metadata in the Metaspace (defining methods, field structure, and class identity).
- 8 bytes (64-bit architecture without compressed OOPs)
- 4 bytes (64-bit architecture with Compressed OOPs enabled by default in JVMs under 32GB Heap)
- Instance Data: Contains the actual user-defined fields of the class.
- Padding: The JVM allocates memory in exact multiples of 8 bytes. If the sum of Header + Instance Data is not divisible by 8, padding bytes are added to complete the boundary.
3. Calculating Java Object Size
Let us step through calculating the exact heap memory footprint of a Java object.
Primitive & Reference Sizes Reference
byte,boolean: 1 byteshort,char: 2 bytesint,float: 4 byteslong,double: 8 bytes- Reference Pointer: 4 bytes (with Compressed OOPs enabled) or 8 bytes (without Compressed OOPs)
Concrete Calculation Example
Consider the following Java class on a 64-bit JVM with Compressed OOPs enabled (default):
public class Student {
int id; // 4 bytes
double gpa; // 8 bytes
boolean active; // 1 byte
}
Let's compute the size of new Student():
1. Object Header:
- Mark Word: 8 bytes
- Compressed Klass Pointer: 4 bytes
Header Subtotal = 12 bytes
2. Instance Fields:
- int id: 4 bytes
- double gpa: 8 bytes
- boolean active: 1 byte
Fields Subtotal = 13 bytes
3. Unpadded Total: 12 + 13 = 25 bytes
4. Alignment Padding:
- Next multiple of 8 >= 25 is 32.
- Padding required = 32 - 25 = 7 bytes
Total Object Heap Footprint = 32 bytes
Note: The JVM reorganizes internal fields (Field Reordering) to optimize byte alignment and eliminate internal space waste before appending final trailing padding.
4. Demystifying Parameter Passing: Call by Value vs. Call by Reference
A common point of confusion among developers is whether Java is "Call by Value" or "Call by Reference".
Definitions
- Call by Value: The program passes a copy of the actual variable's value into the function. Modifying the parameter inside the function has zero effect on the original variable in the caller.
- Call by Reference: The program passes the actual memory alias/reference of the caller's variable. Modifying the parameter inside the function directly updates the caller's original variable.
The Golden Rule of Java
JAVA IS STRICTLY 100% CALL BY VALUE. There is no Call by Reference in Java.
When passing an object to a method in Java, you are not passing the object itself, nor are you passing a reference by reference. You are passing a copy of the reference variable (memory address) by value.
5. Parameter Passing Mechanics: Primitives vs. References
Case 1: Passing Primitive Types
When passing primitives, the raw value is duplicated into the method's local stack frame.
public class PrimitivePassTest {
public static void main(String[] args) {
int x = 10;
int y = 20;
swap(x, y);
System.out.println("x = " + x + ", y = " + y);
// Output: x = 10, y = 20 (Original values remain unchanged!)
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
}
Execution Stack Walkthrough
Case 2: Passing Reference Types
When passing an object reference, a copy of the memory address is passed.
class Car {
String model;
Car(String model) {
this.model = model;
}
}
public class ReferencePassTest {
public static void main(String[] args) {
Car myCar = new Car("Tesla");
changeModel(myCar);
System.out.println("Model: " + myCar.model); // Output: Model: BMW
}
public static void changeModel(Car c) {
c.model = "BMW"; // Mutates the object on the shared heap address
}
}
Why did myCar.model change if Java is Call by Value?
myCaron the main stack holds reference address0x100.- When calling
changeModel(myCar), address0x100is copied into local reference variablec. - Both
myCarandcpoint to the same object on the Heap (0x100). - Modifying
c.modelmutates the underlying heap object at0x100.
6. Object Mutation vs. Reference Reassignment
To unequivocally prove Java is Call by Value, consider what happens when you reassign a reference inside a method:
public class ReassignmentTest {
public static void main(String[] args) {
Car car = new Car("Toyota");
reassignCar(car);
System.out.println("Car model after method: " + car.model);
// Output: Car model after method: Toyota
}
public static void reassignCar(Car c) {
// Reassigning local copy 'c' to point to a brand new object on the heap
c = new Car("Ferrari");
c.model = "Lamborghini";
}
}
Detailed Flow Walkthrough
If Java were truly Call by Reference, reassigning c inside the method would have updated car in main() to point to Ferrari. Because only the reference value was copied, car in main() remains untouched.
7. Object Copying: Why Do We Need Cloning?
In Java, assign operator = on reference variables does not copy the object. It creates an alias.
Student s1 = new Student("John", 101);
Student s2 = s1; // NOT a copy! Both variables point to the same object.
s2.name = "Alex";
System.out.println(s1.name); // Prints "Alex" - Unintended side effect!
To create an independent duplicate of an object, we must perform explicit object copying.
8. Shallow Copy: Mechanism, Implementation & Risks
A Shallow Copy creates a new object instance, but copies primitive field values directly and copies reference pointers to child/nested objects.
Characteristics of Shallow Copy
- Primitives are copied by value.
- Nested objects/references are copied as pointer addresses (shared references).
- Mutating a nested object in the copy will mutate the original object.
Code Implementation (Shallow Copy using Cloneable)
class Address {
String city;
public Address(String city) {
this.city = city;
}
}
class Student implements Cloneable {
int id;
Address address;
public Student(int id, Address address) {
this.id = id;
this.address = address;
}
// Standard Shallow Copy using super.clone()
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class ShallowCopyDemo {
public static void main(String[] args) throws CloneNotSupportedException {
Address addr = new Address("New York");
Student s1 = new Student(1, addr);
// Performing Shallow Copy
Student s2 = (Student) s1.clone();
// Mutating nested object via s2
s2.address.city = "Los Angeles";
// Side effect observed in original s1!
System.out.println("s1 city: " + s1.address.city); // Output: Los Angeles
}
}
9. Deep Copy: Complete Decoupling & Best Practices
A Deep Copy creates a brand-new object instance and recursively creates brand-new duplicate instances of all nested objects in the object graph.
Characteristics of Deep Copy
- Primitives are copied by value.
- Nested objects are recursively duplicated into entirely new heap allocations.
- Original object and copied object are 100% decoupled and isolated.
Code Implementation 1: Manual Deep Copying via clone()
class Address implements Cloneable {
String city;
public Address(String city) {
this.city = city;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Student implements Cloneable {
int id;
Address address;
public Student(int id, Address address) {
this.id = id;
this.address = address;
}
// Custom Deep Copy Implementation
@Override
protected Object clone() throws CloneNotSupportedException {
// Step 1: Perform shallow clone of outer object
Student clonedStudent = (Student) super.clone();
// Step 2: Manually clone nested mutable objects
clonedStudent.address = (Address) this.address.clone();
return clonedStudent;
}
}
Code Implementation 2: Production Best Practice (Copy Constructors)
Java's native Cloneable interface and Object.clone() are widely considered flawed due to exception handling baggage and lack of constructor calls. Production applications prefer Copy Constructors.
class Address {
String city;
public Address(String city) {
this.city = city;
}
// Copy Constructor for Address
public Address(Address other) {
this.city = other.city;
}
}
class Student {
int id;
Address address;
public Student(int id, Address address) {
this.id = id;
this.address = address;
}
// Copy Constructor for Student (Deep Copy)
public Student(Student other) {
this.id = other.id;
// Deep copy nested object explicitly
this.address = new Address(other.address);
}
}
public class DeepCopyConstructorDemo {
public static void main(String[] args) {
Address addr = new Address("Chicago");
Student s1 = new Student(101, addr);
// Perform Deep Copy via Copy Constructor
Student s2 = new Student(s1);
// Mutate nested field in copied object
s2.address.city = "Miami";
// Verification: Original object is completely isolated!
System.out.println("s1 city: " + s1.address.city); // Output: Chicago
System.out.println("s2 city: " + s2.address.city); // Output: Miami
}
}
10. Comparative Synthesis
Call by Value vs. Call by Reference
Shallow Copy vs. Deep Copy
Summary and Key Takeaways
- Java Heap Structure: Every Java object consists of an Object Header (Mark Word + Klass Pointer), Instance Data, and optional Alignment Padding to align total size to an 8-byte boundary.
- Header Overhead: A standard object header on a 64-bit JVM with Compressed OOPs takes 12 bytes (8B Mark Word + 4B Compressed Klass Pointer).
- Strictly Call by Value: Java passes everything by value. For primitives, the raw data value is copied; for objects, the reference address is copied.
- The Reference Reassignment Test: Reassigning a parameter variable (
c = new Object()) inside a method has zero effect on the caller's reference variable because only a local copy of the address was modified. - Reference Aliasing: Simple assignment (
s2 = s1) creates a new reference pointing to the existing object on the heap, not a duplicate object. - Shallow Copy Mechanism:
super.clone()creates a new outer object, but leaves all nested references pointing to the shared original child objects. - Deep Copy Mechanism: Recursively copies outer objects and all nested object graphs to achieve complete memory isolation.
- Production Standard: Prefer using Copy Constructors or deep serialization libraries over Java's legacy
Cloneableinterface for safe, predictable deep copying.