skip to content

When does HotSpot select the Serial collector automatically instead of the default G1, and how does running inside a container with CPU and memory limits change that decision?

level: middleimportance: should knowfreq 36%

answer

  1. Server-class = ≥2 CPUs AND ≥~1792 MB
  2. Not server-class ⇒ Serial, else G1 (JDK 9+)
  3. UseContainerSupport: cgroup quota feeds availableProcessors
  4. cpu: 1 container ⇒ Serial silently
  5. Verify with -Xlog:gc / PrintFlagsFinal; pin explicitly

basics

~20 s

If the JVM does not detect a "server-class" machine — roughly at least two available processors and about 1792 MB of memory — ergonomics fall back to Serial. Container limits count: with container support on, cgroup CPU quota and memory limits feed those checks, so a one-CPU container silently gets Serial.

solid answer

~60 s

HotSpot picks a default collector from an ergonomics test at startup. A **server-class** machine — approximately two or more available processors **and** at least ~1792 MB of usable memory — gets G1 (the default since JDK 9). Anything smaller falls back to **Serial**. The subtlety is that "available processors" and "usable memory" are *container-aware* from JDK 10 onwards (`-XX:+UseContainerSupport`, on by default): the JVM reads the cgroup CPU quota / cpuset and the memory limit rather than the host's hardware. So a service pinned to `cpu: 1` or given a 512 MB limit is running Serial even though the underlying node has 64 cores — a common surprise when a service is "the same code" but behaves differently in Kubernetes than on a laptop. Always verify rather than assume: - `java -Xlog:gc` — the first lines name the collector. - `java -XX:+PrintFlagsFinal -version | grep -E 'UseSerialGC|UseG1GC'`. - `-XX:ActiveProcessorCount=N` overrides the detected CPU count if the quota reporting is wrong for your setup. If you care which collector runs, set it explicitly instead of relying on ergonomics.

code

text · 5 lines
text
$ docker run --cpus=1 --memory=512m img java -Xlog:gc -version
[0.005s][info][gc] Using Serial

$ docker run --cpus=4 --memory=4g img java -Xlog:gc -version
[0.008s][info][gc] Using G1

go deeper

for a junior

Know that the JVM picks a collector at startup and that -XX:+UseSerialGC / -XX:+UseG1GC override it.

for a middle

State the server-class test (≈2 CPUs and ≈1792 MB) and that cgroup limits feed those numbers in containers.

for a senior

Show the diagnosis path — -Xlog:gc, PrintFlagsFinal, os+container logging — and argue for pinning the collector in the deployment.

for a principal

Treat collector selection as a platform contract: capacity changes must not silently alter runtime behaviour across a fleet, so defaults belong in the base image with per-service escape hatches.

## Why the JVM chooses for you HotSpot has shipped **ergonomics** since the early 2000s: at startup it inspects the machine and picks a garbage collector, heap sizes, and thread counts so that a JVM with no tuning flags behaves sensibly on both a laptop and a large server. The collector part of that decision is a coarse binary test. ## The server-class test The JVM classifies the machine as *server-class* when both hold: - **at least 2 available processors**, and - **at least ~1792 MB of physical memory** (historically expressed as 2 GB minus headroom). Server-class ⇒ the default collector (G1 since JDK 9; before that, Parallel). Not server-class ⇒ **Serial**, together with smaller default heap sizing. The reasoning is straightforward: a collector with parallel workers and concurrent phases only pays off when there is a second core for those workers to run on and enough heap that pause scalability matters. On one core, GC worker threads and concurrent marking merely time-slice against the application, adding coordination cost with no wall-clock benefit. Note that both defaults are also affected by `-XX:MaxRAMPercentage` / `-Xmx`, but the *collector* choice is made from detected machine capacity, not from the heap size you request. ## Containers change the inputs, not the rule Before JDK 10, a JVM in a container saw the **host's** CPU count and memory. A one-CPU container on a 64-core node therefore looked server-class, got G1 and a huge default heap, spawned dozens of GC worker threads, and was then throttled to death by the cgroup quota. JDK 10 introduced container awareness (`-XX:+UseContainerSupport`, enabled by default; backported to 8u191). The JVM now reads cgroup limits: - **Memory**: the cgroup memory limit becomes "physical memory" for ergonomics, driving both the server-class test and default heap sizing. - **CPU**: `Runtime.availableProcessors()` derives from the cpu quota/period, cpu shares, and cpuset — for example `cpu: 1` (quota 100000, period 100000) reports 1. So the *rule* did not change; the *readings* did. The practical consequence is that a small container silently ends up on Serial. This is usually the right call, but it must not be a surprise: a service that was benchmarked on a workstation with G1 and low-millisecond young pauses can, in production at `cpu: 1`, exhibit long single-threaded full GCs on the same heap. A related trap: fractional quotas. `cpu: 1.5` rounds up to 2 available processors, tipping the JVM into server-class territory even though it can never use two cores in parallel for a full period. And `cpu` *requests* without *limits* leave no quota at all, so the JVM sees the whole node. ## How to inspect and control it ``` java -Xlog:gc -version # first lines name the collector java -XX:+PrintFlagsFinal -version | grep UseSerialGC java -Xlog:os+container=trace -version # what limits the JVM actually detected ``` Overrides: - `-XX:+UseSerialGC` / `-XX:+UseG1GC` — decide explicitly; ergonomics stop mattering. - `-XX:ActiveProcessorCount=N` — force the CPU count the JVM believes in, useful when quota detection does not reflect how the workload is actually scheduled. - `-XX:-UseContainerSupport` — disables cgroup awareness entirely; almost never the right answer today. ## The operational takeaway Treat the collector as part of your deployment contract, not as something inferred. For any service with a latency SLO, pin the collector and the heap explicitly in the image or manifest so that a change to CPU limits — made by a capacity team who has never heard of the server-class test — cannot silently swap your collector underneath you. Conversely, when a small sidecar or CLI-shaped process gets Serial by ergonomics, that is usually a good outcome and worth leaving alone once verified.

  • A team reports that their service uses far more memory and threads in production than on a developer laptop with the same flags. What would you check first?
    Whether the JVM is detecting container limits at all. On an old JDK, or with container support disabled, or with CPU requests but no limits, the JVM sees the whole node — so it classifies the machine as server-class, sizes the default heap from host memory, and starts GC worker threads proportional to host cores. Check `-Xlog:os+container=trace` and the reported `availableProcessors()`.
  • Is it ever right to force G1 in a one-CPU container?
    Rarely, but yes: if the live set is large enough that a Serial full GC would blow the latency budget, a concurrent collector's ability to do marking work in slices can still help, even though those slices steal CPU from the application. The honest answer is that a one-CPU container with a large live set is a sizing problem first — the better fix is usually more CPU or a smaller heap.

saying these in an interview costs you the question

  • Believing the collector default depends on the -Xmx you request rather than on detected machine capacity
  • Assuming a modern JVM always sees host CPUs — container support has been on by default since JDK 10
  • Saying G1 is always the default; on a non-server-class machine ergonomics choose Serial
  • Treating -XX:ActiveProcessorCount as a way to limit CPU usage; it only changes what the JVM believes

context