Skip to content
academia.sh

Lesson 02 / 11

Memory Areas

Three of six values written by the same thread are never visible to another thread, and three are — what decides this is not the value's type but where it sits: a local variable and a parameter sit in the call frame, an object field and an array element sit on the heap, a static field sits in the class's own storage. Being shared is not being visible: without an ordering tool, the same read is undefined.

Published

In this lesson

  • Distinguishes, by running it, whether a value sits in the call frame, on the heap, or in the class's own storage
  • Shows by counting how many of six values are visible outside the writer thread
  • Explains that being shared does not mean being visible, and why the same read cannot be measured without an ordering tool
Contents

The previous lesson measured whose hands a class enters its runtime through: which loader defines it, which thread initializes it. But it never asked where the fields of an initialized class actually sit, or whether a value one thread writes can be seen by another. This lesson asks that question: how many threads can see a given value?

The answer cannot be given by looking at the value’s type — the same int never leaves the writing thread in one place, yet stands open to every thread in another. What makes the difference is where the value sits.

The importance of this distinction shows up in an ordinary situation: two threads calling the same method at the same time. Even if two threads call writeParam(5, ...) at exactly the same moment, each keeps its own p parameter in its own frame; neither can read the other’s p in any way, because the two frames do not sit in separate address spaces but in separate frame chains. This is not an isolation the programmer sets up by doing something — it is provided by the language itself, with no lock and no keyword required. Heap sharing works in exactly the opposite direction: an object being shared on the heap depends on that object’s reference having been handed to another thread; if the reference is never shared, an object on the heap can remain visible to only a single thread. Call-frame isolation is structural; heap sharing is a design decision.

Three Places: The Call Frame, the Heap, and the Class’s Storage

This topic’s catalog subtitle names the memory areas “the stack, the heap, and metaspace.” On this platform, heap is also the name of a data structure (a priority tree) elsewhere in the curriculum, so it is worth being explicit here: everywhere in this lesson, and from here on, heap names the shared memory area, never the priority-tree structure. All three areas are named among the JVM specification’s own runtime data areas: each thread has its own stack, every thread shares a single heap, and every thread also shares a single method area. The specification does not say how these three areas are implemented — only which data belongs to which area; this lesson looks only at that distinction, not at the implementation.

The runtime places a value into one of three separate areas. When a method is called, that thread’s own call frame opens, and the method’s local variables and parameters are written into it; every thread has its own frame chain, and none can reach another’s. A frame also closes when its call ends: if a method calls itself recursively, several frames of the same method stack up in the same thread, but none of them mixes into another thread’s frame chain. When an object is created with new, its fields are written to the heap, and when an array is created, its elements are too; the heap is a single area shared by every thread, and whichever thread a reference to an object is handed to, that thread reaches the same fields. A class’s own static fields, by contrast, sit apart from any instance, in an area belonging to the class itself — the JVM specification’s method area, known by its current name as metaspace; this area, too, is single and shared among every thread. A static field does not need even a single instance of the class to exist — its slot in metaspace opens while the class is being prepared, before initialization has even run, and it is independent of the number of instances.

ThreadLocal is a fourth pattern: the value it holds physically sits on the heap, but every thread reaches its own record — the access path runs through the thread itself. Because of this, even though it is the heap as a class, it behaves like a value in a call frame. Their lifetimes are not the same, though: a local variable disappears once its own frame closes, carrying no trace into the next call. A ThreadLocal record, by contrast, stays alive as long as the thread does — when the same thread calls the same method a second time, its record is still there, and it reads whatever value was left from the previous call unless it has been overwritten. Both fall into the “alone” column, because neither is visible to another thread; but one is born with the call and dies with it, while the other lives as long as the thread does.

An array element sitting on the heap follows the same rule: as the Java Fundamentals course established, an array is itself an object, so an array’s elements sit on the heap just like an object’s fields do, and whichever thread the same reference reaches, that thread reaches the same elements.

  • JV8 — A value counts as “shared” only when it is read after the writer thread is waited for to finish (through join); a value read before that does not enter this measurement.
  • JV9 — For a local variable and a method parameter, the “alone” result does not come from an observation but from an impossibility: there is no address at which another thread could read these two values, so not even a call could be written to try.
  • JV10 — The six measured values are all written by the same writer thread, in the same run, in the same order; none of them comes from a separate run.
  • JV11 — No environment-dependent data is ever read: no stack depth, no heap size, no address is printed; identity is read only through value equality (== 5).

Three of Six Values Are Alone, Three Are Shared

The measurement writes an object (field and array on the heap), writes a class’s static field (in metaspace), writes a ThreadLocal, and writes a local variable and a method parameter belonging to the same thread. All of this happens in a single writer thread; the read happens from the main thread, after that thread has finished.

// VisibilityBox.java — carries an object field and an array element
class VisibilityBox {
    int field = 0;
    final int[] array = { 0 };
}
// VisibleValues.java — six values in four places: how many are visible outside the writer thread
import java.util.concurrent.CountDownLatch;

public class VisibleValues {
    static int staticField = 0;
    static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    static int aloneCount = 0, sharedCount = 0;

    static void writeParam(int p, int[] target) { target[0] = p; }

    static void seenBy(String label, boolean isShared) {
        if (isShared) sharedCount++; else aloneCount++;
        System.out.printf("%-26s %s%n", label, isShared ? "shared" : "alone");
    }

    public static void main(String[] args) throws InterruptedException {
        VisibilityBox box = new VisibilityBox();
        CountDownLatch written = new CountDownLatch(1);
        int[] paramSeen = { -1 };

        Thread writer = new Thread(() -> {
            int local = 5;
            threadLocal.set(5);
            staticField = 5;
            box.field = 5;
            box.array[0] = 5;
            writeParam(5, paramSeen);
            if (local != 5) throw new IllegalStateException();
            written.countDown();
        });
        writer.start();
        written.await();
        writer.join();

        // local variable and parameter: there is no address at which another thread
        // could read them, so not even a call could be written to try.
        seenBy("local variable", false);
        seenBy("method parameter", false);
        seenBy("thread-local value", threadLocal.get() == 5);
        seenBy("object field", box.field == 5);
        seenBy("array element", box.array[0] == 5);
        seenBy("static field", staticField == 5);
        System.out.printf("visibility: %d alone, %d shared%n", aloneCount, sharedCount);
    }
}
local variable             alone
method parameter           alone
thread-local value         alone
object field               shared
array element              shared
static field               shared
visibility: 3 alone, 3 shared

Three of the six values never left the writer thread at all: three did. The first two lines needed no measurement at all: local and writeParam‘s parameter live in the writer thread’s own frame and vanish with it when it dies; there is no variable name, no field, no method through which the main thread could read them. The if (local != 5) throw line in the code confirms this: local itself can only be used inside its own frame as a check, never carried outside the frame in any form. The writeParam call, on the other hand, did carry the parameter value to the main thread — by copying it into a shared array called paramSeen — but what got carried was not the parameter itself, its copy; the measurement never reads this copy, because the question asked is not “can the value be copied somewhere else” but “is the place the parameter sits visible to another thread,” and the answer is no.

The third line needed a run, and its result pointed the same way: threadLocal.get() called on the main thread reads its own ThreadLocal record, not the writer thread’s; the two threads have two separate values under the same name, and 5 == 0 comes out false. The last three lines all gave true: box.field, box.array[0], and staticField. The object sits in a single place on the heap, and both threads carry the same reference; the static field sits in a single place in the class’s own storage, and both threads look at the same class.

The Difference Between an Object’s Own Field and the Class’s Storage

The previous measurement used a single VisibilityBox object; “sitting in one place on the heap” and “sitting in one place in the class’s own storage” can look like the same thing. The difference appears once two objects are created.

// TwoInstances.java — an object's own field is separate per instance, the class's storage is one place
public class TwoInstances {
    public static void main(String[] args) {
        VisibilityBox first = new VisibilityBox();
        VisibilityBox second = new VisibilityBox();
        first.field = 1;
        second.field = 2;
        VisibleValues.staticField = 99;

        System.out.println("first.field                  : " + first.field);
        System.out.println("second.field                 : " + second.field);
        System.out.println("are the two fields the same  : " + (first.field == second.field));
        System.out.println("two instances exist, staticField is still one: " + VisibleValues.staticField);
    }
}
first.field                  : 1
second.field                 : 2
are the two fields the same  : false
two instances exist, staticField is still one: 99

When two VisibilityBox objects were created, the field field also produced two separate copies; one held 1, the other 2, and the two are not equal. field takes up one slot on the heap per object — as many copies as objects created. staticField, by contrast, is the VisibleValues class’s own field and is independent of the number of objects: it would exist even with zero objects created, and it stays single even once two objects exist. Of the three lines counted as “shared” in the previous measurement, two (the object field, the array element) got their sharing from carrying a reference to the same object; the third (the static field) got its sharing from the class itself. Both are shared because each sits in a single place — on the heap or in metaspace — but that “single place” depends on a different thing in each case: on the object’s identity in one, on the class’s identity in the other.

This difference has a direct consequence when designing a new class: code that wants to make a field deliberately shared declares it static — sharing is then established regardless of how many objects exist; code that wants to keep a field tied only to its own object leaves it as an instance field and never hands the object to a second thread. In neither case does the JVM itself make a choice; whether a field is static, and whether a reference is shared, are both decisions written into the source code.

A Bounding Measurement: Being Shared Is Not Being Visible

The three “shared” lines above were obtained through a conditional read: the writer.join() call placed an order between the write and the read, and the read only happened once the write had definitely finished. Without that order — say, if the main thread read staticField right after writer.start(), without calling join — what the same three lines would give is undefined: the old value the writer thread had not yet written (0) might be seen, or the new value (5) might be seen. Writing a single int is atomic in Java, so the value read is never a half-written mess; the real issue is not atomicity but ordering — the compiler and the processor are allowed to reorder instructions as long as they do not break observable single-threaded behavior, and this reordering can leave it undecided which write counts as happening before which read, between two threads. This is not a number to be measured and reported, but a state the language leaves undefined; the lesson therefore never runs such a read, it only names that it exists — this course’s rule is to model a race condition, not to run one.

The static field counted as “shared” because it sits in a single place on the heap and both threads look at the same address — but sitting in a single place is not a guarantee that the most recent value at that place will be seen. “Sitting in the same place” is a fact about location, settled at compile time; “being visible” is a fact about ordering, and it is established only at runtime, depending on whatever ordering tool sits between the threads. Which rule visibility depends on — which write counts as happening before which read, how tools like volatile and locks guarantee that order — is the subject of the Java Memory Model lesson in the Concurrency topic; this lesson only shows that visibility has to be built from zero, and that without an ordering tool like join it is zero.

Stack depth and heap size are not measured in this lesson: both are environment-dependent data that change with the machine they run on. The only thing measured is how many threads a value is visible to — location information (which of the four places) is settled at compile time and does not change from run to run; visibility information (whether the most recent value at that location can be read) is only meaningful together with a correct ordering tool, and this lesson measures it only in the case where that tool exists.

Summary

  • A value sits in one of three places: a local variable and a parameter in the call frame, an object field and an array element on the heap, a static field in the class’s own storage (metaspace); ThreadLocal sits on the heap but is reached only through the thread itself.
  • On this platform, heap is the name used for the memory area; the priority-tree data structure that shares the same English name is a separate concept elsewhere in the curriculum.
  • Three of the six measured values (local variable, parameter, thread-local value) never leave the writer thread; three (object field, array element, static field) do.
  • For a local variable and a parameter, the “alone” result is not an observation but an impossibility: there is no address at which another thread could read them.
  • Being shared is not being visible: without an ordering tool like join, the result of the same read is undefined; that is why the lesson never runs an unordered read.
  • Stack depth and heap size are environment-dependent and are not measured.

Next Step

This lesson assumed that values sitting on the heap were still reachable — the box variable was always kept in hand, no reference was ever dropped, and first and second stayed alive in main’s own frame until the last line. But the last reference to an object can also be lost; the program drops an object, no frame and no static field can reach it anymore, and what remains on the heap is a field nobody can read. The next lesson measures that situation: which thread takes on the cleanup of an object that has lost its reachability — not the caller, not a thread the program created, but a party that never appears in any record at all.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close