JDK 27 Switches Default GC to G1—Why Your Unchanged Java Service Might Slow Down

JDK 27 changes the default garbage collector from a resource‑aware Serial GC to G1, which can unexpectedly increase CPU usage, alter latency and throughput for small‑container Java services even when no code or JVM flags are changed, so teams must verify the GC in use and benchmark both collectors under realistic limits.

Java Tech Enthusiast
Java Tech Enthusiast
Java Tech Enthusiast
JDK 27 Switches Default GC to G1—Why Your Unchanged Java Service Might Slow Down

Why the default GC change matters

Many Java teams assume that upgrading the JDK does not affect application behavior as long as no new language features are used. In July 2026 JDK 27 entered the final release stage and OpenJDK announced that, unless a GC is explicitly specified, HotSpot will always select G1 as the default collector, abandoning the previous logic that chose Serial GC on single‑CPU or low‑memory machines.

This means that a service whose code and JVM arguments remain unchanged may start using a different garbage collector after the upgrade.

When the change can impact you

Before JDK 27, if the start‑up command did not specify a collector, HotSpot would:

Use G1 on most server‑class environments.

Fall back to Serial GC when it detected a single CPU or physical memory below 1792 MiB.

From JDK 27 onward the fallback logic is removed; G1 becomes the default regardless of the environment. Applications are affected only if they satisfy both conditions:

The runtime environment has limited resources (e.g., a Kubernetes pod with 500 m CPU and 512 MiB memory).

No explicit -XX:+UseSerialGC or -XX:+UseG1GC flag is set.

Such configurations are common in micro‑services, scheduled jobs, configuration‑center clients, internal tools and serverless functions.

Serial GC vs. G1: trade‑offs

Serial GC is simple, single‑threaded and pauses the application during collection. It works well for tiny heaps, low request rates and single‑core environments because it adds little overhead.

G1 partitions the heap into regions and performs concurrent marking and region‑based reclamation to keep pause times low. It is better suited for medium‑to‑large heaps, latency‑sensitive services and long‑running online applications. OpenJDK notes that recent improvements have reduced G1’s native memory overhead to be comparable to Serial, its throughput may be slightly lower, but its maximum pause is usually lower.

Why the same service can appear slower

Consider a small Spring Boot service limited to 512 MiB heap and 1 CPU, handling ~5 QPS. Under Serial GC the collection frequency is low, pauses are noticeable but infrequent, and background GC work is minimal.

After upgrading to JDK 27, the same service may switch to G1, which introduces:

Additional background threads for concurrent marking.

More fine‑grained GC phases.

Reduced maximum pause times.

On a constrained CPU these extra threads compete with business threads, often raising overall CPU usage and sometimes slightly reducing throughput, even though the longest pauses become shorter. The result can be faster tail‑latency (P99) but higher average latency or CPU cost.

Missing visibility makes diagnosis hard

Many teams only monitor error rate, average response time, CPU and heap size, but omit crucial GC metrics such as:

Current garbage collector.

GC count and total pause time.

Maximum pause per collection.

Allocation and promotion rates.

Full GC occurrences.

Without these, a performance regression caused by a GC change is difficult to pinpoint.

How to determine which GC is active

Check the JVM start‑up parameters: ps -ef | grep java If the command contains -XX:+UseG1GC the service already uses G1; -XX:+UseSerialGC forces Serial. You can also query the effective flags:

jcmd VM.flags
jcmd VM.command_line

Enabling GC logging helps: -Xlog:gc*,safepoint For Spring Boot you can log the collector at start‑up:

import jakarta.annotation.PostConstruct;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class JvmRuntimeLogger {
    private static final Logger log = LoggerFactory.getLogger(JvmRuntimeLogger.class);

    @PostConstruct
    public void logRuntimeInformation() {
        log.info("java.version={}", System.getProperty("java.version"));
        log.info("java.vendor={}", System.getProperty("java.vendor"));
        List<String> collectors = ManagementFactory.getGarbageCollectorMXBeans()
                .stream().map(GarbageCollectorMXBean::getName).toList();
        log.info("garbage.collectors={}", collectors);
        log.info("available.processors={}", Runtime.getRuntime().availableProcessors());
        log.info("max.heap.bytes={}", Runtime.getRuntime().maxMemory());
    }
}

Container‑level resource detection

The JVM now reads container limits. The real resource boundary is what the container reports, not the host’s total CPU or memory. Verify the values with: Runtime.getRuntime().availableProcessors(); and ensure Kubernetes requests and limits match the intended test environment. Also check for manual overrides such as -XX:ActiveProcessorCount.

Don’t blindly lock in Serial GC

Because G1’s default has been chosen after years of improvement, forcing Serial GC may avoid a performance shift but also forfeits lower maximum pauses, more stable tail latency and future G1 optimisations. The recommended approach is to benchmark both collectors under realistic limits.

Benchmarking Serial vs. G1

Run the same image with identical business data and resource caps in two groups:

Serial GC

java \
  -XX:+UseSerialGC \
  -Xms512m \
  -Xmx512m \
  -Xlog:gc* \
  -jar app.jar

G1 GC

java \
  -XX:+UseG1GC \
  -Xms512m \
  -Xmx512m \
  -Xlog:gc* \
  -jar app.jar

Collect at least the following metrics for both the start‑up/pre‑heat phase and the steady‑state phase:

Throughput

P50, P95, P99 latency

Maximum response time

Total GC count and pause time

Maximum single pause

Average and peak CPU

Heap usage and process RSS

Container restarts

Do not rely solely on average latency; tail latency and pause behavior are often decisive.

Testing in realistic environments

Development machines (e.g., 12 CPU, 32 GB RAM) do not reflect the constraints of a 1‑CPU, 768 MiB container. Use Docker or Kubernetes to limit resources:

docker run \
  --cpus="1" \
  --memory="768m" \
  your-app:jdk27

Only then will the impact of G1’s background work be observable.

Spring Boot observability

If the project already uses Actuator and Micrometer, monitor these meters:

jvm.gc.pause
jvm.memory.used
jvm.memory.committed
jvm.threads.live
process.cpu.usage
system.cpu.usage
http.server.requests (count, avg, p95, p99, errors)

Combine GC pause data with CPU and latency to decide whether the trade‑off is acceptable.

Avoid over‑tuning G1 immediately

Modern JVMs adapt well; adding many G1 tuning flags ( -XX:MaxGCPauseMillis, -XX:G1HeapRegionSize, etc.) without a concrete problem can introduce new risks. Tune only after identifying a specific symptom such as excessive max pause, long concurrent marking, rapid old‑gen growth, frequent Full GC, or CPU saturation.

GC is not a cure for memory leaks

Switching collectors will not fix leaks caused by retained references (e.g., unbounded caches, static collections, ThreadLocal leaks, message backlogs, large object lifetimes, class‑loader leaks). When heap usage continuously grows, perform a heap dump, analyze reference chains, check allocation hotspots and cache capacities.

Which services should prioritize testing

Containers with ≤1 GiB memory.

Single‑CPU or low‑CPU services.

Large numbers of identical instances.

Endpoints sensitive to P99 latency (payments, login, real‑time control).

Short‑lived tasks (serverless, batch, frequent restarts).

Applications that do not explicitly set a GC.

Suggested JDK‑upgrade checklist

Beyond compile, test and start‑up verification, add runtime checks:

Garbage collector changed?
CPU count changed?
Max heap size changed?
Startup time changed?
P95/P99 latency changed?
GC max pause changed?
CPU and RSS changed?
OOMKilled occurrences?
Throughput changed?
Shutdown behavior normal?

Use a canary or gray‑release strategy (old JDK vs. new JDK) and compare the same traffic.

Key takeaway

JDK 27’s default‑GC switch is not a bug but a deliberate change that can affect services running in constrained containers. Upgrading a JDK is therefore a runtime‑policy change as well as a language/API change. Proper upgrade processes must include runtime and performance regression testing, explicit GC verification, realistic resource‑constrained benchmarking, and metric‑driven decisions rather than assuming the newer collector is automatically better for every workload.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Garbage CollectioncontainerizationSpring BootJava performanceG1 GCJDK 27Serial GC
Java Tech Enthusiast
Written by

Java Tech Enthusiast

Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.