You take an old startup script, you move to Java 25, and the JVM does not start anymore:
Unrecognized VM option 'PrintGCDateStamps'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
Many memory and GC options that you still find on the web have changed or are gone. Some are just ignored, others stop the JVM at startup. And the options that are useful today fit in a few lines.
The options that matter
| Option | What it does | When to change it | When to leave it alone |
|---|---|---|---|
-Xms, -Xmx | Initial and maximum heap size | Almost always on a server: the default is often too small | — |
-Xss | Stack size of each thread | After a StackOverflowError in a recursion that is really needed | Otherwise: the default is enough |
-XX:+UseG1GC, -XX:+UseParallelGC, -XX:+UseZGC | GC choice | When you have a clear need (latency, throughput) | By default: let the JVM choose first |
-XX:MaxGCPauseMillis | Pause time goal (G1: 200 ms by default) | If G1 pauses are too long for you | To make the GC “faster”: shorter pauses can cost throughput |
-Xlog:gc* | GC logs | Always useful in production, to a file | — |
-XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath | Heap dump when the heap is full | In production, to understand an OutOfMemoryError | — |
-XX:MaxRAMPercentage | Maximum heap as a percentage of memory (25% by default) | In a container, instead of a fixed -Xmx | — |
Heap size: -Xms and -Xmx
-Xms sets the initial heap size, -Xmx the maximum size. Without
them, the JVM starts with 1/64 of the physical memory and can grow up to
1/4. On my machine (32 GB), that is 512 MB and 8 GB.
On a server, both often get the same value. Oracle’s guide explains why: it is more predictable, because the JVM no longer has to decide the heap size. But the other side is that if the value is wrong, the JVM cannot fix it.
-Xss sets the stack size of each thread: 1 MB by default on Linux and
macOS on x64, 2 MB on Aarch64 (Apple Silicon Macs, for example).
Pauses, logs and heap dumps
-XX:MaxGCPauseMillis is a goal, not a promise. The documentation calls
it a “soft goal”: the JVM does its best, but it may miss it. To reach a
shorter goal, it can run the GC more often, and then the application
does less useful work.
-Xlog:gc* writes the GC logs. To send them to a file:
-Xlog:gc*:file=gc.log. Rotation is built in: by default, 5 files of
about 20 MB, which you can change with filecount and filesize.
-XX:+HeapDumpOnOutOfMemoryError writes an .hprof file when the heap
is full. It is off by default. Without -XX:HeapDumpPath, the file goes
to the current directory, named java_pid<pid>.hprof. Be careful: it
only works when the heap is full, not for the other kinds of
OutOfMemoryError (for example when the JVM cannot create a new
thread).
In a container
On Linux, the JVM finds the container by itself: it sees its memory
limit and its number of CPUs. -XX:MaxRAMPercentage gives the part of
this memory used for the maximum heap, 25% by default. This percentage
follows the container limit, but a fixed -Xmx does not move when the
limit changes. The details, with measures:
Java in Docker and Kubernetes.
What changed: the options that no longer work
| Option | Java 8 | Java 11 | Java 17, 21 and 25 | Use instead |
|---|---|---|---|---|
-XX:PermSize, -XX:MaxPermSize | Ignored, with a warning | Ignored, with a warning | Refused | -XX:MetaspaceSize, -XX:MaxMetaspaceSize |
-XX:+UseConcMarkSweepGC | Accepted | Accepted, with a warning (deprecated) | Refused | Another GC (CMS is gone since Java 14) |
-XX:+UseParNewGC | Accepted | Refused | Refused (removed in Java 10) | — |
-XX:+UseCGroupMemoryLimitForHeap | Experimental | Refused | Refused (removed in Java 11) | Nothing: the JVM finds the container |
-XX:+PrintGCDateStamps, -XX:+PrintGCTimeStamps | Accepted | Refused | Refused | Nothing: -Xlog handles the dates |
-XX:+UseGCLogFileRotation | Accepted | Refused | Refused | Nothing: -Xlog handles rotation |
-XX:+PrintTenuringDistribution | Accepted | Refused | Refused | -Xlog:gc+age*=debug |
-XX:+PrintGCDetails | Accepted | Deprecated: a warning | Deprecated: a warning, then -Xlog:gc* | -Xlog:gc* |
-Xloggc:<file> | Accepted | Deprecated: a warning | Deprecated: a warning, then -Xlog:gc:<file> | -Xlog:gc:<file> |
So the old GC logging options were already refused in Java 11.
“Refused” means “Unrecognized VM option”, and the JVM does not start. A deprecated option still works, with a warning:
[0.001s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead.
A recent case: -XX:+ZGenerational turned on the generational mode of
ZGC in Java 21. Since Java 24, it is the only mode: in Java 25, the
option is ignored with a warning, and -XX:+UseZGC is enough.
Test an old option before you keep it
The simplest way is to start the JVM with your options and -version:
java -XX:+PrintGCDateStamps -Xmx2g -version
If an option is refused, you see it at once, without starting the
application. The documentation of the java command
also has three lists: deprecated, obsolete and removed options. But they
are short: an option removed a long time ago is not there anymore
(-XX:PermSize, for example). It also has a table with the -Xlog
equivalent of each old GC logging option.
Sources: the documentation of the java command and
Oracle’s garbage collection guide
(JDK 25), JEP 122 (PermGen),
JEP 271 (GC logs with -Xlog),
JEP 363 (CMS),
JDK-8173421 (ParNew),
JDK-8194086 (cgroups),
JEP 490 (ZGC).
Key points
Keep few options, and know why each one is there: the heap size, GC logs to a file and the heap dump. Do not copy a list of options from a forum or an old script without understanding it: at best, part of it does nothing anymore; at worst, the JVM does not start.