DEV Community

Zoe
Zoe

Posted on

When Major GC Strikes: Why Your JVM App Suddenly Freezes

Introduction

My Java application was experiencing performance issues, and I suspected it was related to how the JVM was managing memory. To optimize performance, I needed a clear understanding of how garbage collection (GC) works in the JVM 💭

This post breaks down how Garbage Collection works, the difference between Minor and Major GC, and why Full GC can freeze your whole app. Learn what to watch in your metrics so you can spot memory issues before your users feel the lag.

⚠️ TL;DR - Watch out for Major GC!

  • Major GC (a.k.a. Full GC) causes your whole app to pause - and not in a chill way 😵
  • Major GC itself isn't bad, but if the pause time is above miliseconds, your users might feel the lag.
  • We saw memory usage spike, GC old gen size grow, and major GC run more frequently
  • The root cause? The service that was newly released had a bug that was consuming too much memory
  • Always keep an eye on major GC metrics to catch issues early before users feel the lag!

How Java Applications Use Memory

First things first, we need to understand how Java applications utilize memory to fully understand garbage collection.
Java splits memory into multiple areas:

1. Heap Memory - Our Focus Area For This Post

  • Stores: Objects and arrays.
  • Managed by: Garbage Collector.
  • Subdivided into: Young Generation (Eden + Survivor spaces) and Old Generation(Where long-lived objects are promoted)

Example: new User() allocates memory in the heap.

2. Stack Memory

  • Stores: Local variables and method call info.
  • Per-thread stack.
  • Cleared automatically on method return.

Example: int count = 5 goes on the stack.

3. Metaspace (Java 8+)

  • Stores: Class metadata.
  • Native memory (not heap).
  • Grows dynamically but can OOM.

Example: Defining a class like class Product.

4. Code Cache

  • Stores: JIT-compiled native bytecode.

5. Native Memory

  • Includes: Thread stacks, DirectByteBuffer, JNI memory.
  • Not managed by -Xmx.

What Is the Heap?

Since we'll be focusing on garbage collection in this post, we'll dig a bit deeper here, which is where the garbage collection happens.

  • Dynamic memory allocation area.
  • Where new Object() lives.
  • Operated on by the GC.

Heap Structure

  • Young Generation: New objects go here (Eden + Survivor)
  • Old Generation: Long-lived objects gets promoted here
  • Metaspace(Not technically heap): Class metadata lives here.

GC Lifecycle Example

  1. User user = new User(); → Eden
  2. Survives GC → Survivor → Old Gen
  3. Becomes unreachable → Collected by GC

Heap Management Tips

  • Too small → OutOfMemoryError
  • Too large → Long GC pauses
  • Monitor and tune with -Xms, -Xmx

Minor GC vs Major GC

There are two types on garbage collection in Java.
The main difference between Java GC Minor and Major collections comes down to which part of the heap they operate on and how heavy the operation is.

Minor GC

  • Operates On: Young Generation
  • Frequency: Frequent (seconds)
  • Performance: Low
  • Pause Time: Short
  • Purpose: Clean short-lived objects

Major GC

  • Operates On: Old Generation
  • Frequency: Rare (minutes+)
  • Performance: High
  • Pause Time: Long
  • Purpose: Clean long-lived objects

Full GC

  • Operates On: Entire Heap + Metaspace
  • Frequency: Emergency
  • Performance: Very High
  • Pause Time: Longest
  • Purpose: Full memory cleanup

GC Spikes: What Do They Mean?

So what does it mean when the frequency of GC spikes?
For minor GC, it's usually not a problem, but major GC can be problematic depending on the time consumed.

  • Minor GC Spikes
  • Heavy creation and destruction of objects
  • Eden space fills up quickly
  • High frequency leads to increased CPU load and latency

Countermeasures (Infra perspective):

  • Increase Young Gen size using -Xmn
  • Utilize object pools

Countermeasures (App perspective):

  • Check if temporary objects are being created excessively (e.g., overuse of new String())
  • Review whether cache and buffer designs are appropriate
  • Improve code to reuse unnecessary objects

Major GC Spikes ← Watch Out!

  • Pressure on Old Gen
  • Potential memory leaks
  • Long application pauses
  • Major GC itself isn't bad, but if it takes too long, your application might become laggy

Countermeasures (Infra perspective):

  • Analyze memory state with heap dumps
  • Increase heap size (adjust -Xmx)
  • Continuously monitor Old Gen usage with tools like Datadog

Countermeasures (App perspective):

  • Work with the dev team to identify which processes or classes are holding large amounts of memory
  • Detect memory leaks or objects living longer than expected
  • Review object lifecycle management and release unnecessary references
  • Check if usage of finalize() or caches is appropriate

How Heap Size Is Determined

Heap does not grow with system memory by default. Typically, for a memory with 2GB limit, the max heap size is around 512MB~1GB.
To customize the heap size, use:

  • -Xms: Initial heap
  • -Xmx: Max heap

GC Metric Analysis

Well, now we know that it can be bad if major GC happens frequently. So how do we look out for it?
Here are some datadog queries that might help you analyze the metrics related to garbage collection.

Minor Collection Count

exclude_null(avg:jvm.gc.minor_collection_count{...})
Enter fullscreen mode Exit fullscreen mode

Minor Collection Time

exclude_null(avg:jvm.gc.minor_collection_time{...})
Enter fullscreen mode Exit fullscreen mode

Major Collection Count

exclude_null(avg:jvm.gc.major_collection_count{...})
Enter fullscreen mode Exit fullscreen mode

Major Collection Time

exclude_null(avg:jvm.gc.major_collection_time{...})
Enter fullscreen mode Exit fullscreen mode

Old Gen Size

exclude_null(avg:jvm.gc.old_gen_size{...})
Enter fullscreen mode Exit fullscreen mode

My Personal Experience

This is what I observed after the initial release of a new service:

  1. Minor GC spiked right after release.
  2. Major GC occurred days later (still microseconds, so not too bad).
  3. Old Gen size steadily increased.
  4. Memory usage jumped 40%.

Root Cause

Since it seemed to be caused by the release of a new service, We asked the devs to dig into the app to identify memory-heavy objects. Turns out, the bug was basically about constructor misuse ! The original code accidentally created a full copy of a collection 🫠
But I was able to learn a lot about garbage collection and memory management thanks to this incident.

Final TL;DR

  • GC manages Java memory automatically.
  • Minor GC = Young Gen; fast and frequent.
  • Major GC = Old Gen; slow and heavy.
  • Heap structure (Eden → Survivor → Old Gen) determines GC behavior.
  • Watch for GC spikes: Minor = churn, Major = leaks.
  • Tune with -Xms, -Xmx, and monitor GC metrics.

Thank You!

Thanks for reading! ✨
I also post on my personal blog, where I share notes and experiments.
If you'd like to connect, you can find me on GitHub or LinkedIn. 🚀

Top comments (0)