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 Olegb = 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.javaOutput (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 MBAfter 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 Listorstatic Mapthat 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: 19288StackOverflowError 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.javaOutput:
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.