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

Memory of a Java process: thread stacks, the heap, the Metaspace and other native memory areasJava processeverything outside the heap is native memoryStacksone per thread ·-XssThread mainprocess() · clientmain()Thread 2read()run()frames, local variablesHeapshared · managed by the GC ·-XmxClientStringListbyte[]objects created withnewinterned stringsstatic fieldsMetaspacenative memoryno limit by defaultClientfields, methods…Stringfields, methods…class descriptions-XX:MaxMetaspaceSizeAlso in native memoryCode cachemachine code from the JITup to 240 MB by defaultDirect memoryByteBuffer.allocateDirect()-XX:MaxDirectMemorySizeInternal datagarbage collector,compiler and more

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 onlineWhat is true todaySince
Classes live in PermGen, an area with a fixed sizeTheir metadata lives in the Metaspace, in native memory, with no limit by defaultJava 8
You tune PermGen with -XX:PermSize and -XX:MaxPermSizeThese options were ignored with a warning, then rejected: the JVM does not start. Use -XX:MetaspaceSize and -XX:MaxMetaspaceSize insteadIgnored up to Java 16, rejected since Java 17
OutOfMemoryError: PermGen spacePermGen is gone, and so is this errorJava 8
Interned strings and static fields live in PermGenThey live on the heapJava 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.

Further reading