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
- User user = new User(); → Eden
- Survives GC → Survivor → Old Gen
- 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{...})
Minor Collection Time
exclude_null(avg:jvm.gc.minor_collection_time{...})
Major Collection Count
exclude_null(avg:jvm.gc.major_collection_count{...})
Major Collection Time
exclude_null(avg:jvm.gc.major_collection_time{...})
Old Gen Size
exclude_null(avg:jvm.gc.old_gen_size{...})
My Personal Experience
This is what I observed after the initial release of a new service:
- Minor GC spiked right after release.
- Major GC occurred days later (still microseconds, so not too bad).
- Old Gen size steadily increased.
- 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)