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

OptionWhat it doesWhen to change itWhen to leave it alone
-Xms, -XmxInitial and maximum heap sizeAlmost always on a server: the default is often too small—
-XssStack size of each threadAfter a StackOverflowError in a recursion that is really neededOtherwise: the default is enough
-XX:+UseG1GC, -XX:+UseParallelGC, -XX:+UseZGCGC choiceWhen you have a clear need (latency, throughput)By default: let the JVM choose first
-XX:MaxGCPauseMillisPause time goal (G1: 200 ms by default)If G1 pauses are too long for youTo make the GC “faster”: shorter pauses can cost throughput
-Xlog:gc*GC logsAlways useful in production, to a file—
-XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPathHeap dump when the heap is fullIn production, to understand an OutOfMemoryError—
-XX:MaxRAMPercentageMaximum 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

OptionJava 8Java 11Java 17, 21 and 25Use instead
-XX:PermSize, -XX:MaxPermSizeIgnored, with a warningIgnored, with a warningRefused-XX:MetaspaceSize, -XX:MaxMetaspaceSize
-XX:+UseConcMarkSweepGCAcceptedAccepted, with a warning (deprecated)RefusedAnother GC (CMS is gone since Java 14)
-XX:+UseParNewGCAcceptedRefusedRefused (removed in Java 10)—
-XX:+UseCGroupMemoryLimitForHeapExperimentalRefusedRefused (removed in Java 11)Nothing: the JVM finds the container
-XX:+PrintGCDateStamps, -XX:+PrintGCTimeStampsAcceptedRefusedRefusedNothing: -Xlog handles the dates
-XX:+UseGCLogFileRotationAcceptedRefusedRefusedNothing: -Xlog handles rotation
-XX:+PrintTenuringDistributionAcceptedRefusedRefused-Xlog:gc+age*=debug
-XX:+PrintGCDetailsAcceptedDeprecated: a warningDeprecated: a warning, then -Xlog:gc*-Xlog:gc*
-Xloggc:<file>AcceptedDeprecated: a warningDeprecated: 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.

Further reading