Fundamentals8 min

Garbage Collector and Performance: the invisible impact

The GC can be the silent villain of your latency. Understand how it works and how to minimize its impact on performance.

In languages with automatic memory management (Java, C#, Go, JavaScript), the Garbage Collector (GC) is responsible for freeing unused memory. While it's a huge convenience for developers, the GC can be a silent performance villain.

This article explores how the GC works, its impact on latency and throughput, and strategies to minimize problems.

The GC is like a janitor cleaning while you work. Sometimes it needs to stop everything for a deep clean.

How the Garbage Collector Works

Basic concept

The GC identifies objects in memory that are no longer referenced and frees that space for reuse.

Allocation → Use → Reference removed → GC identifies → Memory freed

Types of collection

Minor/Young GC

  • Collects short-lived objects
  • Fast (milliseconds)
  • Frequent

Major/Full GC

  • Collects entire heap
  • Slow (hundreds of ms to seconds)
  • Less frequent
  • Usually causes "stop-the-world"

Stop-the-World (STW)

During certain GC phases, the application is completely paused. No application code executes.

Time
│
│  App  │ GC │    App    │ GC │  App
│───────│████│───────────│████│──────
        │STW │           │STW │

These pauses appear as latency spikes.

Impact on Performance

Latency

Symptoms:

  • Periodic latency spikes
  • p99 much higher than p50
  • Erratic latency

Example:

p50: 10ms
p95: 15ms
p99: 500ms  ← Probably GC

Throughput

Time spent in GC is time not spent processing requests.

Rule of thumb:

  • GC < 5% of total time: acceptable
  • GC 5-10%: attention
  • GC > 10%: serious problem

Predictability

Even if the average is good, GC pauses destroy predictability — essential for latency SLOs.

Factors that Increase GC Pressure

1. High allocation rate

The more objects you create, the more work for the GC.

Common culprits:

  • Intermediate strings in loops
  • Boxed primitives (Integer vs int)
  • Streams and lambdas creating temporary objects
  • Serialization/deserialization

2. Medium-lived objects

Objects that survive a few minor collections but die before becoming permanent create extra work.

3. Large heap

Paradoxically, very large heaps can worsen GC:

  • Full GC takes longer
  • More objects to check

4. Fragmentation

Objects of varying sizes leave holes in memory, forcing more frequent GCs.

Mitigation Strategies

1. Reduce allocations

Before:

for (User user : users) {
    String key = "user:" + user.getId();  // New String each iteration
    cache.get(key);
}

After:

StringBuilder sb = new StringBuilder("user:");
int prefixLen = sb.length();
for (User user : users) {
    sb.setLength(prefixLen);
    sb.append(user.getId());
    cache.get(sb.toString());
}

2. Object pooling

Reuse expensive objects instead of creating new ones.

// Pool of reusable buffers
ByteBuffer buffer = bufferPool.acquire();
try {
    // use buffer
} finally {
    bufferPool.release(buffer);
}

3. Use primitive types

// Avoid
List<Integer> numbers;  // Each Integer is an object

// Prefer
int[] numbers;  // Array of primitives
// or use libraries like Eclipse Collections, Trove

4. Choose the right GC

JVM offers multiple GCs:

GC Focus When to use
G1 Balanced Default, good general choice
ZGC Low latency Pause-sensitive applications
Shenandoah Low latency Similar to ZGC
Parallel Throughput Batch processing

5. Tune the GC

Common parameters (JVM):

# Heap size
-Xms4g -Xmx4g

# G1 pause goal
-XX:MaxGCPauseMillis=200

# Use ZGC for low latency
-XX:+UseZGC

Warning: premature tuning can make things worse. First measure, then adjust.

6. Monitor GC

Enable GC logs:

-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=10M

Important metrics:

  • GC frequency
  • Pause duration
  • Total time in GC
  • Memory recovered per cycle

GC in Other Languages

Go

Go has concurrent GC with very short pauses (< 1ms typical).

Optimizations:

  • Use sync.Pool for temporary objects
  • Pre-allocate slices when size is known
  • Avoid creating closures in hot loops

Node.js (V8)

V8 has generational GC similar to JVM.

Optimizations:

  • Avoid creating functions inside loops
  • Reuse objects when possible
  • Use typed arrays for numeric data

.NET

.NET has generational GC with different modes (Workstation, Server).

Optimizations:

  • Use structs for small, short-lived objects
  • Span to avoid allocations
  • ArrayPool for buffers

Identifying GC Problems

Symptoms

  1. Periodic latency spikes
  2. High CPU without application code using it
  3. Sawtooth memory pattern
  4. Application momentarily freezes

Diagnosis

  1. Enable GC logs
  2. Correlate pauses with latency spikes
  3. Analyze frequency and duration
  4. Identify if it's minor or major GC
  5. Profile to find allocation sources

Conclusion

The Garbage Collector is a double-edged sword:

  • Benefit: frees developers from managing memory manually
  • Cost: can cause unpredictable pauses and degrade performance

For latency-sensitive systems:

  1. Reduce allocations — less garbage = less collection
  2. Choose the right GC — different GCs have different trade-offs
  3. Monitor continuously — GC is a constant source of latency problems
  4. Tune carefully — based on data, not intuition

The best GC is one you don't even notice is running.

garbage collectormemoryJVMlatency

Want to understand your platform's limits?

Contact us for a performance assessment.

Contact Us