Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Memory and garbage collection

In Java you don't free memory by hand, as in C: unneeded objects are removed by the garbage collector (GC). But it's still important to understand where data lives and when an object becomes "garbage" — otherwise a program can eat all the memory or crash with an error.

The stack and the heap

The JVM keeps data in two places. The stack is each thread's memory for method calls: it holds local variables and parameters, and it clears itself when a method finishes. The heap is shared memory for all objects created with new. A variable of a primitive type holds the value itself, while a variable of an object type holds only a reference to an object in the heap:

void main() {
    int a = 5;
    int b = a;
    b = 10;
    IO.println("a = " + a);
 
    StringBuilder first = new StringBuilder("Anna");
    StringBuilder second = first;
    second.append(" and Oleg");
    IO.println("first = " + first);
}

Output:

a = 5
first = Anna and Oleg

b = a copied the number, but second = first copied only the reference: both variables point to the same object in the heap, so a change made through second is visible through first too.

Garbage collection

An object is needed as long as it can be reached through references — from local variables, static fields, other live objects. When no references are left, the object becomes unreachable, and the garbage collector frees its memory. Here the program creates 10 GB of arrays, although the JVM is allowed only 256 MB (the -Xmx option), and -Xlog:gc prints every collection:

void main() {
    long total = 0;
    for (int i = 1; i <= 100_000; i++) {
        byte[] report = new byte[100_000];
        total += report.length;
    }
    IO.println("Created: " + total / 1_000_000 + " MB");
}

Run:

java -Xmx256m -Xlog:gc Main.java

Output (shortened; your times and numbers may differ):

[0.018s][info][gc] Using G1
[1.687s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 48M->4M(256M) 6.739ms
[1.744s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 115M->4M(256M) 3.839ms
...
[4.158s][info][gc] GC(65) Pause Young (Normal) (G1 Evacuation Pause) 156M->3M(256M) 0.767ms
Created: 10000 MB

After each iteration the report array becomes garbage. The entry 156M->3M means the heap held 156 MB before the collection and only 3 MB after it. The collection took less than a millisecond.

Memory leaks

Java has memory leaks too — when unneeded objects are still reachable, so the collector isn't allowed to remove them. Typical causes:

  • static collections: a static List or static Map that things are added to "just in case" and never removed from — a static field lives as long as the program runs;
  • listeners: an object subscribed to another object's events and forgot to unsubscribe — the event source holds a reference to the subscriber forever;
  • caches without a size limit and unclosed resources — files, database connections.

The cure is always the same: drop references when an object is no longer needed — remove elements from collections, unsubscribe listeners, limit the cache size.

StackOverflowError

Each thread's stack is small — usually about a megabyte. Every method call takes up space in it, so endless recursion quickly exhausts it:

int depth = 0;
 
void dive() {
    depth++;
    dive();
}
 
void main() {
    try {
        dive();
    } catch (StackOverflowError e) {
        IO.println("Caught: " + e);
        IO.println("Depth: " + depth);
    }
}

Output (the depth changes from run to run):

Caught: java.lang.StackOverflowError
Depth: 19288

StackOverflowError and OutOfMemoryError are subclasses of Error, not Exception: they are failures of the JVM itself, and real programs don't catch them but fix the cause.

Which collectors exist

The JDK has several garbage collectors. G1 runs by default, and it suits most programs. ZGC (the -XX:+UseZGC option) keeps pauses under a millisecond even with a huge heap; it's chosen for servers where fast response matters. For small programs there's the simple Serial collector. The memory setting changed most often is just the heap size: -Xmx is the maximum, -Xms the initial size.

Common mistake

A static cache that is only ever added to grows until memory runs out. Every report was saved "just in case" — and none of them can be removed:

static Map<Integer, byte[]> reportCache = new HashMap<>();
 
byte[] buildReport(int id) {
    byte[] report = new byte[10_000_000];
    reportCache.put(id, report);
    return report;
}
 
void main() {
    for (int id = 1; id <= 1000; id++) {
        buildReport(id);
    }
    IO.println("All reports built");
}

Run:

java -Xmx256m Main.java

Output:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at Main.buildReport(Main.java:4)
	at Main.main(Main.java:11)

Java heap space means there's no room left in the heap even after garbage collection. Raising -Xmx won't help — the cache will eat the new memory too. You need to remove old entries or limit the cache size, for example by keeping only the last hundred reports.

Thanks for reading!

Donate to the site — we work hard for you.