No GC Collect Call

ID

csharp.no_gc_collect_call

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Code Smell

Language

CSharp

Tags

code_smell, garbage-collection, memory, performance

Description

Reports calls to GC.Collect. An explicit collection overrides the runtime’s own decision about when collecting pays off, blocks every managed thread while it runs, and normally costs more throughput than the memory it reclaims.

Rationale

The .NET collector is generational and self-tuning: it measures allocation rate and survival rate and collects when the cost is lowest. GC.Collect short-circuits that model. A forced full collection walks all generations, and every object that is still reachable gets promoted to an older generation — so the objects that survive the forced pass become more expensive to collect later, not cheaper. Meanwhile all managed threads are suspended for the duration.

The memory the call appears to free would have been reclaimed by the next natural collection anyway. A GC.Collect in production code is almost always a workaround for a leak that is really a lingering strong reference, an undisposed handle, or a static cache that is never trimmed — and calling the collector hides the symptom without fixing any of those.

Other GC members are not reported: GC.SuppressFinalize is mandated by the dispose pattern, GC.KeepAlive is the documented way to keep an object alive across a native call, and GC.AddMemoryPressure is the supported way to account for unmanaged allocations.

Test code is not reported either. Forcing a deterministic collection before measuring is a standard technique for a leak-regression test — often the only way such a test can observe that memory a previous iteration allocated was actually reclaimed — and is not the production performance smell this rule is otherwise about.

using System;

public class Cache
{
    private byte[] buffer = new byte[1024];

    public void Trim()
    {
        buffer = null;
        GC.Collect();                            // FLAW — forces a full blocking collection
    }

    public void Release(IntPtr handle)
    {
        GC.SuppressFinalize(this);               // OK — required by the dispose pattern
        GC.KeepAlive(handle);                    // OK — keeps the handle alive across a native call
        GC.AddMemoryPressure(4096);              // OK — accounts for an unmanaged allocation
    }
}

Remediation

Delete the call and let the runtime schedule collections. If the code is trying to release memory, find what is still holding a reference: dispose IDisposable objects (preferably with using), unsubscribe from events, and bound the size of long-lived caches. If the code is trying to release an unmanaged resource deterministically, implement IDisposable and dispose it rather than waiting for a finalizer.

The rare legitimate use — a benchmark harness or a memory-profiling test that needs a clean baseline — belongs in test code, not in the shipped path.