.NET में GC wait और memory leak कैसे अलग करें — बढ़ती memory को observe, compare और prove करने की practical procedure

· अद्यतन तिथि: · · .NET, C#, GC, Memory Leak, Diagnostics, dotnet-counters, dotnet-dump, Operations, legacy asset reuse

1. पहले यह समझ लें

.NET application चलाते हुए कभी-कभी memory usage धीरे-धीरे बढ़ती दिखती है।

Task Manager या top देखें तो process की memory बढ़ रही है। Container की memory usage भी बढ़ रही है। Monitoring में Working Set या RSS का graph steadily ऊपर जा रहा है।

यह देखकर तुरंत «memory leak तो नहीं?» सोचना स्वाभाविक है। लेकिन .NET में process की memory बढ़ना और memory leak होना एक ही बात नहीं है।

.NET में garbage collection है। Object unused होते ही memory तुरंत OS को वापस नहीं मिलती। GC allocation स्थिति, heap thresholds, memory pressure, generations और workload देखकर चलता है।

इसलिए ये स्थितियाँ बनती हैं।

  • Unused object अभी GC से collect नहीं हुआ
  • GC हो चुका है, पर process का Working Set तुरंत नहीं गिरता
  • First access, JIT, cache, connection pool से एक बार बढ़कर फिर स्थिर हो जाता है
  • Managed heap स्थिर है, पर native memory, threads, sockets या image-processing library की तरफ बढ़ रहा है
  • वाकई, unused होना चाहिए था वह object कहीं से अभी भी referenced है

यह लेख आखिरी वाले मामले — «वाकई leak हो रहा है» — को कैसे अलग करें, यही समझाता है। देखने वाली चीज़ सिर्फ memory usage नहीं, ये तीन बातें हैं।

  1. GC के बाद surviving memory बढ़ रही है या नहीं
  2. बढ़ रहा type कौन-सा है
  3. उस object को कौन reference कर रहा है

.NET की memory leak investigation «memory बढ़ रही है» पर खत्म नहीं होती। लक्ष्य यह है: «इस type के objects बढ़ रहे हैं, और इस root से continuously referenced हैं»।

इस लेख का code, build और run हो सकने वाले sample set के रूप में GitHub पर है (typical leak patterns की library, GC wait बनाम survivors का अंतर देखने वाला demo, और WeakReference से retention/collection verify करने वाले unit tests)।

dotnet-gc-or-memory-leak - komurasoft-blog-samples (GitHub)

इस लेख का knowledge map

.NET app में memory बढ़ना दो स्थितियों में बँटता है: सिर्फ GC ने अभी collect नहीं किया, या static collection, unbounded cache, event unsubscribe न करना, Timer dispose न करना, IDisposable release न करना, DI lifetime mismatch जैसी वजहों से reference सच में बचा रहता है। अलग करने के लिए पहले dotnet-counters से GC heap, Gen 2, और LOH का trend देखें; cumulative value Total Allocated या OS की नज़र का Working Set बढ़ने भर से निष्कर्ष न निकालें। फिर dotnet-gcdump से load के पहले और बाद type-wise Count और Size compare करके बढ़ रहे types पहचानें, और dotnet-dump के dumpheap व gcroot commands से यह तक पहुँचें कि उस object को कौन continuously reference कर रहा है। Forced GC से memory गिरे तो भी root cause नहीं मिटता, और production में डालने वाला उपाय नहीं बनता।

.NET में GC wait और memory leak को अलग करने का knowledge map.NET में memory बढ़ना GC wait है या genuine memory leak, इसे dotnet-counters, dotnet-gcdump, dotnet-dump और gcroot से अलग करने का relation दिखाने वाला diagramसे verifyसे verifyसे verifyसे verifyसे verifyसे verifyसे verifyrecommended नहींrecommended नहींrecommended नहींcause हो सकताcause हो सकताcause हो सकताcause हो सकताcause हो सकताcause हो सकताcause हो सकतापहले करना चाहिएपहले करना चाहिएincompatibleincompatibleuse करता हैrequire करता है.NET का garbage collection (GC).NET का memory leak (unintended retention)dotnet-countersdotnet-gcdumpdotnet-dumpgcroot commandGC Heap (managed heap)dumpheap -stat commanddotnet-traceGen 2 heapTotal Allocated (cumulative allocation)Working Set / RSSforced GC (Induced Collection)static collection में hold रहनाsize या expiry limit के बिना unbounded cacheevent unsubscribe न करने से leakTimer dispose न करने से leakIDisposable release न करने से leakDI lifetime का mismatchLOH (Large Object Heap).NET Framework.NET (Core और उसके बाद)

Diagram में solid line हमेशा लागू रहने वाला relation दिखाती है और dashed line conditional relation दिखाती है (शर्तें detail page पर हर relation के explanation में दी गई हैं)। Relations की पूरी list (कुल 23, evidence और certainty सहित) तथा मुख्य concepts की definitions knowledge map की detail page पर संकलित हैं (जापानी में)। Data: JSON-LD / Turtle

2. पहले “memory leak” का मतलब एक जैसा करें

.NET में memory leak सिर्फ C/C++ वाला रूप नहीं है — «allocate की memory free करना भूल गए»।

Managed code में GC objects collect करता है। GC collect कर पाएगा या नहीं, यह इस पर तय होता है कि उस object तक पहुँचने वाला reference अभी बचा है या नहीं।

यानी .NET का typical memory leak यह है:

Business logic के हिसाब से object की अब ज़रूरत नहीं, फिर भी static field, cache, event, Timer, collection, DI lifetime, async context आदि से referenced रहता है, इसलिए GC को वह अभी भी in-use दिखता है।

GC होशियार है, पर business के हिसाब से ज़रूरत है या नहीं, यह वह नहीं जानता। Reference है तो alive मानता है।

इसलिए .NET में «leak» से ज़्यादा «unintended retention» सोचना आसान हो जाता है।

दूसरी ओर, नीचे की स्थितियाँ तुरंत memory leak नहीं कहलातीं।

स्थिति Leak न होने का कारण
Working Set / RSS बढ़ रहा है यह OS ने process को दी हुई memory है; managed heap पर live objects की मात्रा से मेल नहीं खाती
Total Allocated बढ़ रहा है Startup के बाद allocated cumulative मात्रा है, इसलिए app कुछ भी करे तो आमतौर पर बढ़ती है
GC Heap पल भर के लिए बढ़ जाता है अगले GC तक uncollected objects बचे हो सकते हैं
Startup के तुरंत बाद बढ़ता है JIT, type load, initial cache, connection pool, template expansion से अक्सर होता है
LOH बड़ा है बड़े arrays/buffers का reuse, fragmentation, या pooling strategy का असर हो सकता है
Memory नहीं गिरती GC collect कर ले, तब भी process तुरंत OS को memory लौटाए यह ज़रूरी नहीं

उल्टा, नीचे की बातें जितनी एक साथ दिखें, memory leak का शक उतना मजबूत होता है।

Observation मतलब
वही operation दोहराते ही, GC के बाद heap बढ़ता है Surviving objects बढ़ रहे हैं
Gen 2 या LOH का size बढ़ता रहता है Long-lived objects, या बड़े objects, टिके हुए हैं
कई dumps में उसी type का Count / Size बढ़ता है बढ़ रहा type पहचाना जा सकता है
gcroot पर static, event, cache, या long-lived service से reference दिखता है GC collect क्यों नहीं कर पा रहा, यह explain हो जाता है
Load रोकने के बाद भी, काफी समय या verification GC के बाद वापस नहीं आता सिर्फ temporary allocation होने की संभावना कम है

3. आप कौन-सी “memory” देख रहे हैं, यह अलग करें

Memory investigation में पहली उलझन यह है कि कई memory metrics गड्डमड्ड हो जाते हैं। सब «memory» कहलाते हैं, अर्थ अलग हैं।

Metric क्या मापता है कैसे पढ़ें
Working Set / RSS Physical memory पर resident process pages OS की नज़र से memory। GC heap स्वयं नहीं
Private Bytes / Commit Process की अपनी committed memory Native memory, stacks, JIT code, GC segments भी शामिल
GC Heap Size Managed heap पर objects की मात्रा .NET की GC-managed memory देखने का entry point
Total Allocated Startup के बाद allocated cumulative मात्रा आमतौर पर बढ़ती रहती है। Leak decide करने के लिए अकेले इस्तेमाल न करें
Gen 0 / Gen 1 / Gen 2 Generation-wise heaps Gen 2 में जो बचा है, वह long-lived है
LOH 85,000 bytes या उससे बड़े objects वाला heap बड़े arrays, strings, buffers से बढ़ता है
POH Pinned objects वाला heap Native interop और pinning का असर देखने का clue
Finalization Queue Finalization की wait कर रहे objects Missed Dispose, या finalizer jam का clue

कौन-सा metric कहाँ देख रहा है, diagram में ऐसा दिखता है।

Process memory, managed heap, native, Commit और Working SetProcess memory managed heap और native में बँटती है, दोनों Private Bytes / Commit में count होते हैं, और Working Set / RSS केवल physically resident हिस्सा दिखाता हैcounted incounted inonly physically resident partProcess की इस्तेमाल हो रही memoryManaged sideGC Heap Size में दिखने वाला हिस्साNative sideGC Heap Size में न दिखने वाला हिस्साGen 0 / Gen 1short-lived allocationsGen 2survive किए long-lived objectsLOH85,000 bytes या उससे बड़े objectsPOHpinned objectsThread stacksJIT code, loaded assembliesP/Invoke, COM, external library buffersPrivate Bytes / Commitprocess-private committed memoryWorking Set / RSS

इस diagram से ये दो बातें याद रखें।

  1. dumpheap से दिखता है सिर्फ managed side। Native side बढ़ रहा हो तो heap जितना भी देखो, culprit नहीं निकलता।
  2. Working Set और Commit nested relationship नहीं हैं। Committed होने पर भी physical memory पर न हो तो Working Set में नहीं आता, और उल्टा shared library pages जैसे non-private pages Working Set में गिने जा सकते हैं। «Working Set नहीं गिर रहा, इसलिए GC collect नहीं कर रहा» — यह निष्कर्ष इसी वजह से नहीं निकाला जा सकता।

शुरुआत से हर metric को गहराई से देखने की ज़रूरत नहीं। पहले सवाल को तोड़ें:

Process की memory बढ़ रही है
  ↓
Managed heap भी बढ़ रहा है?
  ↓
GC के बाद surviving मात्रा बढ़ रही है?
  ↓
कौन-सा type बढ़ रहा है?
  ↓
कौन reference कर रहा है?

यह क्रम रखने से «दिखने वाली memory growth» और «वास्तविक leak» को confuse करना आसान नहीं रहता।

4. Decision flow

Practice में इस flow से triage आसान रहता है।

1. Reproduction conditions तय करें
   - कौन-सा API, screen, job, या batch बढ़ाने वाला है
   - कितनी बार चलाने पर बढ़ता है
   - Load रोकने पर क्या होता है

2. dotnet-counters से trend देखें
   - Working Set
   - GC Heap
   - Gen 2 / LOH
   - Total Allocated
   - GC counts

3. Time-separated comparison करें
   - Startup के तुरंत बाद
   - Warmup के बाद
   - Load के दौरान
   - Load रुकने के बाद
   - वही operation N बार दोहराने के बाद

4. Dump दो या ज़्यादा बार लें
   - before
   - after
   - हो सके तो load रुकने के बाद भी

5. बढ़ रहे types खोजें
   - dumpheap -stat
   - gcdump report
   - Visual Studio / PerfView

6. Reference source देखें
   - gcroot
   - gchandles
   - finalizequeue

7. Decide करें
   - GC wait
   - Normal cache growth
   - Managed memory leak
   - Native memory की समस्या
   - LOH fragmentation या temporary large allocation

महत्वपूर्ण यह है कि एक measurement से decide न करें। Memory leak «बढ़ते रहने का trend» है, इसलिए एक point नहीं, same conditions पर time-separated comparison करें।

5. इस्तेमाल होने वाले tools

इस लेख में मुख्यतः ये tools हैं।

Tool कब इस्तेमाल करें
dotnet-counters Running process के GC और Working Set का trend देखने के लिए
dotnet-gcdump Live managed objects की statistics हल्के ढंग से लेने के लिए
dotnet-dump Heap विस्तार से देखने और dumpheap / gcroot से reference तक पहुँचने के लिए
Visual Studio Memory Usage Windows पर GUI comparison चाहिए हो
PerfView Windows पर GC / heap / trace गहराई से देखना हो
dotnet-trace Allocations और GC events को timeline पर follow करना हो

पहले CLI tools install करें।

dotnet tool install --global dotnet-counters
dotnet tool install --global dotnet-dump
dotnet tool install --global dotnet-gcdump
dotnet tool install --global dotnet-trace

पहले से हों तो update करें।

dotnet tool update --global dotnet-counters
dotnet tool update --global dotnet-dump
dotnet tool update --global dotnet-gcdump
dotnet tool update --global dotnet-trace

Investigation का target process खोजें।

dotnet-counters ps

आगे के examples में target process ID <PID> लिखा है।

Linux, macOS, और container environments में diagnostic tools और target process एक ही user पर चलने चाहिए। Environment के हिसाब से TMPDIR, diagnostic port, और container के PID namespace का असर भी पड़ता है।

Production पर चलाना हो तो सीधे dump न लें। पहले test environment में load और impact देखें।

5.1 अगर target .NET Framework 4.x हो

dotnet-counters, dotnet-dump, dotnet-gcdump वे tools हैं जो .NET Core 3.0 और बाद के runtime के diagnostic features इस्तेमाल करते हैं। Target .NET Framework 4.x application हो तो ये काम नहीं करते। Maintenance का target Windows Forms, WPF, या ASP.NET का पुराना app हो तो यही मामला है।

Mapping इस तरह सोचें।

इस लेख का tool .NET Framework 4.x पर विकल्प
dotnet-counters से trend देखना Performance Monitor, या Get-Counter से .NET CLR Memory category के counters
dotnet-gcdump से type statistics compare करना PerfView का GC heap dump, या Visual Studio «Memory Usage» से snapshots compare करना
dotnet-dump collect से dump लेना ProcDump, Task Manager का «Create dump file», या Windows Error Reporting settings से dump निकालना
dotnet-dump analyze पर dumpheap / gcroot WinDbg में .loadby sos clr चलाकर !dumpheap -stat और !gcroot
dotnet-trace से allocations follow करना PerfView की GC heap allocation collection

सोच वही है: «trend देखो», «type statistics दो बार compare करो», «reference source तक जाओ» — तीन steps। Tools बदलते हैं, तरीका नहीं।

Performance Monitor पर .NET CLR Memory category में पहले ये counters देखें।

Counter क्या देखता है
# Bytes in all Heaps Gen 1, Gen 2, LOH का योग। इस लेख के GC Heap Size के करीब
Gen 2 heap size Gen 2 के वर्तमान bytes। बढ़ता रहे तो leak का शक
Large Object Heap size LOH का वर्तमान size
# Gen 2 Collections Full GC की संख्या। अचानक तेज़ बढ़े तो over-allocation का शक
% Time in GC हाल के GC cycle में GC पर गया समय का अनुपात
Finalization Survivors Finalization की wait में survive किए objects। Missed Dispose का clue
# Total committed Bytes GC द्वारा committed virtual memory

Localized Windows पर ये counter names local language में दिख सकते हैं। न मिलें तो English name से search करने के बजाय localized category name भी देखें, जैसे .NET CLR メモリ

WinDbg analysis का entry point SOS load करना है।

0:000> .loadby sos clr
0:000> !dumpheap -stat
0:000> !gcroot <OBJECT_ADDRESS>

.NET Framework में SOS commands पर ! लगता है। इस लेख के section 9 के बाद आने वाले dumpheap -stat और gcroot को !dumpheap -stat, !gcroot पढ़ लें — procedure वही रहती है।

6. पहले dotnet-counters से trend देखें

पहले detailed dump नहीं, trend देखें।

dotnet-counters monitor \
  --process-id <PID> \
  --refresh-interval 3 \
  --counters System.Runtime

Output .NET version के हिसाब से थोड़ा बदलता है। .NET 9 और बाद में System.Runtime Meter names दिख सकते हैं, .NET 8 और पहले traditional EventCounter names।

मुख्यतः ये items देखें।

Item क्या देखें
dotnet.process.memory.working_set OS की नज़र से process resident memory
dotnet.gc.last_collection.heap.size हाल के GC के बाद generation-wise heap size
dotnet.gc.last_collection.memory.committed_size GC द्वारा committed memory
dotnet.gc.heap.total_allocated Startup के बाद cumulative allocated मात्रा
dotnet.gc.collections Generation-wise GC counts
dotnet.gc.pause.time GC pause time का cumulative

पहले subset पर monitor करना ठीक है।

dotnet-counters monitor \
  --process-id <PID> \
  --refresh-interval 3 \
  --counters System.Runtime[dotnet.process.memory.working_set,dotnet.gc.last_collection.heap.size,dotnet.gc.last_collection.memory.committed_size,dotnet.gc.heap.total_allocated,dotnet.gc.collections]

बाद में देखने के लिए CSV में save करें।

dotnet-counters collect \
  --process-id <PID> \
  --refresh-interval 5 \
  --format csv \
  --output counters.csv \
  --counters System.Runtime

इस बिंदु पर ये फर्क देखने हैं।

6.1 सिर्फ Total Allocated बढ़े

dotnet.gc.heap.total_allocated cumulative है। Application request handle करे तो objects allocate होते हैं, और वे objects तुरंत unused होकर GC से collect हो जाएँ, तब भी cumulative allocated मात्रा बढ़ती है।

इसलिए Total Allocated बढ़ना अकेले memory leak नहीं है। देखने वाली बात यह है: allocate करने के बाद बचा या नहीं।

Total Allocated: बढ़ता है
GC Heap Size:    कुछ ऊपर-नीचे होते हुए स्थिर रहता है
Gen 2 / LOH:     बढ़ते नहीं रहते

यह leak से ज़्यादा high-allocation application है।

Leak fix नहीं, allocation कम करना है — buffer reuse, LINQ का ज़्यादा इस्तेमाल कम करना, string generation घटाना, serialization review, आदि।

6.2 Working Set बढ़े, GC Heap स्थिर रहे

Working Set या RSS बढ़ रहा हो, पर GC Heap स्थिर हो — यह हो सकता है। तब managed object leak होना ज़रूरी नहीं।

संभावित कारण ये हैं।

  • JIT code
  • Loaded assemblies
  • Thread stacks
  • Native library memory
  • Marshal.AllocHGlobal जैसी unmanaged memory
  • Image, compression, crypto, DB driver जैसे native buffers
  • Socket, file handle, SSL, HTTP/2, gRPC जैसे internal buffers
  • OS ने process से physical pages अभी वापस नहीं लिए

इस स्थिति में dumpheap जितना भी देखो, मुख्य culprit न मिले।

Decision का अंदाज़ा यह है।

Working Set / RSS: बढ़ता है
GC Heap Size:      स्थिर
Gen 2 / LOH:       स्थिर

यह .NET managed heap leak नहीं। Native memory, handles, thread count, sockets, external libraries पर शक करें।

dotnet-counters पर रुकें नहीं। OS tools, container metrics, handle count, thread count, native heap, external library metrics भी देखें।

6.3 GC Heap बढ़े, पर load रुकने के बाद वापस आए

Load के दौरान GC Heap बढ़ना स्वाभाविक है।

Requests ज़्यादा हैं। Temporary objects ज़्यादा हैं। बड़ा JSON handle हो रहा है। Temporary lists या arrays बन रहे हैं।

ऐसे में अगले GC तक heap बढ़ता है। Load रोकें तो GC चलता है, heap वापस आ सकता है।

Load के दौरान:     GC Heap बढ़ता है
Load रुकने के बाद: GC Heap गिरता है, या एक स्थिर मान पर लौटता है
दोहराने के बाद:    Baseline बढ़ता नहीं रहता

यह «अभी collect नहीं हुआ» या «temporary allocation ज़्यादा है» माना जा सकता है।

हाँ, load के दौरान temporary allocation बहुत ज़्यादा हो तो GC counts और pause time बढ़कर performance समस्या बनती है। Leak न हो, तब भी performance improvement का विषय बन सकता है।

6.4 GC के बाद Gen 2 / LOH बढ़ते रहें

ध्यान इसी pattern पर है।

वही operation दोहराएँ
  ↓
Gen 2 बढ़ता है
  ↓
LOH बढ़ता है
  ↓
Load रोकने पर भी वापस नहीं आता
  ↓
अगली measurement में और बढ़ता है

Gen 2 वह generation है जहाँ long-lived objects जाते हैं। LOH वह heap है जहाँ बड़े arrays और strings आसानी से जाते हैं।

यहाँ बढ़ता रहे तो leak, unbounded cache, huge buffer retention, missed event unsubscribe, static collection, long-lived service retention पर शक करें।

इस चरण के बाद आगे बढ़ें।

7. “GC wait” है या नहीं, यह कैसे सोचें

«अभी सिर्फ collect नहीं हुआ» यह देखने के लिए, काफी GC opportunities के बाद की स्थिति देखें।

Production code में आसानी से GC.Collect() न डालें।

GC.Collect() GC को force करता है। खासकर all-generation blocking GC application में pause time पैदा करता है। सामान्य operations में GC पर छोड़ना मूल नियम है।

Investigation में, controlled test environment पर «forced GC के बाद भी बचा है?» देखना होता है।

Diagnostic console app या reproduction environment हो तो इस तरह के code से full GC के बाद की स्थिति देख सकते हैं।

static void ForceFullGcForDiagnosticsOnly()
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
}

मुख्य बात: इसे solution की तरह इस्तेमाल न करें। सिर्फ investigation के लिए है।

देखना यह flow है।

Operation से पहले
  ↓
Operation N बार दोहराएँ
  ↓
Load रोकें
  ↓
काफी wait करें, या test environment में full GC induce करें
  ↓
GC के बाद heap operation-से-पहले के करीब लौटा या नहीं

लौटे तो GC wait या temporary allocation की संभावना ज़्यादा है। न लौटे, और वही operation दोहराने पर baseline उठता रहे, तो कुछ survive कर रहा है। वह «कुछ» dump से खोजें।

8. dotnet-gcdump से हल्की comparison

पहली comparison के लिए dotnet-gcdump सुविधाजनक है।

dotnet-gcdump running .NET process से GC dump लेता है, heap पर type-wise statistics देखने के लिए।

dotnet-gcdump collect --process-id <PID> --output before.gcdump

Load के बाद एक बार और लें।

dotnet-gcdump collect --process-id <PID> --output after.gcdump

CLI से सरल report भी देख सकते हैं।

dotnet-gcdump report before.gcdump > before-heap.txt
dotnet-gcdump report after.gcdump  > after-heap.txt

देखें type-wise Count और Size

उदाहरण: after में ये types बहुत बढ़े हों तो investigation target बनते हैं।

Size (Bytes)   Count       Type
============   =====       ====
180,000,000    2,000,000   System.String
120,000,000    1,000,000   MyApp.Models.Customer
 90,000,000       25,000   System.Byte[]

महत्वपूर्ण «बड़ा type» नहीं, «बढ़ा हुआ type» है।

System.String और System.Byte[] कई apps में ऊपर आते हैं। ऊपर होना अकेले culprit नहीं बनाता।

Comparison का नज़रिया यह है।

before → after कैसे पढ़ें
Count लगभग वही उस type के मुख्य culprit होने की संभावना कम
Count और Size दोनों बढ़ें उम्मीदवार बनता है
MyApp.* types बढ़ें Business logic वाली retention का शक आसान है
System.Byte[] बढ़े Buffer, serialization, image, compression, HTTP, DB पर शक करें
System.String बढ़े Cache, logs, JSON, dictionary keys, duplicate strings पर शक करें
Task, Timer, CancellationTokenSource बढ़ें Async processing, Timer, missed cancellation cleanup पर शक करें

dotnet-gcdump comparison का अच्छा entry point है, पर capture के समय Gen 2 GC induce करता है। Heap बड़ा हो या latency सख्त हो, तो pause time और extra memory consumption पर ध्यान दें।

Windows पर .gcdump Visual Studio या PerfView में खोलकर compare कर सकते हैं। Non-Windows पर CLI report से type statistics देखें, reference की गहराई dotnet-dump पर ले जाना practical है।

9. dotnet-dump से heap और references देखें

«बढ़ रहा type» दिखने लगे तो अगला सवाल: «collect क्यों नहीं हो रहा?»।

इसके लिए dotnet-dump से dump लें, SOS commands से analyse करें।

dotnet-dump collect \
  --process-id <PID> \
  --type Heap \
  --output myapp-1.dmp

थोड़ा समय बाद एक बार और लें।

dotnet-dump collect \
  --process-id <PID> \
  --type Heap \
  --output myapp-2.dmp

Dump लेना भारी operation है। खासकर Full / Heap dump size बड़ा होता है, process और container पर load पड़ता है। Production पर लें तो timing, disk space, container memory limit, personal/sensitive data के मिल जाने पर ध्यान दें।

लिया हुआ dump analyse करें।

dotnet-dump analyze myapp-2.dmp

पहले पूरे heap की statistics देखें।

> dumpheap -stat

Output type-wise count और size है।

MT               Count       TotalSize Class Name
00007f...        120000      3840000   MyApp.Models.Order
00007f...        250000      8000000   System.String
00007f...         10000     40000000   System.Byte[]

किसी type पर filter करें।

> dumpheap -stat -type MyApp.Models.Order

या किसी MethodTable पर।

> dumpheap -mt <MT>

Instance का address मिल जाए तो reference source देखें।

> gcroot <OBJECT_ADDRESS>

यही सबसे महत्वपूर्ण कदम है। gcroot बताता है कि object alive क्यों है।

मान लीजिए ऐसा reference path दिखे।

static MyApp.CustomerCache._items
  -> System.Collections.Concurrent.ConcurrentDictionary<string, Customer>
  -> MyApp.Models.Customer
  -> System.String

यहाँ GC collect न करने का कारण साफ है। Customer static cache से referenced है, इसलिए GC को वह अभी in-use दिखता है।

यहीं से ये सवाल पूछे जा सकते हैं।

  • वह cache वाकई ज़रूरी है?
  • Size limit है?
  • Expiry है?
  • Key बढ़ते रहने वाला design तो नहीं?
  • Tenant, user, date, request ID आदि को key बनाकर अनंत बढ़ तो नहीं रहा?

Memory leak investigation dumpheap -stat पर खत्म नहीं होती। dumpheap -stat बताता है «क्या ज़्यादा है», gcroot बताता है «क्यों बचा है»। Fix तक ले जाने वाला दूसरा है।

10. फर्क करने की quick reference

Practice में आम patterns।

Observation संभावना अगला क्या देखें
सिर्फ Total Allocated बढ़े Normal allocation, या over-allocation Allocation Rate, GC counts, CPU, dotnet-trace
Working Set बढ़े, GC Heap स्थिर Native memory, JIT, stacks, OS-side retention Thread count, handle count, native tools, external libraries
GC Heap सिर्फ load पर बढ़े, रुकने पर लौटे GC wait, temporary allocation Load रुकने के बाद Gen 2 / LOH, GC counts
GC के बाद Gen 2 बढ़ता रहे Long-lived object retention dumpheap -stat, gcroot
LOH बढ़ता रहे बड़े arrays, buffers, fragmentation, huge strings System.Byte[], System.Char[], LOH, Free regions
System.String बड़ा हो String cache, JSON, logs, dictionary keys Strings रखने वाले अपने types खोजें
System.Byte[] बड़ा हो Buffer, serialization, image, compression, networking Owner type, ArrayPool return miss, native interop
Task बढ़े Incomplete async, wait queues Async waits, cancellation, channels, queues
Timer बढ़े Missed Timer dispose Dispose, unregister, long-lived services
CancellationTokenSource बढ़े Missed CTS dispose, बहुत linked tokens Dispose, unlink, timeout बनाने वाली जगहें
EventHandler या delegate बचे Missed event unsubscribe Publisher / subscriber lifetime का अंतर
Finalization Queue बढ़े Missed Dispose, finalizer jam finalizequeue, finalizer thread
Pinned handles ज़्यादा Pinned buffers, native interop gchandles, POH, pinning वाली जगहें

11. आम leak patterns

सात patterns हैं, ऊपर से क्रम से पढ़ना ज़रूरी नहीं। जो symptom आप देख रहे हैं उसके करीब की पंक्ति से शुरू करें।

Section Pattern Typical symptom पहले कौन-सा metric देखें
11.1 static collection Operation count के अनुपात में बढ़े, load रोकने पर भी न लौटे Gen 2। gcroot पर static field आए या नहीं
11.2 Unbounded cache Uptime के अनुपात में बढ़े। Restart पर लौटे Gen 2। Cache entries और System.String की वृद्धि
11.3 Missed event unsubscribe Screen या scope खोल-बंद करने पर बढ़े संबंधित ViewModel या handler type का Count। Delegate के रास्ते gcroot
11.4 Missed Timer dispose Short-lived समझने वाले objects न हटें, callbacks भी चलते रहें System.Threading.Timer या TimerQueueTimer का Count
11.5 Missed IDisposable release GC Heap स्थिर, फिर भी handle count या process memory बढ़े Handle count, Finalization Queue, Working Set
11.6 AsyncLocal या context retention Request खत्म होने पर भी DTO बचा रहे Async state machine के रास्ते gcroot
11.7 DI lifetime mismatch Request count के अनुपात में बढ़े Singleton type से gcroot

«पहले कौन-सा metric देखें» कॉलम section 10 की quick reference के साथ जोड़ी है। Section 10 «observation से संभावना सीमित करने वाली तालिका» है, यह तालिका «pattern से वापस उन metrics पर जाने वाली तालिका» है।

11.1 static collection

सबसे साफ pattern।

public static class CustomerStore
{
    private static readonly List<Customer> Customers = new();

    public static void Add(Customer customer)
    {
        Customers.Add(customer);
    }
}

इस code में Customers में जोड़े गए Customer process जीवित रहने तक बने रहते हैं। Temporary store का इरादा हो, static से referenced रहने तक GC collect नहीं करता।

Fix की दिशा इस्तेमाल पर निर्भर है।

  • Size limit लगाएँ
  • Expiry लगाएँ
  • MemoryCache जैसा cache mechanism इस्तेमाल करें
  • Explicitly हटाएँ
  • Static छोड़कर सही lifetime वाली service पर ले जाएँ
  • Persistence मकसद हो तो DB या external storage पर ले जाएँ

महत्वपूर्ण बात «static बुरा है» नहीं। Static पर रखा हुआ long-lived हो जाता है — यह गुण समझकर इस्तेमाल करें।

11.2 Unbounded cache

Cache जान-बूझकर memory इस्तेमाल करता है, इसलिए design के मुताबिक बढ़ना leak नहीं। लेकिन size limit या expiry के बिना cache व्यावहारिक memory leak बन जाता है।

public sealed class ReportCache
{
    private readonly Dictionary<string, Report> _cache = new();

    public Report GetOrCreate(string userId, DateTime date)
    {
        var key = $"{userId}:{date:O}";

        if (_cache.TryGetValue(key, out var report))
        {
            return report;
        }

        report = BuildReport(userId, date);
        _cache[key] = report;
        return report;
    }
}

इस उदाहरण में userId और date के combinations बढ़ते रहें तो cache भी बढ़ता रहता है।

खासकर खतरनाक वे keys हैं जिनमें ये हों।

  • Request ID
  • वर्तमान समय
  • GUID
  • Session ID
  • User input बिना normalize किए
  • SQL या search criteria को ज्यों का त्यों string बनाना

Cache के लिए ये शर्तें पहले तय करें।

शर्त उदाहरण
Max entries 10,000 तक
Max size 256MB तक
Expiry Last access से 30 मिनट
Absolute expiry बनाने से 6 घंटे
Eviction / drop Tenant delete, user delete, settings change
Watch Count, estimated size, hit rate, eviction count

«Cache है इसलिए बढ़ना ठीक है» नहीं — «कहाँ तक बढ़ना ठीक है» तय करना ज़रूरी है।

11.3 Missed event unsubscribe

Event तब leak बनता है जब long-lived publisher short-lived subscriber को referenced रखे।

public sealed class OrderViewModel
{
    private readonly OrderService _service;

    public OrderViewModel(OrderService service)
    {
        _service = service;
        _service.OrderChanged += OnOrderChanged;
    }

    private void OnOrderChanged(object? sender, OrderChangedEventArgs e)
    {
        // update view model
    }
}

OrderService singleton हो और OrderViewModel हर screen पर बने, तो OrderService का event OrderViewModel को referenced रखता है। Screen बंद करने पर भी unsubscribe न हो तो ViewModel बच जाता है।

Fix का उदाहरण।

public sealed class OrderViewModel : IDisposable
{
    private readonly OrderService _service;

    public OrderViewModel(OrderService service)
    {
        _service = service;
        _service.OrderChanged += OnOrderChanged;
    }

    public void Dispose()
    {
        _service.OrderChanged -= OnOrderChanged;
    }

    private void OnOrderChanged(object? sender, OrderChangedEventArgs e)
    {
        // update view model
    }
}

gcroot पर यह delegate या event handler के रास्ते का reference दिख सकता है।

यह pattern WPF, WinForms, long-lived services, message brokers, event aggregators में अक्सर आता है।

11.4 Missed Timer dispose

System.Threading.Timer, PeriodicTimer, Reactive Extensions की subscription भी dispose न करने पर बची रहती हैं।

public sealed class PollingWorker
{
    private readonly Timer _timer;

    public PollingWorker()
    {
        _timer = new Timer(_ => Poll(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
    }

    private void Poll()
    {
        // polling
    }
}

यह PollingWorker temporary object माना गया हो तो Timer dispose करने वाला design चाहिए।

public sealed class PollingWorker : IDisposable
{
    private readonly Timer _timer;

    public PollingWorker()
    {
        _timer = new Timer(_ => Poll(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
    }

    public void Dispose()
    {
        _timer.Dispose();
    }

    private void Poll()
    {
        // polling
    }
}

Timer callback का delegate रखता है, और वहाँ से target object तक reference जुड़ सकता है।

11.5 Missed IDisposable release

IDisposable release miss हमेशा managed heap leak की तरह नहीं दिखता।

File, socket, DB connection, native handle, buffer जैसे resource problems के रूप में निकल सकता है।

public async Task<string> ReadAsync(string path)
{
    var stream = File.OpenRead(path);
    using var reader = new StreamReader(stream);
    return await reader.ReadToEndAsync();
}

इस उदाहरण में StreamReader stream बंद करता है, इसलिए अक्सर बड़ी समस्या नहीं बनती, पर ownership अस्पष्ट code में leak होता है।

मूल नियम: using / await using से ownership साफ रखें।

public async Task<string> ReadAsync(string path)
{
    await using var stream = File.OpenRead(path);
    using var reader = new StreamReader(stream);
    return await reader.ReadToEndAsync();
}

Missed Dispose ये symptoms देता है।

  • Handle count बढ़े
  • Sockets बढ़ें
  • Files बंद न हों
  • Native memory बढ़े
  • Finalization Queue बढ़े
  • GC Heap स्थिर हो, फिर भी process memory बढ़े

तब dumpheap काफी नहीं। OS handles, sockets, external library की स्थिति भी देखें।

11.6 AsyncLocal या context retention

AsyncLocal<T> सुविधाजनक है, पर अंदर बड़ी चीज़ रखें तो देर तक बच सकती है।

Log correlation ID जैसा छोटा value आमतौर पर समस्या नहीं। लेकिन user info, request body, बड़ा DTO, DB context जैसा रखें तो unintended retention बनता है।

public static class RequestContext
{
    public static readonly AsyncLocal<RequestInfo?> Current = new();
}

AsyncLocal async flow पर चलता है, इसलिए साधारण static field से ढूँढना मुश्किल हो सकता है।

अंदर छोटी, साफ चीज़ रखें, और ज़रूरत खत्म होने पर null पर वापस लाने वाला design सोचें।

11.7 DI lifetime mismatch

ASP.NET Core जैसे DI में singleton, scoped, transient की lifetimes अलग हैं।

Long-lived singleton per-request data रख ले तो request खत्म होने पर भी objects बच सकते हैं।

public sealed class AuditBuffer
{
    private readonly List<RequestAudit> _items = new();

    public void Add(RequestAudit item)
    {
        _items.Add(item);
    }
}

यह singleton हो तो _items app की lifetime जितना जीता है।

Design के तौर पर buffer करना हो तो size limit, flush, delete, backpressure चाहिए। सिर्फ «बाद में देख लेंगे» हो तो log या external storage पर भेजें।

12. LOH को खासकर गलत पढ़ना आसान है

LOH Large Object Heap का संक्षेप है। .NET में बड़े objects छोटे objects से अलग heap पर जाते हैं। Typical उदाहरण बड़ा array है।

var buffer = new byte[1024 * 1024 * 10]; // 10MB

LOH पर आम समस्याएँ ये तीन हैं।

  1. बड़े objects बार-बार बनाना
  2. बड़े objects देर तक पकड़े रखना
  3. बड़े objects बनाने-मिटाने से fragmentation

LOH बढ़ रहा है, इसलिए तुरंत leak नहीं। बड़े buffers reuse करने वाला design हो तो एक size तक बढ़कर स्थिर हो सकता है, और GC collect कर ले तब भी Working Set तुरंत गिरे यह ज़रूरी नहीं।

हाँ, ये स्थितियाँ शक करने लायक हैं।

  • System.Byte[] हर operation पर बढ़े
  • System.Char[] या huge String बढ़े
  • Image, PDF, Excel, ZIP, crypto, compression के बाद वापस न आए
  • ArrayPool<T>.Rent किए arrays return न किए जा रहे हों
  • बड़ी response पूरी memory पर लादी जा रही हो
  • MemoryStream.ToArray() बहुत इस्तेमाल हो

ArrayPool<T> इस्तेमाल करें तो हमेशा return करें।

var pool = ArrayPool<byte>.Shared;
var buffer = pool.Rent(1024 * 1024);

try
{
    // use buffer
}
finally
{
    pool.Return(buffer);
}

Pool में return करने से process की memory तुरंत गिरे, यह ज़रूरी नहीं। Pool reuse के लिए memory रख सकता है।

यहाँ भी देखने वाली बात: «बढ़ता रह रहा है?», «size limit है?», «reuse हो रहा है?»।

13. gcroot कैसे पढ़ें

gcroot दिखाता है कि कोई object कहाँ से referenced है।

Typical roots की तालिका।

Root मतलब
static field Type के static field से referenced
local variable / stack Running thread के stack से referenced
GC handle GCHandle, pin, delegate, interop आदि से referenced
finalization queue Finalization की wait में पकड़ा हुआ
thread / async state machine चल रहा या wait कर रहा async work पकड़े हुए है

Investigation में अक्सर देखने वाली बात lifetime का अंतर है।

Long-lived object
  -> Short-lived होना चाहिए था वह object

यह रूप दिखे तो leak candidate है।

उदाहरण के लिए यह संदिग्ध है।

SingletonService
  -> List<RequestContext>
  -> RequestContext
  -> LargeDto

SingletonService पूरी app के साथ जीता है। उसमें request-level RequestContext जमा हो रहे हों तो design दोबारा देखना होगा।

दूसरी ओर, ये roots timing के हिसाब से सामान्य हो सकते हैं।

Thread stack
  -> Controller action local variable
  -> RequestDto

Request processing चल रही हो तो local variable बचना स्वाभाविक है।

इसलिए dump का timing महत्वपूर्ण है।

सिर्फ load के दौरान नहीं — load रुकने के बाद, queue खाली होने के बाद, कुछ समय idle रखने के बाद का dump भी लें तो decide करना आसान होता है।

14. “Forced GC से गिर गया, इसलिए हल” नहीं है

Investigation में GC.Collect() चलाया, memory गिर गई। तब «तो नियमित GC.Collect() चला दें» सोचना खतरनाक है।

Forced GC root cause नहीं मिटाता। सिर्फ उन objects को उसी क्षण collect करता है जो अभी collect नहीं हुए थे।

High allocation rate समस्या हो तो forced GC pause time बढ़ाकर performance खराब करता है। Genuine leak हो तो referenced objects forced GC से भी collect नहीं होते।

Investigation में यह फर्क देखें।

Forced GC के बाद निर्णय
काफी गिरे, फिर baseline स्थिर GC wait, या temporary allocation मुख्य कारण
थोड़ा गिरे, पर दोहराने पर floor उठता रहे कुछ survive कर रहा है। Leak candidate
लगभग न गिरे Continuously referenced, या GC heap के बाहर मुख्य कारण
GC Heap गिरे, Working Set न गिरे OS / GC segments / native-side retention की संभावना

Production में GC.Collect() नियमित चलाने से पहले हमेशा पहचानें कि «क्या बढ़ रहा है»।

15. Practical investigation procedure

यहाँ से actual investigation procedure का summary है।

15.1 Reproduction scenario तय करें

पहले investigation conditions स्थिर करें।

Target:       /api/report/export
Operation:    वही conditions पर 100 बार चलाएँ
Interval:     5 सेकंड
Observation:  warmup 5 मिनट + load 10 मिनट + idle 5 मिनट
Environment:  staging / Release build / production-equivalent settings

Memory investigation में हर बार अलग operation करते हुए देखने से decide नहीं होता। «क्या करने पर बढ़ा» स्थिर करें।

15.2 Baseline लें

Startup के तुरंत बाद नहीं, warmup के बाद को baseline बनाएँ।

कारण: startup के तुरंत बाद ऐसी एक-बार की वृद्धि होती है।

  • JIT
  • DI container build
  • Settings load
  • पहली DB connection
  • पहली TLS / HTTP connection
  • JSON serializer metadata generation
  • Razor / template initialization
  • Logger और metrics initialization

क्रम यह रखें।

1. App start
2. Health check या representative API कुछ बार चलाएँ
3. 1–5 मिनट wait करें
4. Baseline के रूप में counters और dump लें

15.3 Load के दौरान counters लें

dotnet-counters collect \
  --process-id <PID> \
  --refresh-interval 5 \
  --format csv \
  --output report-export-counters.csv \
  --counters System.Runtime

साथ-साथ reproduction चलाएँ। देखने वाली चीज़ graph का आकार है।

सामान्य के करीब आकार:
  Load पर बढ़े
  GC से ऊपर-नीचे हो
  Load रुकने पर लौटे
  Baseline बढ़ता न रहे

संदिग्ध आकार:
  Operation count के अनुपात में बढ़े
  Gen 2 / LOH का floor उठे
  Load रुकने पर भी न लौटे
  अगले load पर floor और उठे

15.4 Dump दो बार लें

Load के पहले और बाद।

dotnet-dump collect --process-id <PID> --type Heap --output before.dmp
# load चलाएँ
dotnet-dump collect --process-id <PID> --type Heap --output after.dmp

गुंजाइश हो तो load रुकने के बाद भी लें।

# load रुकने के बाद, queue खाली हो, कुछ समय wait करके
dotnet-dump collect --process-id <PID> --type Heap --output idle-after.dmp

Compare करते समय before और after के अलावा idle-after महत्वपूर्ण है।

Load पर बढ़ा हो, idle के बाद लौटे, तो leak न होने की संभावना है।

15.5 बढ़े हुए types देखें

dotnet-dump analyze after.dmp
> dumpheap -stat

before तरफ भी वैसा ही देखें।

Manual comparison ठीक है; पहले ऊपर के types compare करें।

देखने वाले नज़रिए।

  • अपने namespace के types बढ़ रहे हैं?
  • System.String के पीछे अपना type तो नहीं?
  • System.Byte[] किसके पास है?
  • List<T> या Dictionary<TKey,TValue> तो नहीं बढ़ रहे?
  • Task या async state machine तो नहीं बढ़ रहे?
  • Timer या CancellationTokenSource तो नहीं बढ़ रहे?

15.6 Reference source देखें

Candidate object address उठाएँ, gcroot चलाएँ।

> dumpheap -type MyApp.Models.ReportResult
> gcroot <OBJECT_ADDRESS>

gcroot के नतीजे से पकड़े रखने वाला parent खोजें।

MyApp.Services.ReportCache
  -> Dictionary<string, ReportResult>
  -> ReportResult

यहाँ तक पहुँचें तो code review का विषय दिखने लगता है।

  • ReportCache singleton है?
  • Size limit है?
  • Delete होता है?
  • Keys बढ़ते रहते हैं?
  • ReportResult बहुत बड़ा तो नहीं?
  • Cache की जगह DB या file पर भेजना चाहिए?

16. dotnet-trace कब इस्तेमाल करें

dotnet-dump एक क्षण का snapshot है — «नतीजे में क्या बचा है» देखने के लिए उपयुक्त। दूसरी ओर, «कब, कहाँ बहुत allocate हो रहा है» देखना हो तो dotnet-trace इस्तेमाल करें।

उदाहरण: GC-related events सहित trace लें।

dotnet-trace collect \
  --process-id <PID> \
  --duration 00:00:01:00 \
  --clrevents gc+gchandle \
  --clreventlevel informational \
  --output gc-trace.nettrace

Allocation sampling तक देखना हो तो event मात्रा बढ़ती है, इसलिए test environment में छोटी अवधि से शुरू करें।

dotnet-trace collect \
  --process-id <PID> \
  --duration 00:00:00:30 \
  --clrevents gc+gcsampledobjectallocationhigh \
  --clreventlevel informational \
  --output allocation-trace.nettrace

Trace dump से अलग angle से मदद करता है।

क्या देखना है उपयुक्त tool
क्या बचा है dump / gcdump
कौन reference कर रहा है dump + gcroot
कब बहुत allocate हुआ trace
GC कब चला counters / trace
Pause time समस्या है? counters / trace

Leak investigation में पहले dump से «बचा हुआ» देखें, ज़रूरत हो तो trace से «बनाने वाली जगह» देखना efficient है।

17. Code में verification metrics निकालना

गंभीर diagnostics बाहरी tools से होने चाहिए, पर app में सरल diagnostic log रखना मदद करता है।

उदाहरण: admin endpoint या periodic log से GC जानकारी निकालना।

public static class GcDiagnostics
{
    public static object Snapshot()
    {
        var info = GC.GetGCMemoryInfo();

        return new
        {
            TotalMemory = GC.GetTotalMemory(forceFullCollection: false),
            HeapSizeBytes = info.HeapSizeBytes,
            FragmentedBytes = info.FragmentedBytes,
            MemoryLoadBytes = info.MemoryLoadBytes,
            HighMemoryLoadThresholdBytes = info.HighMemoryLoadThresholdBytes,
            Gen0Collections = GC.CollectionCount(0),
            Gen1Collections = GC.CollectionCount(1),
            Gen2Collections = GC.CollectionCount(2)
        };
    }
}

अकेले इस जानकारी से leak decide नहीं होता। हाँ, incident पर ये निर्णय आसान होते हैं।

  • Gen 2 अचानक बढ़ा है?
  • HeapSize बढ़ रहा है?
  • FragmentedBytes बढ़ रहा है?
  • TotalMemory और process memory का अंतर बड़ा है?
  • Deploy के बाद trend बदला है?

App log में डालें तो बहुत ज़्यादा न निकालें। High frequency पर भारी diagnostics स्वयं load बन जाते हैं।

18. “Memory leak” declare करने का criterion

Investigation के अंत में इस रूप में explain कर सकें। Report में उतारते समय भरने वाले कॉलम पहले रखें।

कॉलम क्या लिखें
Issue किसे दिक्कत है। किस operation पर कितना बढ़ता है
Observation conditions Environment, build configuration, data volume, run count, warmup time, observation interval, इस्तेमाल किए tools और versions
Observation Counters कैसे चले। Working Set और GC Heap अलग लिखें
Comparison before और after dumps में कौन-सा type कितनी entries बढ़ा
Reference source gcroot पर दिखा retention path
Cause Code के किस design ने वह retention पैदा किया
Fix क्या बदलेंगे। असर कैसे मापेंगे
Open items ज़रूरत हो तो। इस observation से explain न हुआ, और अगला क्या देखना है

Observation conditions न छोड़ना खासकर महत्वपूर्ण है। Conditions न लिखी हों तो fix के बाद re-measurement से compare नहीं हो पाता, और section 20 की verification meaningless हो जाती है।

इस रूप में भरें तो उदाहरण ऐसा बनता है।

Issue:
  /api/report/export 100 बार चलाने पर, load रुकने के बाद भी GC Heap 300MB बढ़ा रह जाता है।

Observation conditions:
  staging / Release build / production-equivalent settings और data volume.
  Warmup 5 मिनट के बाद, वही conditions पर 100 बार, फिर 5 मिनट idle.
  dotnet-counters 5 सेकंड interval पर collect.

Observation:
  dotnet-counters पर Gen 2 heap size operation count के अनुपात में बढ़ा.
  Working Set के साथ GC Heap भी बढ़ा.

Comparison:
  before.dmp और after.dmp compare करने पर MyApp.Models.ReportResult 12,000 entries बढ़ा.

Reference source:
  gcroot पर MyApp.Services.ReportCache._items से referenced था.

Cause:
  ReportCache singleton था, user ID + वर्तमान समय को key बनाता था, delete / expiry / size limit नहीं थे.

Fix:
  MemoryCache पर बदला, size limit और expiry सेट किए.
  Cache entry count को metric बनाया.

यहाँ तक explain हो जाए तो सिर्फ «memory बढ़ रही है» नहीं — reproduction conditions, observed values, बढ़ा type, reference source, cause, fix जुड़ा हुआ report बनता है।

19. Investigation के दौरान सावधानियाँ

19.1 Release build पर देखें

Debug build optimization, local variable lifetime, debug info के असर से production से अलग दिख सकता है।

Production-equivalent investigation में Release build, operations के करीब settings, करीब data volume पर देखें।

19.2 सिर्फ startup के तुरंत बाद से decide न करें

Startup के तुरंत बाद कई initializations से memory बढ़ती है।

Warmup के बाद baseline लें, वहाँ से बढ़ रहा है या नहीं देखें।

19.3 एक dump से culprit न बनाएँ

Heap पर ऊपर का type ज़रूरी नहीं culprit हो।

System.String और System.Byte[] कई apps में बड़े दिखते हैं।

महत्वपूर्ण: time-separated comparison में बढ़ा या नहीं, और किसके पास है।

19.4 Dump में sensitive data होता है

Memory dump में requests, credentials, connection strings, personal data, business data आ सकते हैं।

Storage, export, sharing, deletion के नियम तय करें।

19.5 Container में dump लेना स्वयं जोखिम है

Container memory limit सख्त हो तो dump की extra memory या page-in से OOM Kill हो सकता है।

Production container से पहले staging पर आज़माएँ, limit, disk space, permissions, PID namespace देखें।

19.6 GC Heap के बाहर भी leak होता है

.NET investigation है, इसलिए सब कुछ GC heap पर दिखे यह ज़रूरी नहीं।

इन समस्याओं में GC Heap स्थिर रहकर भी process memory बढ़ सकती है।

  • Native libraries
  • P/Invoke
  • COM
  • Image processing
  • Compression libraries
  • Crypto
  • DB drivers
  • Sockets
  • Marshal.AllocHGlobal
  • NativeMemory.Alloc
  • बहुत ज़्यादा threads

तब dotnet-dump का dumpheap काफी नहीं। OS-side diagnostics, external library metrics, handles, threads, native memory देखना पड़ता है।

19.7 Production dump से पहले stakeholders से तय कर लें

Production पर dump लेना technical operation से पहले coordination का काम है। इसे छोड़कर सिर्फ «investigation है, dump लेने दें» कहें तो आमतौर पर रुक जाता है।

पहले impact के तौर पर explain करने वाली बातें।

  • Dump लेना target process के लिए भारी है, दौरान response रुकता दिख सकता है। कितना रुकता है heap size, disk speed, environment पर निर्भर है, इसलिए staging पर वही procedure एक बार चलाकर माप लें।
  • Response रुकने का समय लंबा हो तो health-check timeout, load balancer से अलग होना, cluster failover ट्रिगर हो सकता है।
  • Full या Heap dump process की memory usage के हिसाब से बड़ा होता है। Write destination की खाली disk space पहले देखें।
  • Container में dump के page-in से memory limit पार होकर container kill हो सकता है (19.5)।
  • Dump में personal data या credentials आ सकते हैं (19.4)।

उसके बाद, लेने से पहले यह तय करें।

क्या तय करें उदाहरण
Approval कौन देगा Service owner और IT department। दोनों की सहमति पहले लें
कब लें Off-hours आदि, जहाँ अस्थायी response delay स्वीकार्य हो
कहाँ लिखें काफी खाली local disk। Shared folder पर सीधे न लिखें
Access किसका Storage की access rights investigation team तक सीमित करें
कब मिटाएँ Investigation पूरी होने के बाद deletion deadline पहले लिख लें
Fail हो तो क्या Response न लौटे तो restart procedure, और decide करने वाला व्यक्ति
Pre-check Staging पर वही procedure एक बार चलाएँ, duration और file size नोट करें

Request मौखिक न हो। यह सामग्री एक पन्ने पर लिखकर देना पक्का तरीका है। Format का उदाहरण।

Purpose:     /api/report/export के बाद memory न लौटने का कारण पहचानना
Method:      dotnet-dump collect --type Heap load के पहले और बाद, 2 बार
Impact:      दौरान target process की response धीमी हो सकती है.
             Staging की measured values अलग पन्ने पर attach
Window:      Off-hours. Owner और IT साथ बैठ सकें
Write to:    Target server की local disk. खाली space पहले देखें
Contains:    Memory पर request data, connection strings आदि आ सकते हैं
Handling:    Storage access सिर्फ investigation team. Export नहीं
Deletion:    Investigation पूरी होने के बाद, अधिकतम 1 महीने में मिटाएँ, deletion record रखें
Rollback:    Dump के बाद response न लौटे तो process restart करें

«किस लिए», «कितना रुकता है», «कहाँ रखेंगे और कब मिटाएँगे» एक साथ हों तो decide करने वाला हाँ/ना आसानी से दे पाता है। उल्टा यह अस्पष्ट हो तो investigation स्वयं रुक जाती है।

20. Fix के बाद verification

Leak जैसी जगह ठीक करने के बाद वही procedure से फिर मापें।

Fix से पहले:
  100 runs के बाद Gen 2 +300MB
  ReportResult +12,000 entries

Fix के बाद:
  100 runs के बाद Gen 2 +20MB के अंदर स्थिर
  ReportResult load रुकने के बाद baseline पर लौटता है
  Cache entries ऊपरी सीमा 1,000 पर स्थिर

Fix verify करते समय हमेशा same conditions पर compare करें।

  • वही data volume
  • वही count
  • वही load duration
  • वही warmup
  • वही observation interval
  • वही tools

Memory investigation में before/after comparison कमजोर हो तो report persuasive नहीं रहता।

21. सारांश

.NET में memory बढ़े तो तुरंत leak न कहें। इस क्रम से अलग करें।

  1. Working Set / RSS अकेले से decide न करें
  2. dotnet-counters से GC Heap, Gen 2, LOH, GC counts देखें
  3. Load के दौरान, load रुकने के बाद, time-separated compare करें
  4. dotnet-gcdump या dotnet-dump से बढ़ा type देखें
  5. gcroot से reference source देखें
  6. Static, cache, events, Timer, DI lifetime, async context जाँचें
  7. GC Heap स्थिर हो तो native memory या OS-side समस्या भी शक करें

«अभी collect नहीं हुआ» और «memory leak हो रहा है» का फर्क अंत में references से तय होता है।

Unused object referenced न हो तो GC के timing पर collect होता है। Unused होना चाहिए, फिर भी referenced रहे, तो GC collect नहीं कर सकता।

यानी investigation का लक्ष्य यह है।

क्या बढ़ रहा है?
हर GC के बाद भी बचा है?
कौन reference कर रहा है?
वह reference design के हिसाब से ज़रूरी है?

यहाँ तक पता चले तो memory graphs के पीछे नहीं भागते, code में ठीक करने वाली जगह तक पहुँच जाते हैं।

संदर्भ

निकटवर्ती विषयों में गहराई से जाने के लिए समान टैग वाले नवीनतम लेख।

ये पृष्ठ विषय को सेवाओं और निर्णयों के व्यापक संदर्भ में रखते हैं।

यह लेख निम्नलिखित सेवाओं से सीधे जुड़ा है।

अक्सर पूछे जाने वाले प्रश्न

इस लेख के विषय पर परामर्श में अक्सर पूछे जाने वाले प्रश्न।

.NET app की memory usage बढ़ रही है — क्या यह memory leak है?
Process की memory बढ़ना और memory leak होना एक ही बात नहीं है। .NET में GC allocation स्थिति और heap thresholds देखकर चलता है, इसलिए unused object अभी collect न हुआ हो, या GC के बाद भी OS तुरंत memory वापस न ले — ये दोनों हो सकते हैं। देखने वाली तीन बातें हैं: GC के बाद surviving memory बढ़ रही है या नहीं, कौन-सा type बढ़ रहा है, और उस object को कौन reference कर रहा है।
.NET memory leak जाँचने के लिए कौन-से tools इस्तेमाल करें?
पहले dotnet-counters से Working Set, GC Heap, Gen 2/LOH और GC counts का trend देखें। फिर dotnet-gcdump से load के पहले और बाद GC dump लें, type-wise Count और Size compare करके बढ़े हुए types पहचानें। अंत में dotnet-dump से heap dump लें, dumpheap -stat और gcroot से यह तक पहुँचें कि object collect क्यों नहीं हो रहा। एक measurement काफी नहीं; same conditions पर time-separated comparison ज़रूरी है।
.NET में आम memory leak patterns कौन-से हैं?
Typical examples: static collection में add करते रहना, size limit या expiry के बिना unbounded cache, long-lived publisher पर event unsubscribe न करना, Timer dispose न करना, IDisposable release न करना, और DI lifetime mismatch जहाँ singleton request-level data रख लेता है। .NET leak को «free करना भूल गए» से ज़्यादा «unintended retention» समझें — object की ज़रूरत नहीं, फिर भी reference बचा है।
GC.Collect() नियमित चलाएँ तो memory समस्या हल हो जाएगी?
नहीं। Forced GC सिर्फ उन objects को तुरंत collect करता है जो अभी collect नहीं हुए थे; root cause नहीं मिटता। High allocation rate हो तो pause time बढ़कर performance खराब होती है, और genuine leak में referenced objects forced GC से भी collect नहीं होते। Investigation के लिए controlled test environment में «forced GC के बाद भी बचा है?» देखना ठीक है, पर production में solution की तरह डालने से पहले हमेशा यह पहचानें कि क्या बढ़ रहा है।

लेखक की प्रोफ़ाइल

लेख के लेखक का परिचय पृष्ठ।

Go Komura

KomuraSoft LLC के प्रतिनिधि

Windows सॉफ़्टवेयर विकास, तकनीकी परामर्श और बग जाँच में विशेषज्ञ, विशेष रूप से मौजूदा सिस्टम वाली परियोजनाओं और पुनरुत्पादन में कठिन बग में।

सार्वजनिक लिंक

ब्लॉग पर लौटें