Does your old Java configuration fail to start on Java 17? The answer is in how the JVM uses memory.
In Java, each thread has its own stack for local variables. Objects live on the heap, the area the garbage collector cleans up. Class data lives in the Metaspace, outside the heap. The JVM also uses other memory areas that are easy to forget.
Three areas you need to know
The stack: one per thread
Each thread gets its own stack. Every method call adds a frame to it, with the method’s parameters and local variables. When the method returns, its frame is gone. The garbage collector is not needed to free this memory.
A local variable of an object type does not store the object itself. It stores a reference to an object on the heap.
If calls go too deep, for example with infinite recursion, the stack runs
out of space and you get a StackOverflowError. You can change the
stack size with -Xss. The default depends on the platform: 1 MB on
Linux x64, 2 MB on Linux and macOS on ARM (AArch64).
The heap: objects, and where the GC works
Every object you create with new goes on the heap, which all threads
share. This is where the garbage collector works: it finds the objects
that nothing points to anymore and frees their space.
You set the maximum heap size with -Xmx. Without it, the JVM usually
sets the maximum to 25 % of the memory. On a machine with 32 GB of
memory, that gives a heap of up to 8 GB.
Interned strings (String.intern()) and the static fields of classes
are on the heap too.
The Metaspace: classes, outside the heap
For each class it loads, the JVM keeps a description of the class: its fields, its methods… This metadata lives in the Metaspace, in native memory, outside the heap.
By default, the Metaspace has no size limit: it grows as more classes
are loaded. You can set a limit with -XX:MaxMetaspaceSize.
What people often forget
The heap and the Metaspace are not the whole story. The JVM also uses native memory for:
- thread stacks: one per thread, so more threads means more memory;
- the code cache: the machine code produced by the JIT compiler, up to 240 MB by default;
- direct memory: buffers allocated outside the heap with
ByteBuffer.allocateDirect(). If you do not set-XX:MaxDirectMemorySize, the JVM picks the limit; - internal data of the garbage collector, the compiler and more.
What has changed
Say a developer reuses an old JVM configuration, copied from an old project or a forum:
java -Xms512m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=256m -jar app.jar
On Java 8, the application starts. The JVM just prints two warnings that are easy to miss in the logs:
OpenJDK 64-Bit Server VM warning: ignoring option PermSize=128m; support was removed in 8.0
OpenJDK 64-Bit Server VM warning: ignoring option MaxPermSize=256m; support was removed in 8.0
On Java 17, 21 or 25, it does not start at all:
Unrecognized VM option 'PermSize=128m'
Error: Could not create the Java Virtual Machine.
And the JVM stops at the first unknown option: you remove PermSize, run
it again, and now MaxPermSize fails.
Why? PermGen was removed in Java 8.
| What you still read online | What is true today | Since |
|---|---|---|
| Classes live in PermGen, an area with a fixed size | Their metadata lives in the Metaspace, in native memory, with no limit by default | Java 8 |
You tune PermGen with -XX:PermSize and -XX:MaxPermSize | These options were ignored with a warning, then rejected: the JVM does not start. Use -XX:MetaspaceSize and -XX:MaxMetaspaceSize instead | Ignored up to Java 16, rejected since Java 17 |
OutOfMemoryError: PermGen space | PermGen is gone, and so is this error | Java 8 |
| Interned strings and static fields live in PermGen | They live on the heap | Java 8 at the latest |
Sources: JEP 122, Remove the Permanent Generation,
and the JDK 17 documentation for the java command
(section “Removed Java Options”).
Two common mistakes
“Everything is on the heap”
The heap holds objects, plus interned strings and static fields.
Classes, compiled code, thread stacks and direct buffers live elsewhere,
in native memory. -Xmx does not limit them.
“-Xmx is all the memory my application uses”
-Xmx only limits the heap. The Java process always uses more than
that. For example, with Java 25 and -Xmx512m, the JVM already reserves
about 598 MB of usable memory (“committed” in Native Memory Tracking),
just to print its version. When you size a machine or a container,
count the whole process, not just the heap.
Summary
Three areas to remember: one stack per thread, the heap for objects and
the Metaspace for classes. -Xmx only limits the heap, and the heap is
only part of the process memory. If an old configuration still mentions
PermGen, it is time to clean it up.