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 नहीं, ये तीन बातें हैं।
- GC के बाद surviving memory बढ़ रही है या नहीं
- बढ़ रहा type कौन-सा है
- उस 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 में डालने वाला उपाय नहीं बनता।
flowchart LR
accTitle: .NET में GC wait और memory leak को अलग करने का knowledge map
accDescr: .NET में memory बढ़ना GC wait है या genuine memory leak, इसे dotnet-counters, dotnet-gcdump, dotnet-dump और gcroot से अलग करने का relation दिखाने वाला diagram
dotnet_garbage_collection[".NET का garbage collection (GC)"]
dotnet_memory_leak[".NET का memory leak (unintended retention)"]
dotnet_counters["dotnet-counters"]
dotnet_gcdump["dotnet-gcdump"]
dotnet_dump["dotnet-dump"]
gcroot_command["gcroot command"]
gc_heap["GC Heap (managed heap)"]
dumpheap_command["dumpheap -stat command"]
dotnet_trace["dotnet-trace"]
gen2_heap["Gen 2 heap"]
total_allocated_memory["Total Allocated (cumulative allocation)"]
working_set_rss["Working Set / RSS"]
induced_gc["forced GC (Induced Collection)"]
static_collection_leak["static collection में hold रहना"]
unbounded_cache["size या expiry limit के बिना unbounded cache"]
event_subscription_leak["event unsubscribe न करने से leak"]
timer_disposal_leak["Timer dispose न करने से leak"]
idisposable_leak["IDisposable release न करने से leak"]
di_lifetime_mismatch["DI lifetime का mismatch"]
large_object_heap["LOH (Large Object Heap)"]
dotnet_framework[".NET Framework"]
dotnet[".NET (Core और उसके बाद)"]
dotnet_garbage_collection -->|"से verify"| dotnet_counters
dotnet_memory_leak -->|"से verify"| dotnet_gcdump
dotnet_memory_leak -->|"से verify"| dotnet_dump
dotnet_memory_leak -->|"से verify"| gcroot_command
gc_heap -->|"से verify"| dumpheap_command
dotnet_memory_leak -.->|"से verify"| dotnet_trace
dotnet_memory_leak -->|"से verify"| gen2_heap
total_allocated_memory -->|"recommended नहीं"| dotnet_memory_leak
working_set_rss -->|"recommended नहीं"| dotnet_memory_leak
induced_gc -->|"recommended नहीं"| dotnet_memory_leak
static_collection_leak -.->|"cause हो सकता"| dotnet_memory_leak
unbounded_cache -.->|"cause हो सकता"| dotnet_memory_leak
event_subscription_leak -.->|"cause हो सकता"| dotnet_memory_leak
timer_disposal_leak -.->|"cause हो सकता"| dotnet_memory_leak
idisposable_leak -.->|"cause हो सकता"| dotnet_memory_leak
di_lifetime_mismatch -.->|"cause हो सकता"| dotnet_memory_leak
large_object_heap -.->|"cause हो सकता"| dotnet_memory_leak
dotnet_counters -->|"पहले करना चाहिए"| dotnet_gcdump
dotnet_gcdump -->|"पहले करना चाहिए"| dotnet_dump
dotnet_counters -->|"incompatible"| dotnet_framework
dotnet_dump -->|"incompatible"| dotnet_framework
dotnet_garbage_collection -->|"use करता है"| gc_heap
dotnet_garbage_collection -->|"require करता है"| dotnet
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 में ऐसा दिखता है।
flowchart TB
accTitle: Process memory, managed heap, native, Commit और Working Set
accDescr: Process memory managed heap और native में बँटती है, दोनों Private Bytes / Commit में count होते हैं, और Working Set / RSS केवल physically resident हिस्सा दिखाता है
PROC["Process की इस्तेमाल हो रही memory"] --> MANAGED["Managed side<br/>GC Heap Size में दिखने वाला हिस्सा"]
PROC --> NATIVE["Native side<br/>GC Heap Size में न दिखने वाला हिस्सा"]
MANAGED --> G01["Gen 0 / Gen 1<br/>short-lived allocations"]
MANAGED --> G2["Gen 2<br/>survive किए long-lived objects"]
MANAGED --> LOH["LOH<br/>85,000 bytes या उससे बड़े objects"]
MANAGED --> POH["POH<br/>pinned objects"]
NATIVE --> STK["Thread stacks"]
NATIVE --> JITC["JIT code, loaded assemblies"]
NATIVE --> INTEROP["P/Invoke, COM, external library buffers"]
MANAGED -.counted in.-> COMMIT["Private Bytes / Commit<br/>process-private committed memory"]
NATIVE -.counted in.-> COMMIT
COMMIT -.only physically resident part.-> WS["Working Set / RSS"]
इस diagram से ये दो बातें याद रखें।
dumpheapसे दिखता है सिर्फ managed side। Native side बढ़ रहा हो तो heap जितना भी देखो, culprit नहीं निकलता।- 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 पर आम समस्याएँ ये तीन हैं।
- बड़े objects बार-बार बनाना
- बड़े objects देर तक पकड़े रखना
- बड़े objects बनाने-मिटाने से fragmentation
LOH बढ़ रहा है, इसलिए तुरंत leak नहीं। बड़े buffers reuse करने वाला design हो तो एक size तक बढ़कर स्थिर हो सकता है, और GC collect कर ले तब भी Working Set तुरंत गिरे यह ज़रूरी नहीं।
हाँ, ये स्थितियाँ शक करने लायक हैं।
System.Byte[]हर operation पर बढ़ेSystem.Char[]या hugeStringबढ़े- 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 का विषय दिखने लगता है।
ReportCachesingleton है?- 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.AllocHGlobalNativeMemory.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याHeapdump 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 न कहें। इस क्रम से अलग करें।
- Working Set / RSS अकेले से decide न करें
dotnet-countersसे GC Heap, Gen 2, LOH, GC counts देखें- Load के दौरान, load रुकने के बाद, time-separated compare करें
dotnet-gcdumpयाdotnet-dumpसे बढ़ा type देखेंgcrootसे reference source देखें- Static, cache, events, Timer, DI lifetime, async context जाँचें
- 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 में ठीक करने वाली जगह तक पहुँच जाते हैं।
संदर्भ
- इस लेख का पूरा sample code set (library, demo, unit tests) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/dotnet-gc-or-memory-leak
- .NET: Fundamentals of garbage collection
- .NET: Debug a memory leak
- .NET CLI: dotnet-counters diagnostic tool
- .NET CLI: dotnet-dump diagnostic tool
- .NET CLI: dotnet-gcdump diagnostic tool
- .NET CLI: dotnet-trace diagnostic tool
- .NET: Induced collections
- .NET: Large object heap
- .NET: Workstation and server garbage collection
- .NET Framework: Performance counters
- .NET Framework: SOS.dll (SOS Debugging Extension)
संबंधित लेख
निकटवर्ती विषयों में गहराई से जाने के लिए समान टैग वाले नवीनतम लेख।
Practical multithreading best practices: .NET संस्करण — और thread जोड़ने से पहले क्या तय करें
Multithreaded .NET/C# code को कभी-कभी crash या hang होने से बचाने वाले design नियमों का practical सार: thread स्वयं न बनाकर Task पर चलें,...
C# और PowerShell से WMI/CIM का उपयोग — Hardware जानकारी, process monitoring और remote query की practical guide
PC का serial number लेना, disk खाली स्थान की monitoring, और process start होने का पता लगाने का आम तरीका WMI/CIM है। यह लेख Get-CimInstanc...
Windows I/O की गहराई (भाग 4) — Cache Manager: आपका WriteFile disk तक कब पहुँचता है
Windows Cache Manager चित्रों से समझाने वाली series का भाग 4। File mapping के रूप में लागू cache, read-ahead और lazy writing, FlushFileBu...
Named Pipes व्यवहार में — design से security तक, Windows की standard IPC
Named pipes — Windows की standard inter-process communication — की practical guide। यह लेख primary sources से byte और message mode का चुन...
Spurious wakeup — condition variable "बिना notification के" क्यों जागती है और Windows पर सही wait कैसे करें
Condition variable की wait notification आए बिना भी लौट सकती है (spurious wakeup)। यह लेख Windows implementation से समझाता है कि spec इसे ...
संबंधित विषय
ये पृष्ठ विषय को सेवाओं और निर्णयों के व्यापक संदर्भ में रखते हैं।
Windows के तकनीकी विषय
Windows विकास, बग जाँच और मौजूदा संपत्तियों के उपयोग का प्रवेश-द्वार।
इस विषय से जुड़ी सेवाएँ
यह लेख निम्नलिखित सेवाओं से सीधे जुड़ा है।
Windows ऐप विकास
व्यावसायिक ऐप, डिवाइस एकीकरण और संचार उपकरण, आवश्यकताओं से विकास तक।
बग और कारणों की जाँच
हम पुनरुत्पादन में कठिन बग, लंबे समय चलने के बाद की समस्याओं, लीक और संचार विच्छेद की जाँच करते हैं।
अक्सर पूछे जाने वाले प्रश्न
इस लेख के विषय पर परामर्श में अक्सर पूछे जाने वाले प्रश्न।
- .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 की तरह डालने से पहले हमेशा यह पहचानें कि क्या बढ़ रहा है।