Çöp toplama mekanizmasına sahip dillerde bellek sızıntıları meydana gelir.

Çöp toplama mekanizmasına sahip dillerde bellek sızıntıları meydana gelebilir mi?

Evet. Ve bunun nedenini anlamak, yazılım geliştirmedeki en kalıcı efsanelerden birini unutmayı gerektiriyor.

Efsane: Çöp toplama bellek sızıntılarını önler. Gerçek: Çöp toplama, belirli bir bellek sorununu, yani bir programın bir bellek bloğuna olan tüm işaretçilerini kaybettiği ve onu serbest bırakmanın hiçbir yolunun olmadığı "bağlantı kopması" sorununu önler. Çöp toplamanın önleyemediği ve önleyemeyeceği şey ise tam tersi sorundur: Artık ihtiyaç duymadığı bir nesneye referans tutan bir program. Çöp toplayıcı canlı bir referans görür ve nesneyi doğru bir şekilde bırakır. Geliştirici o nesneyi kullanmayı bırakmayı amaçlamıştır. Bellek büyür. Etki, geleneksel bir sızıntıyla aynıdır: sınırsız bellek büyümesi, sonunda OOM hataları, hizmet yeniden başlatmaları, ancak mekanizma farklıdır.

Bu ayrım önemlidir çünkü sorunu nerede arayacağınızı ve nasıl çözeceğinizi değiştirir. C ve C++'da bellek sızıntısı, eksik bir bellek kaynağı anlamına gelir. free() or deleteJava, Python, C#, Go veya JavaScript'te bu, temizlenmesi gereken ancak temizlenmemiş bir referans anlamına gelir. Çöp toplayıcı arızalı değildir. Programın tutmaması gereken bir referansı doğru şekilde dikkate almaktadır.

Her Dilde Sızıntı Tespiti

SMART TS XL Uzun ömürlü referans zincirlerini ve sınırsız veri yapılarını tüm kod tabanınıza eşler.

Daha fazla bilgi

Çöp Toplayıcılar Gerçekte Ne Yapar?

Çöp toplayıcının görevi, programın kök dizinlerinden, genel değişkenlerden, yığın değişkenlerinden ve iş parçacığına özgü durumdan artık erişilemeyen belleği belirlemek ve geri kazanmaktır. Kök dizinden erişilebilen her nesne canlı kabul edilir. Erişilemeyen her nesne ise çöp toplama için uygundur.

Üç baskın GC stratejisinin her biri erişilebilirliği farklı şekilde tanımlar:

İşaretle ve süpür (Java'nın HotSpot'u, Go, C#/.NET tarafından kullanılır) kök kümesinden başlar ve erişilebilir tüm nesne referanslarını tarar. Tarama sırasında erişilemeyen nesneler silinir. Bu, döngüsel referansları doğru şekilde ele alır; birbirine işaret eden ancak başka hiçbir yerden erişilemeyen iki nesne de toplanır. Ancak, kök kümesinden erişilebilen bir kapsayıcıyı (statik bir nesne) ele alamaz. HashMapsınıf düzeyinde List(Girişler eklenip asla çıkarılmadığı için sınırsız bir şekilde büyüyen bir sistem.)

Referans sayımı (CPython'ın temel mekanizması) her nesneye kaç referansın işaret ettiğini takip eder. Sayı sıfıra ulaştığında, nesne hemen serbest bırakılır. Klasik hata modu: A'nın B'ye ve B'nin de A'ya referans verdiği döngüsel referanslar. Her ikisinin de referans sayısı en az 1'dir ve asla serbest bırakılmazlar. CPython bunu ek bir döngü toplayıcı ile ele alır, ancak döngü toplayıcının, nesnelerle ilgili uç durumları vardır. __del__ yöntemleri.

Nesilsel çöp toplama (çoğu modern çalışma zamanı ortamında yukarıdakiyle birlikte kullanılır), nesneleri ne kadar süre hayatta kaldıklarına bağlı olarak genç ve yaşlı nesillere ayırır. Kısa ömürlü nesneler düşük maliyetle sık sık toplanır. Uzun ömürlü nesneler ise nadiren toplanır. Bellek sızıntıları için sonuç şudur: Kısa ömürlü olması gereken ancak yanlışlıkla saklanan nesneler eski nesile geçer ve giderek daha az sıklıkla toplanır, bu da sızıntıyı gözlemlemeyi ve teşhis etmeyi zorlaştırır.

Bu mekanizmaların hiçbiri programın bir nesneyi hâlâ kullanıp kullanmaması gerektiğini kontrol etmez, yalnızca ona erişip erişemeyeceğini kontrol eder.

Çöp Toplama (GC) Dillerinde Bellek Sızıntılarına Neden Olan Beş Mekanizma

1. Statik ve Sınıf Düzeyi Koleksiyonları

Java'da statik bir alan, Python'da bir sınıf değişkeni, C#'da statik bir özellik; bunlar uygulama sürecinin tamamı boyunca varlığını sürdürür. Statik bir alanda saklanan herhangi bir koleksiyon, açıkça temizlenmedikçe programın tüm ömrü boyunca nesneler biriktirir.

Java

// Java: static cache that grows without bound
public class SessionManager {
    // Lives for the lifetime of the JVM process
    private static final Map<String, UserSession> sessions = new HashMap<>();

    public void createSession(String userId) {
        sessions.put(userId, new UserSession(userId));
        // Session is added but never removed when it expires
    }
    // Missing: cleanup when session expires or user logs out
}

MKS sessions Harita, kök dizinden erişilebilen sınıf yükleyicisinden erişilebilir. Her UserSession Oluşturulan bellek birikir. Uzun süreli çalışan bir hizmette, bu durum bellek kullanımını saatler veya günler içinde tüketir. Çözüm, ya aktif kaldırma (çağrı) gerektirir. sessions.remove(userId) Oturum kapatma veya süre dolma durumunda) veya zaman sınırlı önbellek gibi otomatik silme özelliğine sahip bir yapıya geçiş.

2. Olay Dinleyicileri ve Geri Çağrılar

Bir gözlemci, dinleyici veya geri çağırma işlevi kaydetmek, olay kaynağından dinleyiciye bir referans oluşturur. Eğer dinleyici hiçbir zaman kaydı silinmezse, olay kaynağı dinleyiciyi olay kaynağı var olduğu sürece canlı tutar; bu da amaçlanandan çok daha uzun olabilir.

piton

# Python: listener retained by long-lived event source
class DataSource:
    def __init__(self):
        self._listeners = []

    def add_listener(self, listener):
        self._listeners.append(listener)

    # Missing: remove_listener method

class DataProcessor:
    def __init__(self, source: DataSource):
        self.source = source
        source.add_listener(self._on_data)   # strong reference held in source

    def _on_data(self, data):
        pass

# Each DataProcessor created is never collected because
# DataSource._listeners holds a reference to its bound method
source = DataSource()
for _ in range(10000):
    processor = DataProcessor(source)
    # processor goes out of scope here
    # but source._listeners still holds 10,000 bound method references

3. İş Parçacığı Havuzlarında İş Parçacığına Özgü Değişkenler

İş parçacığına özgü depolama, belirli bir göreve değil, bir iş parçacığına özgüdür. İş parçacıklarının birçok istekte yeniden kullanıldığı iş parçacığı havuzlarını kullanan uygulamalarda, bir istek sırasında ayarlanan iş parçacığına özgü değişkenler, aynı iş parçacığı tarafından işlenen sonraki her istek için de ayarlı kalır.

Java

// Java: ThreadLocal leak in a thread pool
public class RequestContextHolder {
    private static final ThreadLocal<RequestContext> context = new ThreadLocal<>();

    public static void setContext(RequestContext ctx) {
        context.set(ctx);
    }

    public static RequestContext getContext() {
        return context.get();
    }

    // Missing: clearContext() method called after each request
}

// In a servlet or request handler:
public void handleRequest(HttpRequest req) {
    RequestContextHolder.setContext(new RequestContext(req));
    // ... process request
    // Missing: RequestContextHolder.clearContext();
    // The RequestContext stays associated with this thread indefinitely
}

Milyonlarca isteği işleyen 50 iş parçacığından oluşan bir iş parçacığı havuzunda, her iş parçacığı sonunda bir veri tutar. RequestContext Son isteğin verilerine, bağlantı ayrıntılarına, kullanıcı kimlik bilgilerine ve istek parametrelerine referans verilir. Bunlar hiçbir zaman toplanmaz çünkü iş parçacıklarının kendileri hiçbir zaman toplanmaz.

4. Büyük Nesne Grafiklerini Yakalayan Kapanışlar

Kapanış fonksiyonları (closures), kapsadıkları kapsamdaki değişkenlere yapılan referansları yakalar. Özellikle JavaScript ve Python'da, küçük bir geri çağırma fonksiyonu veya işleyici, büyük bir nesneyi, tüm bir DOM ağacını, bir istek nesnesini veya büyük bir veri kümesini yanlışlıkla yakalayarak, bunların toplanması gereken zamandan çok sonra bile toplanmasını engelleyebilir.

javascript

// JavaScript: closure capturing large object in event listener
function setupHandler() {
    const largeDataset = fetchLargeDataset(); // 50MB of data

    document.getElementById('btn').addEventListener('click', function() {
        // Only uses one field, but captures entire largeDataset in closure
        console.log(largeDataset.summary);
    });

    // largeDataset cannot be collected as long as the button exists
    // because the event listener closure holds a reference to it
}

// Fix: capture only what you need
function setupHandlerFixed() {
    const largeDataset = fetchLargeDataset();
    const summary = largeDataset.summary; // extract only needed data

    document.getElementById('btn').addEventListener('click', function() {
        console.log(summary); // closure captures only the string
    });
    // largeDataset is now eligible for collection
}

5. Çıkarma İşlemi Olmayan Sınırsız Önbellekler

Önbellekler, üretim servislerinde bellek sızıntılarının en yaygın kaynaklarından biridir. Tahliye politikası olmadan büyüyen bir önbellek, önbelleğin kendisi her zaman erişilebilir olduğundan toplanamayan nesnelerin biriktiği bir yerdir.

csharp

// C#: unbounded dictionary used as cache
public class PriceService {
    // Growing forever -- no eviction, no size limit
    private static Dictionary<string, decimal> _priceCache = new();

    public decimal GetPrice(string productId) {
        if (!_priceCache.TryGetValue(productId, out var price)) {
            price = FetchPriceFromDatabase(productId);
            _priceCache[productId] = price;
        }
        return price;
    }
    // After processing 1 million unique product IDs, the cache
    // holds 1 million entries and grows with every new product
}

// Fix: use a cache with eviction policy
using Microsoft.Extensions.Caching.Memory;

public class PriceService {
    private readonly IMemoryCache _cache;

    public PriceService(IMemoryCache cache) {
        _cache = cache;
    }

    public decimal GetPrice(string productId) {
        return _cache.GetOrCreate(productId, entry => {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
            entry.SizeLimit = 1;  // bounded by cache size configuration
            return FetchPriceFromDatabase(productId);
        });
    }
}

Dil Bazında Bellek Sızıntıları

Java

Java'da statik koleksiyonların ötesinde en yaygın bellek sızıntısı modelleri şunları içerir: finalize() Metotlar ve iç sınıflar. Statik olmayan iç sınıflar, çevreleyen dış sınıf örneğine örtük bir referans tutar. Bir Activity veya Fragment (Android'de) içinde tanımlanan anonim bir sınıf, o Activity'ye bir referans tutar ve bu da döndürme veya gezinme sonrasında toplanmasını engeller.

Java'nın WeakHashMap Anahtarların anlamlı nesneler olduğu durumlarda statik koleksiyon sorununu çözer: anahtar başka bir yerden erişilemez hale geldiğinde, giriş otomatik olarak kaldırılır. Artık referans verilmediğinde serbest bırakılması gereken değerler için, WeakReference hem de SoftReference Nesneleri, koleksiyon oluşturmayı engelleyen güçlü referanslar oluşturmadan sarmalayın.

Tespit araçları: Yığın dökümlerini analiz etmek için Java VisualVM, Eclipse Memory Analyzer (MAT), JProfiler, YourKit ve HeapHero. Şunlara dikkat edin: byte[], char[], ve Object[] Yığın histogramlarındaki büyüme, genellikle genişleyen koleksiyonlardaki veri yapılarını destekler.

Python

Python'ın çöp toplama mekanizması iki katmandan oluşur: referans sayımı (birincil) ve döngü toplayıcı (tamamlayıcı). Referans sayımı katmanı, sayım sıfıra ulaştığında nesneleri hemen serbest bırakır; bu hızlı ve tahmin edilebilir bir işlemdir. Döngü toplayıcı, dairesel referansları ele alır ancak daha az sıklıkla çalışır ve istisnalar içerir.

piton

# Python circular reference with __del__ -- historically problematic
class Node:
    def __init__(self):
        self.child = None

    def __del__(self):
        print("Node deleted")

a = Node()
b = Node()
a.child = b
b.child = a   # circular reference
del a, b      # neither freed in Python 2; freed by cycle collector in Python 3.4+

Python 3.4 ve üzeri sürümler, döngüsel referansların çoğunu doğru şekilde ele alır. Geriye kalan bellek sızıntısı modelleri şunlardır: modül düzeyindeki genel değişkenlerde saklanan nesneler, artan bellek kullanımı. defaultdict or dict Kayıt defteri olarak kullanılan yapılar ve Python çöp toplama mekanizmasına açık olmayan C uzantı modüllerindeki bellek.

Algılama: tracemalloc (standart kütüphane), memory_profiler, objgraph. objgraph.show_most_common_types() hem de objgraph.show_growth() Bu fonksiyonlar hangi türlerin biriktiğini belirler.

C# ve .NET

C#'ta bellek sızıntıları en sık şu nedenlerle oluşur: abonelikten çıkarılmamış olay işleyicileri, abone örneklerine referans tutan statik olay kaynakları ve IDisposable nesnelerinin serbest bırakılmaması.

csharp

// C#: event handler leak
public class Publisher {
    public static event EventHandler DataChanged;  // static event
}

public class Subscriber {
    public Subscriber() {
        Publisher.DataChanged += OnDataChanged;  // strong reference
    }

    private void OnDataChanged(object sender, EventArgs e) { }

    // Missing: Dispose() calling Publisher.DataChanged -= OnDataChanged
}

MKS Subscriber hiçbir zaman toplanmaz Publisher (Statik olduğu için sonsuza dek var olan) bu durum, statik olayın abone örneğine referans veren bir temsilci tutmasından kaynaklanmaktadır. Çözüm, şunu uygulamaktır: IDisposable ve abonelikten çıkma Dispose()veya kullanarak WeakEventManager.

Tespit: .NET Memory Profiler, dotMemory, Visual Studio Diagnostic Tools ve üretim ortamındaki bellek dökümleri için SOS uzantılı WinDbg.

Go

Go, çok düşük bir "stop-the-world" duraklamasıyla işaretleme ve süpürme çöp toplama yöntemini kullanır. Buna rağmen, Go programları, başlatılan ve asla sonlanmayan goroutine'ler aracılığıyla bellek sızıntıları geliştirir.

go

// Go: goroutine leak -- channel never closed, goroutine blocked forever
func processItems(items []Item) {
    results := make(chan Result)

    go func() {
        for _, item := range items {
            results <- process(item)
        }
        // Missing: close(results)
        // This goroutine blocks on the send if no one reads after items run out
    }()

    for result := range results {
        handleResult(result)
    }
    // If the goroutine blocks and results is never closed,
    // the loop here also blocks -- both goroutines leak
}

Sızdırılan her goroutine, kendi yığınını (başlangıçta 2 KB, ihtiyaç duyulduğunda büyüyen) ve referans verdiği tüm yığın nesnelerini tutar. İstek başına bir goroutine sızdıran bir servis, zamanla binlerce goroutine biriktirir.

Algılama: runtime.NumGoroutine() metrik, pprof goroutine profil uç noktası (/debug/pprof/goroutine), ve test için goleak. pprof profili, her goroutine'in yığın izini göstererek, hangi fonksiyonların süresiz olarak bloke olduğunu belirlemeyi kolaylaştırır.

JavaScript ve Node.js

Node.js'de en yaygın bellek sızıntısı modelleri arasında global değişkenler, silinmeden biriken olay yayıcı dinleyiciler ve düzgün kapatılmayan tamponlar/akışlar yer alır.

javascript

// Node.js: EventEmitter listener leak
const EventEmitter = require('events');
const emitter = new EventEmitter();

function createHandler() {
    emitter.on('data', (data) => {
        // This listener is never removed
        // Each call to createHandler adds another listener
        console.log(data);
    });
}

// Called once per request in a server:
// After 11 calls, Node prints MaxListenersExceededWarning
// After thousands of calls, thousands of listeners accumulate

// Fix: use emitter.once() for one-time listeners
// or explicitly remove with emitter.off()

Tespit: Chrome Geliştirici Araçları yığın anlık görüntüleri (tarayıcı JS için), node --inspect Chrome Geliştirici Araçları (Node.js için) ile, clinic.js Üretim ortamında Node.js profillemesi için ve heapdump Zaman içinde yığın anlık görüntülerini yakalamak ve aralarındaki farkları karşılaştırmak için kullanılan npm paketi.

GC Dillerinde Önleme Kalıpları

Üretim ortamında ortaya çıktıktan sonra bellek sızıntılarını gidermek yerine, bu yapısal kalıplar en yaygın nedenlerin önlenmesini sağlar:

Zayıf referanslar Bir nesnenin toplanmasına engel olmadan ona referans verilmesine izin verin. Çöp toplama işlemi belleğe ihtiyaç duyduğunda, zayıf referanslı nesneler, onlara referanslar mevcut olsa bile toplanmaya uygundur. Kullanım WeakReference<T> Java ve C# dillerinde, weakref.ref() Python dilinde ve WeakRef JavaScript'te, referans veren nesnenin referans verilen nesnenin ömrünü uzatmaması gereken önbellekler ve gözlemci kayıtları için kullanılır.

Boyut sınırlı önbellekler ve tahliye mekanizması ham maddeyi değiştir HashMap or dict Amaca yönelik olarak tasarlanmış önbellek uygulamalarıyla: Guava'nın CacheBuilder Java'da, functools.lru_cache or cachetools Python'da, IMemoryCache C#'da boyut sınırlamalarıyla birlikte, node-lru-cache Node.js'de.

Açık yaşam döngüsü yönetimi Kayıtlı dinleyicilere, açık kaynaklara veya iş parçacığına özgü duruma sahip nesnelerin bir close(), dispose()veya temizleme işlemini gerçekleştiren eşdeğer bir yöntem ve çağıranların her zaman bu yöntemi çağırması, şu yolla sağlanır: try-with-resources Java'da, with Python'da ifadeler, using C#'da bildirimler ve defer Go'da.

Gorutin iptali Go'da kullanımlar context.Context İptal mekanizmasıyla her goroutine'in sonlanma yoluna sahip olmasını sağlar. Seçim yapan goroutine'ler ctx.Done() Çıkış sinyali verilebilir ve böylece birikme önlenebilir.

Statik Analiz Üretim Öncesinde Potansiyel Sızıntıları Nasıl Bulur?

Bellek profilleme, bellek sızıntıları ortaya çıktıktan sonra bulur. Statik analiz ise kod çalışmadan önce bu sızıntılara neden olan yapısal kalıpları bulur.

SMART TS XL'S statik kod analizi Java, Python, C#, JavaScript ve diğer dillerdeki kod tabanlarının yapısal özelliklerini inceleyerek, istenmeyen veri tutma ile ilişkili kalıpları belirler: belgelenmiş silme işlemi olmayan statik koleksiyonlar, karşılık gelen kayıt silme işlemi olmayan olay dinleyici kayıtları, temizleme işlemi yapılmayan ThreadLocal kullanımı ve karşılık gelen imha işlemi olmayan kaynak tahsisleri.

Uygulama bağımlılık eşleme özelliği, çöp toplama dili bellek sızıntılarının tipik olarak içerdiği uzun referans zincirlerini ortaya çıkarır; bu zincirlerdeki nesnelere program köklerinden birkaç dolaylılık seviyesi üzerinden ulaşılabilir ve köke giden yol, nesneleri geçici olarak tutmak için tasarlanmış uzun ömürlü bir kapsayıcıdan geçer.

Bellek yönetimi modellerinin yıllar veya on yıllar önce oluşturulduğu büyük eski kod tabanlarını yöneten kuruluşlar için, SMART TS XL'S etki analizi Bu yetenek, düzeltme işlemini kapsamlı ve sistematik hale getirir: belirli bir anti-pattern'in (sınırsız statik önbellek, kaldırılmamış dinleyici) oluştuğu her yeri belirleyin, etkilenen bileşenleri listeleyin ve düzeltme işlemlerini, üretimdeki olayları tek tek keşfetmek yerine, risk sırasına göre planlayın.