Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 16 – JVM internals, memory, GC and performance

The JVM in containers and key flags

How the JVM sizes its heap and threads from container limits, how to set them with MaxRAMPercentage or -Xmx, and how to check the flags it really uses.

  • Advanced
  • 20 minutes
  • Examples run with Java 25.0.4.1 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: Default heap sizes, measured · Key flags for the JVM in containers · Checking the flags a JVM runs with · Check yourself. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Explain how the JVM sizes its heap and threads from container limits
  • Set the heap deliberately with -XX:MaxRAMPercentage or -Xmx
  • Inspect effective flags with -XX:+PrintFlagsFinal and jcmd VM.flags

Before you start

The JVM in containers sizes itself from what it detects: the memory it may use and the processors it may run on. On Linux, its container support, on by default, reads the container’s memory and CPU limits from the kernel’s control groups, so a JVM in a container with a 512 MB limit treats 512 MB as its physical memory. From that number it picks its maximum heap, 25 % by default (-XX:MaxRAMPercentage), and from the processor count it picks its garbage collector and its thread pools. A 512 MB container therefore gets a heap of only 128 MB unless you ask for more. The key flags let you decide instead of the defaults: -XX:MaxRAMPercentage gives the heap a share of whatever limit the container has, and -Xmx gives it a fixed size. This lesson measures the defaults and shows how to read the flags a JVM actually runs with.

Container limits become the memory and processors the JVM sees, which set the heap, the collector and thread counts unless flags override.Container limitsmemory.max and cpu.maxof its control groupWhat the JVM detectsmemory = the limitprocessors = the CPU quota, rounded upHeapmaximum 25 % of memory(50 % if very small), initial 1/64Collector (JDK 25)G1 with 2+ processors and 1,792 MB+,Serial otherwiseThread countsGC and JIT compiler threads,the common ForkJoinPoolYour flags win-XX:MaxRAMPercentage, -Xmx,-XX:ActiveProcessorCount, -XX:+UseG1GCcontainer support (Linux)override

How a JVM in a container sizes itself

Text description of the diagram

The diagram runs from top to bottom.

  1. Container limits: the memory.max and cpu.max values of the container's control group.
  2. What the JVM detects, through its container support on Linux: the memory limit as its memory, and the CPU quota, rounded up, as its processors.
  3. From that view, ergonomics choose the heap: a maximum of 25 % of memory, 50 % if the memory is very small, and an initial size of 1/64.
  4. Then the collector on JDK 25: G1 with two or more processors and at least 1,792 MB of memory, Serial otherwise.
  5. Then the thread counts: the garbage collector's threads, the JIT compiler's threads and the common ForkJoinPool.

A dashed box with a dashed arrow to the JVM's view notes that your own flags override these defaults: -XX:MaxRAMPercentage, -Xmx, -XX:ActiveProcessorCount, -XX:+UseG1GC and the others.

Default heap sizes, measured

-XX:MaxRAM sets the memory that the heap arithmetic starts from, which is what a container’s memory limit does on Linux. On this Mac, it lets one machine show the defaults for six container sizes:

Heap sizes chosen for 256 MB to 8 GB of memory Python · heap_session.py
import os
import re
import subprocess

from session import JDKS, run

# How the JVM sizes its heap from the memory it may use. -XX:MaxRAM sets that memory for the heap arithmetic, which
# is what a container's memory limit does on Linux; on this Mac it lets one machine show every size.
run("java -XX:MaxRAM=1g -XX:+PrintFlagsFinal -version | grep -E ' (MaxRAM|MaxRAMPercentage|InitialHeapSize|MaxHeapSize) '")


def flags(*options):
    env = dict(os.environ, PATH=os.pathsep.join([str(JDKS["25"]), os.environ.get("PATH", "")]))
    out = subprocess.run(["java", *options, "-XX:+PrintFlagsFinal", "-version"], env=env, capture_output=True, text=True).stdout
    return {m.group(1): int(m.group(2)) for m in re.finditer(r" (MaxHeapSize|InitialHeapSize) += (\d+) ", out)}


print("# The same arithmetic for six memory sizes, in MiB (1 MiB = 1,048,576 bytes)")
print(f"{'memory':>8} {'initial heap':>13} {'max heap':>9} {'share':>6} {'max with MaxRAMPercentage=75':>29}")
for size, mib in [("256m", 256), ("512m", 512), ("1g", 1024), ("2g", 2048), ("4g", 4096), ("8g", 8192)]:
    default = flags(f"-XX:MaxRAM={size}")
    tuned = flags(f"-XX:MaxRAM={size}", "-XX:MaxRAMPercentage=75")
    mx = default["MaxHeapSize"] / 2**20
    print(f"{mib:>8} {default['InitialHeapSize'] / 2**20:>13.0f} {mx:>9.0f} {100 * mx / mib:>5.0f}% {tuned['MaxHeapSize'] / 2**20:>29.0f}")

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ java -XX:MaxRAM=1g -XX:+PrintFlagsFinal -version | grep -E ' (MaxRAM|MaxRAMPercentage|InitialHeapSize|MaxHeapSize) '
openjdk version "25.0.4.1" LTS
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)
   size_t InitialHeapSize                          = 16777216                                  {product} {ergonomic}
   size_t MaxHeapSize                              = 268435456                                 {product} {ergonomic}
 uint64_t MaxRAM                                   = 1073741824                             {pd product} {command line}
   double MaxRAMPercentage                         = 25.000000                                 {product} {default}
# The same arithmetic for six memory sizes, in MiB (1 MiB = 1,048,576 bytes)
  memory  initial heap  max heap  share  max with MaxRAMPercentage=75
     256             8       128    50%                           192
     512             8       128    25%                           384
    1024            16       256    25%                           768
    2048            32       512    25%                          1536
    4096            64      1024    25%                          3072
    8192           128      2048    25%                          6144

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 heap_session.py

The rule has three parts. The maximum heap is 25 % of the memory, the MaxRAMPercentage default. For small memory the JVM gives the heap a larger share, up to MinRAMPercentage, 50 % by default, so that a heap of about 125 MB stays possible; that is why 256 MB of memory got a 128 MB heap, half of it, and 512 MB got the same 128 MB, a quarter. The initial heap is 1/64 of the memory, the documented 1.5625 % of InitialRAMPercentage, but never less than 8 MB here. With -XX:MaxRAMPercentage=75, the maximum heap became three quarters of the memory at every size.

A quarter of the memory is a cautious default for a machine that runs many programs. A container usually runs one Java process, so a heap of 25 % leaves most of the limit unused, and the service runs with a quarter of the memory it pays for. The rest of the limit is still needed, though, for memory outside the heap, which is why a share between 50 % and 75 % is a sensible starting point, to be checked by measuring, and 100 % never is.

The two ways of setting the heap suit different deployments. A percentage follows the limit: the same image, started in a container of 1 GB or of 4 GB, gets a heap that fits each one, so it is the better default for images that run in containers of several sizes. A fixed -Xmx stays the same whatever the limit, which suits a service whose live data you have measured, as long as the limit is changed together with it.

Key flags for the JVM in containers

The key flags, each in one sentence:

  • -XX:MaxRAMPercentage=75 sets the maximum heap to 75 % of the container’s memory limit, whatever that limit is.
  • -Xmx2g sets the maximum heap to exactly 2 GB, and wins over the percentage flags.
  • -Xms sets the initial heap; equal to -Xmx, the heap never has to grow.
  • -XX:ActiveProcessorCount=2 makes the JVM size its thread pools for two processors.
  • -Xss sets the stack size of each thread, 1 MB on Linux on x64 and 2 MB on Arm by default.
  • -XX:MaxMetaspaceSize and -XX:MaxDirectMemorySize cap two of the kinds of memory outside the heap.
  • -XX:+ExitOnOutOfMemoryError ends the JVM at its first OutOfMemoryError, so the container restarts it.
  • -XX:+UseSerialGC or -XX:+UseG1GC chooses the collector instead of the defaults.
  • -XX:NativeMemoryTracking=summary makes the JVM count the memory it uses outside the heap, by category.

Checking the flags a JVM runs with

What a JVM decided can be read in three ways: at start-up, from the system it detected, and from outside while it runs.

Service.java Java · Service.java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/** A stand-in for a service: each line of standard input is a number of worker threads to start, which then wait. */
public class Service {
    public static void main(String[] args) throws Exception {
        var in = new java.io.BufferedReader(new java.io.InputStreamReader(System.in));
        List<Thread> workers = new ArrayList<>();
        CountDownLatch stop = new CountDownLatch(1);
        for (String line; (line = in.readLine()) != null; ) {
            int n = Integer.parseInt(line.trim());
            for (int i = 0; i < n; i++) {
                Thread t = Thread.ofPlatform().daemon().start(() -> {
                    try { stop.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
                });
                workers.add(t);
            }
            System.out.println(workers.size() + " worker threads started");
        }
        stop.countDown();
    }
}
PrintCommandLineFlags, -XshowSettings:system and jcmd VM.flags Python · flags_session.py
from session import background, run

# Three ways to see the flags a JVM runs with: at start-up, from the system it detected, and from a running JVM.
run("""
java -XX:+PrintCommandLineFlags -version
java -XshowSettings:system -version
javac -d out Service.java
""")
print("# Terminal 1: a service started with two flags of its own")
service = background("java -Xmx256m -XX:+ExitOnOutOfMemoryError -cp out Service")
service.send("4")
print("# Terminal 2")
run("jcmd <pid> VM.flags", version=False)
print("# Terminal 1")
service.close()

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ java -XX:+PrintCommandLineFlags -version
-XX:-AOTInvokeDynamicLinking -XX:-AOTRecordTraining -XX:-AOTReplayTraining -XX:ConcGCThreads=4 -XX:G1ConcRefinementThreads=14 -XX:InitialHeapSize=805306368 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=12884901888 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedOops -XX:+UseG1GC 
openjdk version "25.0.4.1" LTS
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)
$ java -XshowSettings:system -version
Operating System Metrics:
    No metrics available for this platform
openjdk version "25.0.4.1" LTS
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)
$ javac -d out Service.java
# Terminal 1: a service started with two flags of its own
$ java -Xmx256m -XX:+ExitOnOutOfMemoryError -cp out Service
> 4
4 worker threads started
# Terminal 2
$ jcmd <pid> VM.flags
<pid>:
-XX:-AOTInvokeDynamicLinking -XX:-AOTRecordTraining -XX:-AOTReplayTraining -XX:CICompilerCount=12 -XX:ConcGCThreads=4 -XX:+ExitOnOutOfMemoryError -XX:G1ConcRefinementThreads=14 -XX:G1EagerReclaimRemSetThreshold=10 -XX:G1HeapRegionSize=1048576 -XX:G1RemSetArrayOfCardsEntries=10 -XX:G1RemSetHowlMaxNumBuckets=8 -XX:G1RemSetHowlNumBuckets=4 -XX:InitialHeapSize=268435456 -XX:MarkStackSize=4194304 -XX:MarkStackSizeMax=536870912 -XX:MaxHeapSize=268435456 -XX:MaxNewSize=160432128 -XX:MinHeapDeltaBytes=1048576 -XX:MinHeapSize=8388608 -XX:NonNMethodCodeHeapSize=7618560 -XX:NonProfiledCodeHeapSize=122028032 -XX:ProfiledCodeHeapSize=122028032 -XX:ReservedCodeCacheSize=251674624 -XX:+SegmentedCodeCache -XX:SoftMaxHeapSize=268435456 -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseNUMA -XX:-UseNUMAInterleaving 
# Terminal 1
> (end of input)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 flags_session.py

-XX:+PrintCommandLineFlags prints the flags that were given and those that ergonomics chose, such as the collector and the heap sizes. -XshowSettings:system is documented as Linux only: there it prints the container’s memory and CPU limits as the JVM detected them, and on this Mac it printed that no metrics are available. jcmd <pid> VM.flags asks a running JVM for every flag that differs from its default, the ones from the command line included, here -XX:MaxHeapSize=268435456 from -Xmx256m and -XX:+ExitOnOutOfMemoryError. For one flag at a time, -XX:+PrintFlagsFinal lists every flag with its value and where it came from: {default}, {ergonomic} or {command line}. On Linux, -Xlog:os+container=trace adds a log line for every limit the JVM reads, which is the quickest way to see why a limit seems to be ignored.

Data Storage Converter Convert flag values such as 268435456 bytes into MiB or GiB.

Check yourself

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 A JDK 25 service runs in a Linux container with a 1 GB memory limit and no heap flags. What maximum heap does the JVM choose, in MiB?

    Type a number.

    Show the answer to question 1

    Answer: 256 MiB

    The default MaxRAMPercentage is 25 %, so the maximum heap is a quarter of the 1,024 MiB limit: 256 MiB, as the recorded table shows for 1 GB.

  2. Question 2 of 4 One container image runs in containers of 1 GB and of 4 GB. Which setting gives each a heap that fits its limit?

    Choose one answer.

    Show the answer to question 2

    Answer: -XX:MaxRAMPercentage=70

    A percentage follows the limit: about 717 MB in the 1 GB container and 2.8 GB in the 4 GB one. A fixed -Xmx is too big for one of them or too small for the other, and the default uses only 25 %.

  3. Question 3 of 4 Which command shows the flags of a JVM that is already running?

    Choose one answer.

    Show the answer to question 3

    Answer: jcmd <pid> VM.flags

    jcmd sends the VM.flags command to the running JVM, which prints its flags that differ from the defaults. PrintFlagsFinal and -XshowSettings start a new JVM and describe that one.

  4. Question 4 of 4 Why did 256 MB of memory get a 128 MB heap, half of it, when the default share is 25 %?

    Choose one answer.

    Show the answer to question 4

    Answer: For very small memory, the JVM allows up to MinRAMPercentage, 50 %, so that a heap of about 125 MB stays possible

    The java documentation describes MinRAMPercentage as the maximum share for small heaps of about 125 MB. Above that, the 25 % of MaxRAMPercentage applies, which is why 512 MB also got 128 MB.

A memory budget for a container, with numbers Pro: opens with a Pro, Premium or Ultimate pass

A container’s memory limit covers the whole process, and the heap is only part of it. This part measures the rest with Native Memory Tracking, builds budgets for three limits, and explains the exit status 137 that a container reports when the kernel ends a JVM.

In this part:

  • Where the memory goes besides the heap, measured
  • Budgets for 512 MB, 1 GB and 4 GB containers
  • Exit status 137: the kernel, not the JVM

1 runnable example (Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

CPU limits, collector choice and thread counts, measured Pro: opens with a Pro, Premium or Ultimate pass

The processor count decides more than speed: it picks the collector and sizes every pool of threads in the JVM and in the libraries above it. This part measures those choices for one to eight processors.

In this part:

  • One, two, four and eight processors
  • Fractional CPU limits and what changes in JDK 27

2 runnable examples (Java, Python) · about 2 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Your turn: flags for three services, with a rubric Pro: opens with a Pro, Premium or Ultimate pass

Choosing flags is arithmetic with a few rules, and it can be checked. This part sets three services with their container limits, works out the flags for each, and gives a rubric for marking an answer.

In this part:

  • Three services, their limits and the question
  • A worked answer
  • How to mark the answer

about 2 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    How does a JDK 25 JVM choose its maximum heap inside a Linux container with a 2 GB memory limit and no heap flags?

    On Linux, the JVM's container support reads the container's memory limit and treats it as the machine's physical memory. Its ergonomics then set the maximum heap to MaxRAMPercentage of that memory, 25 % by default, so a 2 GB limit gives a 512 MB heap, and the initial heap to 1/64, 32 MB. For very small memory, the JVM allows up to MinRAMPercentage, 50 %, so that a heap of about 125 MB stays possible: a 256 MB container gets a 128 MB heap. To use more of the limit, set -XX:MaxRAMPercentage, for example 70, which follows whatever limit the container has, or a fixed -Xmx. java -XX:+PrintFlagsFinal -version | grep MaxHeapSize shows the result.

  2. Mid to senior

    A Java service in a container with a 1 GB limit is restarted several times a day with exit status 137, yet its GC logs show the heap never above 600 MB. What is happening, and how do you fix it?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Senior to staff

    A service in a container limited to 1 CPU and 1 GB uses parallel streams and shows long garbage collection pauses on JDK 25. What did JVM ergonomics choose, and what would you change?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    Review this start command for a web service in a container with a 2 GB memory limit and 2 CPUs: java -Xmx2g -Xss8m -XX:+UseParallelGC -jar app.jar. What is wrong, and what would you write instead?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • On Linux, the JVM in containers reads the memory and CPU limits and sizes itself from them.
  • By default the heap gets 25 % of the memory (50 % for very small memory); -XX:MaxRAMPercentage or -Xmx sets it deliberately.
  • -XX:+PrintFlagsFinal, -XX:+PrintCommandLineFlags and jcmd <pid> VM.flags show the values a JVM really uses.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.