- Katılım
- 3 Ocak 2025
- Mesajlar
- 673
- Elmaslar
- 352
- Puan
- 2.355
- Konum
- Amasya
- Minecraft
- Zyph0rr
Discord:
bosstrap
Memory Leak (Bellek Sızıntısı) Nedir? Kapsamlı Rehber
Ne olduğu, neye yol açtığı, nasıl tespit edildiği ve nasıl önlendiği
Ne olduğu, neye yol açtığı, nasıl tespit edildiği ve nasıl önlendiği
1. Giriş: Neden Bu Konu Önemli?
Bir uygulama açıldığında gayet hızlı çalışır. Bir saat sonra biraz yavaşlar. Altıncı saatte donmalar başlar. Yirmi dördüncü saatte ya çöker ya da işletim sistemi onu öldürür. Sunucuysa oyuncular "lag var" der, masaüstü uygulamasıysa kullanıcı "kapatıp açınca düzeliyor" der.
Bu tablonun arkasındaki en yaygın sebeplerden biri memory leak, yani bellek sızıntısıdır.
Bellek sızıntısı, "bozuk kod" gibi görünmez. Kod derlenir, testler geçer, hata mesajı çıkmaz. Sorun zamanla birikir. Bu yüzden hem tespiti hem de teşhisi diğer hatalara göre daha zordur — ve tam da bu yüzden öğrenmeye değer.
Bu rehber; kavramı sıfırdan anlatmayı, gerçek örneklerle göstermeyi ve elinizdeki araçlarla nasıl bulacağınızı adım adım açıklamayı hedefliyor.
2. Memory Leak Nedir?
Tanım: Program tarafından ayrılmış (allocate edilmiş) bellek bölgelerinin, artık kullanılmadıkları hâlde serbest bırakılmaması ve bu nedenle sistem tarafından geri kazanılamamasıdır.
Daha kısa bir tanım: Kullanılmayan ama bırakılmayan bellek.
Otel Odası Benzetmesi
Bir oteli düşünün. Müşteri geliyor, oda alıyor, kalıyor, çıkarken anahtarı resepsiyona bırakıyor; oda temizlenip yeniden satılabilir hâle geliyor. Sistem sağlıklı çalışıyor.
Şimdi bazı müşterilerin çıkarken anahtarı iade etmediğini düşünün. Otel kayıtlarında oda hâlâ "dolu" görünür. Kimse kalmıyordur ama oda kimseye verilemez. Böyle birkaç oda sorun değildir. Ama her gün on oda böyle kilitlenirse, birkaç hafta içinde otel dolu görünür ve yeni müşteri kabul edemez hâle gelir.
Bellek sızıntısı tam olarak budur: odalar boş, ama defterde dolu.
Sızıntının Tanımı İçin Kritik Nokta
Bellek sızıntısında bellek "kaybolmaz". Bellek hâlâ oradadır, adresi bellidir, hatta içeriği okunabilir. Sorun şudur: programın o belleği bırakma niyeti veya imkânı kalmamıştır.
- Manuel bellek yönetimli dillerde (C, C++) sorun: free() / delete çağrılmamıştır.
- Çöp toplayıcılı (GC) dillerde (Java, C#, JavaScript, Python, Go) sorun: nesneye giden bir referans hâlâ hayattadır. GC "buna hâlâ ulaşılabiliyor" der ve toplamaz.
Bu ikinci durum çok önemlidir ve sık yanlış bilinir:
"Java/JavaScript'te garbage collector var, orada memory leak olmaz." Bu yanlıştır. GC yalnızca ulaşılamayan nesneleri toplar. Yanlışlıkla tuttuğunuz bir referans, nesneyi sonsuza kadar "ulaşılabilir" yapar. GC'li dillerde sızıntı, unutulan referans şeklinde ortaya çıkar.
3. Karıştırılan Kavramlar
Sızıntıyı doğru teşhis etmek için, ona benzeyen ama aynı olmayan durumları ayırmak gerekir.
| Durum | Ne oluyor | Sızıntı mı |
|---|---|---|
| Memory leak | Kullanılmayan bellek bırakılmıyor, kullanım kalıcı olarak artıyor | Evet |
| Yüksek bellek kullanımı | Uygulama gerçekten çok veri tutuyor, ama bu bilinçli bir tercih | Hayır |
| Memory bloat | Veri verimsiz temsil ediliyor, gereğinden fazla yer kaplıyor | Hayır, ama israf |
| Fragmentation | Toplam boş alan yeterli ama sürekli blok bulunamıyor | Hayır, farklı sorun |
| Use-after-free | Serbest bırakılmış belleğe erişiliyor | Hayır, tam tersi hata |
| Kaynak sızıntısı | Dosya, soket, thread, veritabanı bağlantısı kapatılmıyor | Akraba sorun |
Ayırt edici test: Uygulamayı sabit bir yük altında saatlerce çalıştırın. Bellek kullanımı bir plato çizip orada kalıyorsa sızıntı yoktur; yük değişmediği hâlde sürekli tırmanıyorsa sızıntı vardır.
4. Memory Leak Neye Yol Açar? (Belirtiler)
Sızıntının etkileri kademelidir ve genelde şu sırayla görülür:
4.1 Kademe 1 — Sessiz Dönem
Bellek kullanımı yavaşça artar. Kullanıcı hiçbir şey fark etmez. Sızıntı bu dönemde tespit edilirse en ucuz şekilde çözülür.
4.2 Kademe 2 — Yavaşlama
- GC'li dillerde: Çöp toplayıcı, dolmakta olan heap'i temizlemek için daha sık ve daha uzun çalışır. Her GC turunda uygulama duraklar ("stop-the-world"). Bu duraklamalar başta 10 ms iken 500 ms'ye çıkabilir. Kullanıcı bunu takılma / donma / lag olarak hisseder.
- Genel olarak: Fiziksel RAM dolunca işletim sistemi belleği diske taşımaya başlar (swap / paging). Disk RAM'den kat kat yavaştır; sistem geneli sürünmeye başlar.
4.3 Kademe 3 — Kaynak Baskısı
- Aynı makinedeki diğer uygulamalar da bellek bulamaz hâle gelir.
- Cache'ler etkisizleşir, disk I/O patlar.
- Container ortamlarında (Docker/Kubernetes) bellek limiti aşılınca konteyner öldürülür ve yeniden başlatılır. Loglarda genellikle sadece OOMKilled yazar; uygulama tarafında hiçbir hata izi kalmayabilir.
4.4 Kademe 4 — Çökme
- Java: java.lang.OutOfMemoryError: Java heap space
- C/C++: malloc NULL döner, new std::bad_alloc fırlatır, ya da doğrudan segfault
- Node.js: FATAL ERROR: Ineffective mark-compacts near heap limit — JavaScript heap out of memory
- Python: MemoryError
- Linux: Kernel'in OOM Killer'ı süreci sonlandırır; dmesg çıktısında "Out of memory: Killed process..." satırı görülür.
4.5 Kademe 5 — Dolaylı Zararlar
- Gece 03:00 alarmları ve "her sabah sunucuyu restart etme" alışkanlığı
- Sorunu gizlemek için sürekli RAM/donanım büyütülmesi (maliyet)
- Güvenlik boyutu: Uzaktan tetiklenebilen bir sızıntı, saldırgan için bir DoS (hizmet dışı bırakma) vektörüdür. Saldırgan tek yapması gerekeni tekrarlar ve servis kendi kendine çöker.
Kritik uyarı: "Her gece restart atıyoruz, sorun yok" bir çözüm değil, sızıntının varlığının kanıtıdır. Restart, semptomu bastırıp sorunu ölçülemez hâle getirir.
5. Memory Leak Neden Oluşur? (Kök Nedenler)
5.1 Manuel Bellek Yönetimi Hataları (C / C++ / benzeri)
En klasik durum: ayrılan bellek hiç bırakılmaz.
Kod:
[/SIZE][/CENTER]
[SIZE=5][CENTER]void veri_isle() {
char *buffer = malloc(1024);
if (buffer == NULL) return;
if (!dogrula(buffer)) {
return; // ✗ SIZINTI: erken çıkışta free yok
}
islem_yap(buffer);
free(buffer); // yalnızca "mutlu yol"da çalışıyor
}
Bu hatanın tipik kaynakları:
- Erken return ve hata dalları
- Exception fırlatılması (C++'ta free/delete satırı atlanır)
- Yanlış operatör eşleşmesi: new[] ile ayırıp delete (köşeli parantezsiz) ile bırakmak
- Sahiplik belirsizliği: Belleği kimin bırakacağı tanımlı değildir; herkes "diğeri bırakır" varsayar
C++'taki doğru yaklaşım — RAII ve akıllı işaretçiler:
Kod:
void veri_isle() {[/SIZE][/CENTER]
[SIZE=5][CENTER] auto buffer = std::make_unique<char[]>(1024); // sahiplik net
if (!dogrula(buffer.get())) {
return; // ✓ unique_ptr yıkıcısı belleği otomatik bırakır
}
islem_yap(buffer.get());
} // ✓ exception fırlasa bile bellek serbest kalır
5.2 Unutulan Referanslar (Java, C#, JS, Python, Go)
GC'li dillerde sızıntının %90'ı bu başlığın altındadır: nesne artık gerekli değildir, ama bir yerden hâlâ referans verilmektedir.
a) Sürekli büyüyen statik/global koleksiyonlar
Kod:
[/SIZE][/CENTER]
[SIZE=5][CENTER]public class Kayit {
// static = uygulama ömrü boyunca yaşar
private static final List<Oturum> TUM_OTURUMLAR = new ArrayList<>();
public static void oturumAc(Oturum o) {
TUM_OTURUMLAR.add(o); // ✗ ekleniyor...
}
// ...ama hiçbir yerde remove() yok. Liste sonsuza kadar büyür.
}
Bu, sunucu uygulamalarındaki en sık sızıntı kalıbıdır. Her bağlantı/oyuncu/istek listeye eklenir, çıkışta silinmez.
b) Sınırsız cache
Cache mantıklı bir optimizasyondur — ta ki tavanı olmayana kadar. Tavanı olmayan cache, adı "cache" olan bir bellek sızıntısıdır.
Kod:
[/SIZE][/CENTER]
[SIZE=5][CENTER]// ✗ Sınır yok, süre yok, tahliye politikası yok
private final Map<String, Sonuc> cache = new HashMap<>();
// ✓ Boyut sınırı ve süre ile
private final Cache<String, Sonuc> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(30))
.build();
c) Kaldırılmayan listener / callback / event handler
Bir nesne kendini bir olay kaynağına dinleyici olarak kaydeder ama kaydını hiç sildirmez. Olay kaynağı yaşadığı sürece dinleyiciyi — ve dinleyicinin tuttuğu her şeyi — hayatta tutar.
Kod:
[/SIZE][/CENTER]
[SIZE=5][CENTER]// ✗ Sızdıran
function panelAc() {
const agirVeri = new Array(1_000_000).fill('x');
window.addEventListener('resize', () => console.log(agirVeri.length));
// Panel kapansa bile listener window üzerinde kalır → agirVeri asla toplanmaz
}
// ✓ Temizlenen
function panelAc() {
const agirVeri = new Array(1_000_000).fill('x');
const handler = () => console.log(agirVeri.length);
window.addEventListener('resize', handler);
return function kapat() {
window.removeEventListener('resize', handler); // ✓ referans kopar
};
}
React tarafındaki karşılığı, useEffect içinde cleanup fonksiyonu döndürmemektir:
Kod:
[/SIZE][/CENTER]
[SIZE=5][CENTER]useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id); // ✓ bu satır olmazsa interval + closure sızar
}, []);
d) Kapatılmayan kaynaklar
Dosya, soket, veritabanı bağlantısı, thread, timer... Bunların her biri hem işletim sistemi kaynağı hem de bellek tutar.
Kod:
[/SIZE][/CENTER]
[SIZE=5][CENTER]// ✓ try-with-resources: exception olsa bile kapatır
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.execute();
}
e) İç sınıflar ve gizli this referansı
Java'da statik olmayan iç sınıflar (ve anonim sınıflar), dıştaki sınıfa gizli bir referans tutar. Kısa ömürlü olması gereken bir dış nesne, uzun ömürlü bir iç nesne yüzünden hayatta kalır.
Kod:
class Ekran {[/SIZE][/CENTER]
[SIZE=5][CENTER] private byte[] tampon = new byte[50_000_000];
void basla() {
// ✗ Anonim Runnable, Ekran.this'i tutar → 50 MB planlayıcı yaşadıkça durur
planlayici.scheduleAtFixedRate(new Runnable() {
public void run() { tik(); }
}, 0, 1, TimeUnit.SECONDS);
}
}
Çözüm: static iç sınıf kullanmak, gerekirse WeakReference ile tutmak, ya da görev iptalini garantiye almak.
f) equals/hashCode bozukluğu (sinsi bir kalıp)
Bir nesne HashSet/HashMap anahtarı olarak eklendikten sonra alanları değişirse hash değeri değişir; artık remove() onu bulamaz. Nesne koleksiyonda erişilemez ama silinemez hâlde sıkışır.
g) ThreadLocal temizlenmemesi
Thread havuzlarında thread'ler ölmez, yeniden kullanılır. ThreadLocal.set() yapıp remove() yapmazsanız, değer thread'in ömrü boyunca (yani uygulama boyunca) durur.
h) Java'da substring/dilim tuzağı (eski sürümler)
Java 6 ve öncesinde substring, orijinal dizinin karakter dizisini paylaşırdı; 10 MB'lık bir metinden alınan 5 karakterlik parça 10 MB'ı hayatta tutardı. Java 7u6 sonrası düzeltildi, ama aynı kalıp başka dillerde ve kendi yazdığınız "görünüm (view)" sınıflarında hâlâ karşınıza çıkabilir.
5.3 Dairesel Referanslar
İki nesne birbirini tutar. Modern izleme tabanlı (tracing) GC'ler bunu sorunsuz çözer. Ancak referans sayımı (reference counting) kullanan sistemlerde ölümcüldür:
- Python'un gc modülü döngüleri toplayabilir, ama __del__ içeren bazı eski senaryolarda takılabilir
- C++'ta std::shared_ptr döngüsü klasik örnektir; sayaç asla sıfıra inmez. Çözüm: döngüyü kıran tarafta std::weak_ptr kullanmak
- Eski tarayıcılarda DOM ↔ JavaScript arası döngüler
5.4 Yerel (Native) Bellek Sızıntıları
Heap dışı bellek de sızabilir ve bu tür sızıntılar heap araçlarıyla görünmez:
- Java'da DirectByteBuffer / JNI ile ayrılan bellek
- Node.js'te native modüllerin (Buffer dışı) ayırdığı bellek
- Python'da C uzantılarının ayırdığı bellek
- Metaspace/ClassLoader sızıntıları: uygulama sunucusunda tekrar tekrar deploy edildikçe eski sınıf yükleyicilerin bırakılmaması
İpucu: İşletim sistemi süreç belleği (RSS) sürekli artıyor ama heap grafiği düz duruyorsa, aradığınız şey büyük olasılıkla native bir sızıntıdır.
6. Memory Leak Nasıl Bulunur? Sistematik Yöntem
Sızıntı avında sırayı bozmayın. Sıra bozulduğunda insanlar rastgele kod okuyup saatlerini harcar.
Adım 1 — Doğrula: Gerçekten Sızıntı mı?
Bir grafik olmadan hiçbir şeye başlamayın. Sabit ve tekrarlanabilir bir yük altında bellek kullanımını en az birkaç saat kaydedin.
| Grafikte gördüğün | Anlamı |
|---|---|
| Testere dişi, tepeler aynı seviyede | Sağlıklı, GC işini yapıyor |
| Testere dişi, ama her dip bir öncekinden yüksek | Sızıntı var, klasik imza budur |
| Sürekli artış, hiç düşüş yok | Sızıntı ya da sınırsız cache |
| Yükle beraber artıp yük bitince düşüyor | Normal davranış, sorun yok |
Anahtar ölçüt: Bir tam GC (Java'da Full GC) sonrasındaki taban seviyesi (baseline) her turda yükseliyorsa, sızıntı vardır.
Adım 2 — Daralt: Tekrarlanabilir Bir Senaryo Bul
Amaç, sızıntıyı isteğe bağlı üretebilmektir. Şüpheli işlemi bir döngüde binlerce kez tekrarlayın (aynı sayfayı aç-kapa, aynı isteği at, aynı komutu çalıştır) ve grafiği izleyin. Tekrarlanabilir senaryo, işin en zor ve en değerli parçasıdır; bulunduğunda geri kalanı mekanik hâle gelir.
Adım 3 — Heap Snapshot Al ve Karşılaştır
Tek bir snapshot "ne var" der; iki snapshot arasındaki fark "ne birikiyor" der. Asıl cevap farktadır.
Yöntem:
- Uygulamayı ısıt, bir tam GC tetikle → Snapshot A
- Şüpheli senaryoyu N kez çalıştır
- Yeniden tam GC tetikle → Snapshot B
- B − A farkında sayısı N ile orantılı artan nesne tipini ara
Bu "orantılılık" testi çok güçlüdür: senaryoyu 1.000 kez çalıştırdınız ve bir sınıftan tam 1.000 adet fazladan nesne birikmişse, suçluyu bulmuşsunuz demektir.
Adım 4 — Referans Zincirini (Dominator / GC Root Path) Takip Et
Bu adım işin kalbidir ve en çok atlanan adımdır.
Şunu sormayın: "Bellekte en çok ne var?" — cevap genellikle String veya byte[] çıkar ve hiçbir işe yaramaz.
Şunu sorun: "Bu nesneyi kim tutuyor? O nesneyi kim tutuyor? Bu zincir hangi GC root'a kadar gidiyor?"
Araçlarda bu özellik şu isimlerle geçer:
- Eclipse MAT: Path to GC Roots, Dominator Tree, Leak Suspects Report
- Chrome DevTools: Retainers paneli
- VisualVM / JProfiler: References / Nearest GC Root
Zincirin sonunda neredeyse her zaman şunlardan biri çıkar: bir static alan, bir singleton, bir thread, bir listener kaydı ya da bir cache.
Adım 5 — Düzelt ve Kanıtla
Düzeltmenin ardından aynı ölçümü aynı senaryoyla tekrarlayın. Grafik düzleşmiyorsa iş bitmemiştir. "Sanırım düzeldi" kabul edilebilir bir sonuç değildir; grafik kanıttır.
7. Diller ve Platformlara Göre Araçlar
7.1 Java / JVM
Ölçüm ve izleme
- jcmd <pid> GC.heap_info — anlık heap durumu
- jstat -gcutil <pid> 1000 — saniyede bir GC istatistikleri; taban seviyesinin yükselişini görmenin en hızlı yolu
- GC logları: -Xlog:gc*:file=gc.log:time,uptime,level,tags
- JFR (Java Flight Recorder): -XX:StartFlightRecording=duration=300s,filename=kayit.jfr — düşük maliyetli, üretimde bile kullanılabilir
Heap dump alma
Kod:
[/SIZE][/CENTER]
[SIZE=5][CENTER]jcmd <pid> GC.heap_dump /yol/dump.hprof
# veya
jmap -dump:live,format=b,file=dump.hprof <pid>
Çökme anında otomatik dump için (her üretim uygulamasında olması gereken ayar):
Kod:
[/SIZE][/CENTER]
[SIZE=5][CENTER]-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/dumps
Analiz
- Eclipse MAT — sızıntı analizinde standart araç. "Leak Suspects" raporu çoğu zaman doğrudan suçluyu gösterir
- VisualVM — ücretsiz, snapshot karşılaştırma yapabilir
- JProfiler / YourKit — ticari, ayrıntılı ve rahat kullanımlı
- Async-profiler — allocation profiling; "bu belleği hangi kod satırı ayırıyor" sorusunun cevabı
Not — Minecraft sunucuları ve benzeri uzun ömürlü Java uygulamaları: Bu ortamlarda spark profiler yaygın olarak kullanılır (/spark heapsummary, /spark profiler). Tipik sızıntı kaynakları: kapatılmayan eklenti görevleri, oyuncu çıkışında temizlenmeyen HashMap'ler, Player nesnesinin kalıcı olarak saklanması (bunun yerine UUID saklanmalıdır) ve boşaltılmayan chunk/entity referanslarıdır.
7.2 JavaScript / Tarayıcı
Chrome DevTools → Memory sekmesi
- Heap snapshot: Snapshot al, senaryoyu çalıştır, ikinci snapshot al, "Comparison" görünümüne geç ve Delta sütununa bak
- Allocation instrumentation on timeline: Zaman içinde ayrılan ve hayatta kalan nesneleri gösterir; mavi çubuklar hayatta kalanlardır — sızıntı bunlarda görünür
- Detached DOM nodes: Snapshot içinde Detached araması yapın. Bu, DOM'dan çıkarılmış ama JS tarafından hâlâ referans verilen elemanlardır ve web sızıntılarının en yaygın türüdür
- Performance monitor: JS heap size ve DOM node sayısını canlı izler; ikisi de sürekli tırmanıyorsa sorun kesindir
Sık görülen web sızıntıları: kaldırılmayan event listener'lar, temizlenmeyen setInterval, detached DOM referansları, sınırsız büyüyen global diziler, kapatılmayan observer'lar (MutationObserver, IntersectionObserver) ve sonlandırılmayan WebSocket bağlantıları.
7.3 Node.js
Kod:
[/SIZE][/CENTER]
[SIZE=5][CENTER]node --inspect app.js # Chrome DevTools ile bağlan
node --max-old-space-size=4096 app.js
node --heapsnapshot-signal=SIGUSR2 app.js # sinyalle snapshot al
- process.memoryUsage() ile heapUsed ve rss değerlerini periyodik loglayın
- rss artıyor ama heapUsed sabitse: native/Buffer kaynaklı sızıntı
- clinic.js (özellikle clinic heapprofiler) teşhis için pratiktir
7.4 C / C++
- Valgrind (Memcheck):
Kod:
[/SIZE][/CENTER]
[SIZE=5][CENTER] valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./program
Çok ayrıntılıdır ama programı ~20 kat yavaşlatır.
- AddressSanitizer (ASan) — modern tercih, çok daha hızlı:
Kod:
[/SIZE][/CENTER]
[SIZE=5][CENTER] g++ -fsanitize=address -g program.cpp -o program
./program # çıkışta sızıntı raporu verir
- LeakSanitizer, Dr. Memory, Visual Studio CRT Debug Heap (Windows tarafında)
- En etkili önlem araç değil, disiplindir: RAII, std::unique_ptr / std::shared_ptr, çıplak new/delete'ten kaçınmak, döngüleri weak_ptr ile kırmak.
7.5 Python
- tracemalloc (standart kütüphanede): iki anlık görüntü alıp compare_to() ile farkı listeler
- objgraph: objgraph.show_growth() — hangi tip nesnelerin sayısı artıyor
- memory_profiler: satır satır bellek kullanımı
- gc.set_debug(gc.DEBUG_LEAK) ve gc.garbage: toplanamayan döngüleri gösterir
7.6 İşletim Sistemi Seviyesi
- Linux: top/htop (RES sütunu), ps -o rss,vsz, smem, /proc/<pid>/status
- dmesg | grep -i "out of memory" — OOM Killer'ın süreci öldürüp öldürmediği
- pmap -x <pid> — süreç bellek haritası
- Windows: Task Manager, Resource Monitor, Performance Monitor, RAMMap
- macOS: Activity Monitor, leaks <pid>, Instruments (Leaks / Allocations enstrümanları)
7.7 Üretim Ortamında İzleme
Sızıntıyı kullanıcıdan önce siz görmek istiyorsanız kalıcı izleme şart:
- Prometheus + Grafana ile süreç ve heap belleği metrikleri
- Uyarı kuralı fikri: "Bellek kullanımı 6 saat boyunca monoton arttı" — mutlak eşikten çok daha erken uyarır
- Konteyner ortamlarında bellek limitini ölçüme dayalı belirleyin; sızıntıyı gizlemek için limiti büyütmek çözüm değil, ertelemedir
8. Önleme: Sızıntıyı Baştan Yazmamak
8.1 Kod Yazarken
- Sahipliği netleştirin. Her ayrılan kaynağın "sahibi" belli olsun: onu kim bırakacak, ne zaman bırakacak? Belirsizlik sızıntının ana kaynağıdır.
- Dilin otomatik temizlik mekanizmalarını kullanın. C++'ta RAII ve akıllı işaretçiler; Java'da try-with-resources; Python'da with; C#'ta using; Go'da defer.
- Her koleksiyona bir tavan koyun. Sınırsız büyüyebilen bir liste/map/cache, potansiyel bir sızıntıdır. Boyut sınırı ve/veya süre bazlı tahliye ekleyin.
- Her addin bir removeu, her registerın bir unregisterı olsun. Bunu yazarken düşünün, sonradan değil.
- Cache'i cache gibi kurun. HashMap bir cache değildir. Caffeine/Guava (Java), lru-cache (Node), functools.lru_cache (Python) gibi tahliye politikalı yapılar kullanın.
- Uzun ömürlü nesnelerde ağır nesneleri tutmayın. Örneğin oyun sunucusunda Player nesnesi yerine UUID saklayın; gerektiğinde nesneyi yeniden çözümleyin.
- Ömür (lifecycle) farkına dikkat edin. Kısa ömürlü bir nesneyi uzun ömürlü bir nesneye bağladığınız her yer, potansiyel bir sızıntı noktasıdır.
- Zayıf referansları yerinde kullanın. WeakHashMap, WeakReference, weak_ptr, WeakMap — ama bunlar tasarım hatasını örtmek için değil, kasıtlı "sahiplenmeyen" ilişkiler için kullanılmalıdır.
- Finalizer'lara güvenmeyin. Java'da finalize() kullanımdan kaldırıldı; ne zaman, hatta çalışıp çalışmayacağı belirsizdir. Temizlik açık ve deterministik olmalıdır.
8.2 Süreç Olarak
- Kod incelemesinde sorulacak standart soru: "Bu nesnenin referansını kim, ne kadar süre tutuyor?"
- Dayanıklılık (soak) testi: Sürümden önce uygulamayı gerçekçi yükle 12–24 saat çalıştırıp bellek grafiğini inceleyin. Sızıntıların büyük kısmı bu tek adımla yakalanır.
- Otomatik regresyon kontrolü: CI'da bir senaryoyu N kez çalıştırıp bellek artışını eşikle karşılaştıran basit bir test bile çok işe yarar.
- Statik analiz: SpotBugs, Clang Static Analyzer, ESLint kuralları, sanitizer'lı derleme profilleri.
- Üretimde OOM dump'ı açık tutun. Çökme anındaki heap dump, sonradan üretilemeyen en değerli kanıttır.
9. Sık Yapılan Hatalar ve Yanlış Bilinenler
"GC var, sızıntı olmaz."
GC yalnızca ulaşılamayan nesneleri toplar. Unutulmuş bir referans, GC'yi devre dışı bırakmakla aynı etkiyi yaratır.
"Bellek kullanımı yüksek, demek ki sızıntı var."
Yüksek kullanım tek başına kanıt değildir. Kanıt, zamanla monoton artış ve yükselen taban seviyesidir.
"System.gc() çağırırsam düzelir."
GC'yi zorlamak, tutulan referansları koparmaz. Sızıntıyı çözmez, yalnızca performansı bozar.
"RAM'i artırdık, sorun kalmadı."
Sızıntı çözülmedi, sadece çökme günü ileri atıldı. Üstelik artık heap dump'ı analiz etmek daha zor ve pahalı.
"En büyük nesneyi bulursam sızıntıyı bulurum."
En büyük nesne genelde masumdur (byte[], String). Suçlu, o nesneyi tutan zincirdedir. Her zaman retainer/dominator yolunu takip edin.
"Sızıntı tek bir noktadadır."
Büyük sistemlerde çoğu zaman birden fazla küçük sızıntı bir arada bulunur. Birini düzeltip grafiğin tamamen düzleşmesini beklemeyin; ölçüp tekrarlayın.
10. Hızlı Kontrol Listesi
Bir sızıntı şüphesiyle karşılaştığınızda sırasıyla:
- Bellek grafiğini kaydet — sızıntı mı, yoksa sadece yüksek kullanım mı?
- Tam GC sonrası taban seviyesi yükseliyor mu?
- Sızıntıyı tekrar üretebilen bir senaryo bul
- Öncesi/sonrası heap snapshot al ve farkı karşılaştır
- Sayısı tekrar sayısıyla orantılı artan nesne tipini bul
- O nesnenin GC root'a giden referans zincirini çıkar
- Zincirdeki gereksiz referansı kopar (remove / unregister / close / clear)
- Aynı senaryoyu tekrar çalıştır ve grafiğin düzleştiğini kanıtla
- Aynı hatanın tekrar girmemesi için test veya izleme uyarısı ekle
11. Sonuç
Memory leak, "kodun çalışması" ile "kodun doğru çalışması" arasındaki farkı en net gösteren hatalardan biridir. Derleyici uyarmaz, testler geçer, ilk gün her şey yolunda görünür — ve sorun ancak uygulamanız gerçek dünyada uzun süre ayakta kaldığında ortaya çıkar.
Akılda tutulacak üç şey:
- Sızıntı bir bellek sorunu değil, bir sahiplik sorunudur. "Bu nesneyi kim, ne kadar süre tutuyor?" sorusunun cevabı yoksa, sızıntı adayınız var demektir.
- Tahmin etmeyin, ölçün. Grafik ve heap snapshot olmadan yapılan her teşhis tahmindir.
- Düzeltmeyi kanıtlayın. Ölçüm olmadan "düzeldi" demek, sorunu bir sonraki sürüme ertelemektir.
Bu alışkanlıkları edindiğinizde, bellek sızıntısı korkutucu bir gizem olmaktan çıkıp, adım adım çözülen sıradan bir mühendislik problemine dönüşür.
