Layers of Logic

6.3

Objects in Memory: Size, Copying, Passing

Reference values explain aliasing, argument passing, copying, and why measured object size depends on the JVM.

Going deeper22 min read4 exercises
01

Previously on

Section 6.1 introduced opaque references. Section 6.2 used them in fields and constructor parameters.

Back in Section 4.1, b = a gave two variables access to one array. Objects follow the same rule.

This section carries that rule through method calls, copies, and measured memory layouts.

02

The problem

Three questions expose three common wrong models.

Student b = a;

Did that copy the Student?

static void birthday(Student s) {
    s.age++;
}

If Java passes by value, why can this method change the caller’s object?

class Student {
    String name;
    int age;
    int rollNumber;
    Address address;
}

Can four field widths tell you this object’s total size on every JVM?

All three answers depend on separating a variable, a reference value, and the object reached by that value.

03

The idea

A variable of class type holds one of two things:

  • null
  • an opaque reference to a compatible object

The reference is a value. Java may copy it into another variable, field, array element, or method parameter.

Copying the reference does not copy the object.

Student a = new Student("Aditya", 28);
Student b = a;

b.age = 29;
System.out.println(a.age);
29

a and b are aliases. Both references reach one Student object.

04

Under the hood

Going deeper

Passing arguments

Java copies every argument value into a new parameter variable.

class Student {
    String name;
    int age;

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class PassingDemo {
    static void edit(Student s) {
        s.age = 44;
        s = new Student("Ghost", 99);
        s.name = "Changed ghost";
    }

    public static void main(String[] args) {
        Student original = new Student("Cipher", 30);
        edit(original);
        System.out.println(original.name + " / " + original.age);
    }
}
Cipher / 44

The field write reaches the original object. Reassigning s changes only the parameter variable.

While edit is running

edit

Student s@283creassigned copy

main

Student original@1a2bcaller variable

Heap

Student@1a2b

name"Cipher"
age44

Student@283c

name"Changed ghost"
age99
The parameter first copied original's reference. Reassignment later changed only the parameter slot.

Under pass-by-reference, s = new Student(...) would replace the caller’s variable. Java provides no such parameter mode.

Java always passes by value. When an argument value is a reference, Java copies that reference value.

Assignment, shallow copy, and deep copy

Assignment creates an alias.

Student alias = original;       // zero new Student objects

A shallow copy creates a new outer object and copies each field value.

class Address {
    String city;

    Address(String city) {
        this.city = city;
    }
}

class Student {
    String name;
    Address address;

    Student(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    Student(Student source) {
        this.name = source.name;
        this.address = source.address;       // shallow: same Address
    }
}
Student a = new Student("Aditya", new Address("Guwahati"));
Student b = new Student(a);
b.name = "Rohit";
b.address.city = "Delhi";

System.out.println(a.name + " / " + a.address.city);
System.out.println(b.name + " / " + b.address.city);
Aditya / Delhi
Rohit / Delhi

There are two Student objects and one Address. The copied address reference makes the nested object shared.

Stack

main

Student a@1a2b
Student b@283c

Heap

Student a@1a2b

name"Aditya"
address@364d

Student b@283c

name"Rohit"
address@364d

Address@364d

city"Delhi"
A shallow copy duplicates the Student but shares the mutable Address.

A deeper copy creates another Address.

Student deepCopy() {
    Address copiedAddress = address == null ? null : new Address(address.city);
    return new Student(name, copiedAddress);
}

Strings may remain shared because String objects are immutable. Sharing a mutable object needs a deliberate contract.

OperationResult
Student b = acopies one referenceone Student, two aliases
new Student(a)copies field valuesnew Student, nested references shared
a.deepCopy()copies selected nested objectsindependent mutable state

Object size is implementation-specific

The Java language specification does not assign byte sizes to ordinary objects. It does not fix field order, headers, padding, or reference encoding.

A common 64-bit HotSpot configuration has these settings:

  • 8-byte mark word
  • 4-byte compressed class pointer
  • 4-byte compressed ordinary object references
  • 8-byte object alignment

Under that configuration, this Student often has a 32-byte shallow size.

Part in one sample layoutBytes
Header8-byte mark + 4-byte class pointer12
Two int fields4 + 48
Two reference fields4 + 48
Alignment gapconfiguration rounds 28 to 324
Sample shallow size32

That calculation is a prediction for one named configuration. It is not a Java rule.

Compressed references may turn off for some heap sizes or JVM options. Compact object headers can change the header. Another JVM may reorder fields or align objects differently.

Measure with JOL

The OpenJDK Java Object Layout tool reads the active VM’s layout rules. Add JOL to the classpath, then run code like this:

import org.openjdk.jol.info.ClassLayout;

public class LayoutDemo {
    static class Student {
        String name;
        int age;
        int rollNumber;
        String college;
    }

    public static void main(String[] args) {
        Student s = new Student();
        System.out.println(ClassLayout.parseInstance(s).toPrintable());
    }
}

On one HotSpot run with compressed references and 8-byte alignment, the final lines can look like this:

Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Your output is the evidence for your JVM. Record the JDK, collector, heap options, and JOL output with any reported number.

JOL’s class layout reports shallow size. The String and Address objects reached by fields are separate allocations.

Tools that compare totalMemory() and freeMemory() around allocations measure heap usage with noise. Garbage collection, thread-local buffers, and heap resizing affect those results.

05

What it costs

Aliasing avoids copying large objects on every assignment and method call. The copied value is one reference, regardless of the object’s graph size.

The cost is shared mutation. A field change through one alias becomes visible through every alias that reaches the same object.

Shallow copies allocate less and preserve intended sharing. They also leak nested mutable state when isolation was expected.

Deep copies allocate more, take time, and need a graph policy. Defensive copies use that cost at boundaries where ownership matters.

Small objects can spend more bytes on headers and alignment than on data. Measure the target JVM before turning a sample layout into a production claim.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. Is Java call by value or call by reference? Prove the answer with object parameters.

    Show the answer

    Java is always pass-by-value. Calling a method copies the value from each argument expression into its parameter variable.

    For a reference variable, that value is an opaque reference. The copied reference reaches the same object, so field changes are visible to the caller.

    Reassign the parameter to a new object and the caller's variable stays unchanged. A method receiving the caller's variable by reference could replace that variable. Java cannot, which proves the rule.

  2. Does `Student b = a;` make a shallow copy?

    Show the answer

    No object is copied. The assignment copies one reference value, so a and b become aliases for the same Student.

    A shallow copy requires a new outer Student object. Its primitive fields are copied, while its reference fields still reach the same nested objects.

  3. Can you calculate every Student object's Java size from its field declarations alone?

    Show the answer

    No. The Java language does not fix object headers, reference widths, field order, alignment, or padding.

    A common 64-bit HotSpot setup uses a 12-byte header, 4-byte compressed references, and 8-byte alignment. Other JVMs or options can differ.

    Measure the running configuration with a layout tool such as JOL. Also decide whether you need shallow size or the size of the reachable object graph.

07

Exercises

Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.

4 exercises110 pointsabout 115 minutes

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Prove Java Is Call By Value

Real work·25 min·25 points

checkedex-6-3-a

Four experiments. Together they settle the question completely.

Predict all four before running, and draw the stack for each one. Two frames per call: the caller’s and the method’s, with an arrow for every reference.

changeField and replaceObject are the pair that matters. One reaches the caller’s data and the other does not, and both received exactly the same thing.

Then look hard at swap. It is the cleanest disproof of call by reference there is. In a language that really passed by reference, that method would work. Write down why it cannot work in Java, then write a version that does, and notice what you had to change to make it possible.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class ByValue {
    static int numberAfterCall()
    static int fieldAfterCall()
    static String nameAfterReplace()
    static String afterSwap()
    static String whatIsCopied()
}

Keep the four helper methods (changeNumber, changeField, replaceObject, swap) exactly as written. Each reporting method calls one of them and returns what the CALLER sees afterwards. whatIsCopied is one short phrase naming the single rule.

What your program must do

  • Predict all four results before running
  • Explain why changing a field works and replacing the object does not
  • Show that a swap method cannot work in Java
  • State the one rule that explains all four
ByValue.java
public class ByValue {

    static class Unit {
        String name;
        int readiness;
        Unit(String name, int readiness) { this.name = name; this.readiness = readiness; }
        @Override public String toString() { return name + "/" + readiness; }
    }

    // Leave these four exactly as they are.
    static void changeNumber(int n)   { n = 999; }
    static void changeField(Unit u)   { u.readiness = 999; }
    static void replaceObject(Unit u) { u = new Unit("Ghost", 0); }
    static void swap(Unit a, Unit b)  { Unit t = a; a = b; b = t; }

    // Call each one and report what the CALLER sees afterwards.
    // PREDICT all four before running.
    static int    numberAfterCall()   { return -1; }  // TODO
    static int    fieldAfterCall()    { return -1; }  // TODO
    static String nameAfterReplace()  { return ""; }  // TODO
    static String afterSwap()         { return ""; }  // TODO

    // One short phrase: what does Java copy when it passes an argument?
    static String whatIsCopied() { return ""; }  // TODO

    public static void main(String[] args) {
        // TODO: run all four and check them against your predictions
    }
}
Hint 1
Java always copies the value in the variable. For an int that value is a number. For an object variable it is an opaque reference value.
Hint 2
A copied reference still reaches the same object, which is why changeField reaches the caller's unit.
Hint 3almost the answer
replaceObject and swap both repoint the method's OWN copies. The caller's variables were never touched, which is why neither has any effect outside.
What this is really testing

Whether you can design the experiment that settles the argument. Anyone can repeat the answer. Building the test that proves it is what makes it yours.

B

Shallow, Then Deep

Real work·30 min·25 points

checkedex-6-3-b

Three fields. Clone the object and change all three through the clone. Two of the changes leak back into the original and one does not.

Work out which is which before running, then check.

The field that does not leak is the interesting one. Nothing was copied there either, so why is it safe? The answer is a property you will build deliberately in Section 8.2.

Then write deepCopy so that nothing leaks at all.

Finish with the design question: how deep does this class promise to copy? If Address held a mutable Country, decide whether that object also needs a copy.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Copying {
    static Unit sample()
    static boolean shallowSharesAddress()
    static boolean shallowSharesArray()
    static boolean shallowSharesName()
    static boolean deepSharesAddress()
    static boolean deepSharesArray()
}

Unit is a nested class with a shallowCopy() and a deepCopy() method. sample() returns Atlas, based in Pune, with history 1 2 3. Each reporting method copies a sample, changes something through the COPY, and reports whether the original changed too.

What your program must do

  • Write a shallow copy and a deep copy of the same class
  • Show what the shallow copy shares and what it does not
  • Show the deep copy sharing nothing mutable
  • Say why the String field needs no special treatment
Copying.java
import java.util.Arrays;

public class Copying {

    static class Address {
        String city;
        Address(String city) { this.city = city; }
        @Override public String toString() { return city; }
    }

    static class Unit {
        String name;
        Address base;
        int[] history;

        Unit(String name, Address base, int[] history) {
            this.name = name; this.base = base; this.history = history;
        }

        // TODO: copy the fields one level. References get shared.
        Unit shallowCopy() { return null; }

        // TODO: give every mutable thing its own copy.
        Unit deepCopy() { return null; }
    }

    static Unit sample() { return null; }  // TODO: Atlas, Pune, {1, 2, 3}

    // Copy, change something through the COPY, report on the ORIGINAL.
    static boolean shallowSharesAddress() { return false; }  // TODO
    static boolean shallowSharesArray()   { return false; }  // TODO
    static boolean shallowSharesName()    { return false; }  // TODO
    static boolean deepSharesAddress()    { return false; }  // TODO
    static boolean deepSharesArray()      { return false; }  // TODO

    public static void main(String[] args) {
        // TODO: predict all five before running
    }
}
Hint 1
A shallow copy copies each field's value. For a reference field, that value is a reference, so both objects reach one Address.
Hint 2
Repointing the copy's own name field does not affect the original. The sharing is one level down, at what the fields point at, not at the fields themselves.
Hint 3almost the answer
A String cannot change, so sharing one is never a problem. Only mutable fields need the deep treatment, which is why immutability makes copying easy.
What this is really testing

Whether you can write a deep copy and say where "deep enough" stops. The shallow bug is silent, and only shows up when somebody edits a shared object much later.

C

Count Bytes Under One Layout Model

Hard·30 min·30 points

checkedex-6-3-c

Predict six sizes under the supplied model. Keep the assumptions beside every answer.

These are exercise rules, not Java rules. A JVM may change the header, field order, reference width, and alignment.

Two of these are more interesting than they look. Empty has no fields and is not zero bytes. And padding means that adding a field sometimes costs nothing at all, which is worth knowing when you are trying to make a data structure smaller.

Use JOL to inspect a real layout. A heap-difference experiment includes arrays, allocation buffers, collection timing, and other noise.

Phase X uses the same discipline: name the configuration, measure it, and avoid presenting one HotSpot result as a language guarantee.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Sizes {
    static int roundUp(int bytes)
    static int sizeOfField(String type)
    static int sizeOf(String className)
    static double dataShare(String className)
}

Use this hypothetical layout model for the tested arithmetic: header 12 bytes, reference 4, int 4, long 8, double 8, boolean field 1, char 2, and 8-byte alignment. Real JVM field packing can differ. sizeOf takes a class name as text.

What your program must do

  • Work out all six model sizes on paper before writing code
  • Find the class where adding a field costs nothing
  • Say how many bytes Empty takes under the model
  • Explain which model assumptions require measurement on a real JVM
Sizes.java
public class Sizes {

    // HYPOTHETICAL MODEL FOR THIS EXERCISE:
    // header 12, reference 4, int 4, long 8, double 8, boolean field 1,
    // char 2, object alignment 8. A real JVM may lay these classes out differently.

    static final int HEADER = 12;

    static class Empty { }
    static class OneInt { int a; }
    static class TwoInts { int a, b; }
    static class OneLong { long a; }
    static class ThreeRefs { String a, b, c; }
    static class Mixed { long a; int b; boolean c; String d; }

    static int roundUp(int bytes) { return 0; }  // TODO
    static int sizeOfField(String type) { return -1; }  // TODO
    static int sizeOf(String className) { return -1; }  // TODO

    // What fraction of the object is your data rather than bookkeeping?
    static double dataShare(String className) { return 0; }  // TODO

    public static void main(String[] args) {
        // TODO: print the size of all six and check them against your paper working
    }
}
Hint 1
Under this model, Empty is a 12-byte header rounded to 16. Check the active JVM with JOL before reporting that value as a measured layout.
Hint 2
Adding the first int costs nothing, because 12 + 4 is exactly 16 and the padding was already being paid for. The second int is the one that pushes you to 24.
Hint 3almost the answer
Under the model, three quarters of OneInt is header. Another header scheme or alignment changes that fraction.
What this is really testing

Whether you can calculate layouts under explicit assumptions without turning those assumptions into Java language rules. The tested model is one common HotSpot configuration.

D

Close Both Reference Leaks

Hard·30 min·30 points·The Registry

checkedex-6-3-d

A class with private fields can still expose a mutable array through shared references.

Run it and watch. Then explain each leak precisely, in terms of what value crossed the boundary of the class.

Outside code cannot write alpha.memberIds directly. It can still mutate the same array after the constructor stores the caller’s reference or the getter returns the field’s reference.

Fix both with defensive copies. Then ask whether the name field needs the same protection, and be able to say why not.

Section 8.2 uses the same two boundary copies while building an immutable class.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Leak {
    static boolean leaksThroughConstructor()
    static boolean leaksThroughGetter()
    static boolean safeThroughConstructor()
    static boolean safeThroughGetter()
    static int copiesNeeded()
}

LeakySquad keeps the array it is given and hands it straight back. SafeSquad copies in the constructor and copies in the getter. Each reporting method tries to change the private array from outside and says whether it worked.

What your program must do

  • Change a private field from outside, through the constructor
  • Change it again through the getter
  • Close both holes and confirm neither works any more
  • Say why private did not protect anything here
Leak.java
import java.util.Arrays;

public class Leak {

    // Both ends are open. Leave this one as it is.
    static class LeakySquad {
        private String name;
        private int[] memberIds;
        LeakySquad(String name, int[] memberIds) { this.name = name; this.memberIds = memberIds; }
        int[] getMemberIds() { return memberIds; }
        int size() { return memberIds.length; }
    }

    // TODO: the same class with both holes closed
    static class SafeSquad {
        private final String name;
        private final int[] memberIds;
        SafeSquad(String name, int[] memberIds) { this.name = name; this.memberIds = memberIds; }
        int[] getMemberIds() { return memberIds; }
        int size() { return memberIds.length; }
    }

    // Try to change the PRIVATE array from outside. Did it work?
    static boolean leaksThroughConstructor() { return false; }  // TODO
    static boolean leaksThroughGetter()      { return false; }  // TODO
    static boolean safeThroughConstructor()  { return true; }   // TODO
    static boolean safeThroughGetter()       { return true; }   // TODO

    // How many copies does closing this properly take?
    static int copiesNeeded() { return 1; }  // TODO

    public static void main(String[] args) {
        // TODO: change a private field from outside, twice, two different ways
    }
}
Hint 1
private restricts direct access to the field. It does not prevent mutation through another reference to the same array object.
Hint 2
The constructor kept the caller's array, so the caller still has a reference to it. The getter handed the same array back to anyone who asked.
Hint 3almost the answer
Both ends need a copy, and fixing one leaves the other open. This is why Section 8.2 on immutability spends so much time on defensive copying.
What this is really testing

Whether you can identify a mutable reference crossing a class boundary and apply defensive copying on both input and output.

08

After the credits

Section 8.2 uses defensive copies when mutable references cross an immutable object’s boundary.

Section 8.3 gives == and equals() separate jobs. == asks whether two references identify the same object. equals() can compare state.

Phase X returns to memory layout with boxed values and collections. An Integer[] includes an array of references plus separate Integer objects.

Mutable keys combine aliasing with equality. Change a key’s equality state after insertion and a hash table may search the wrong bucket.

Threads you opened in this section

Java is always call by value will return in 8.2 - Immutable Classes