CodeGym /Kurslar /C# SELF /C#-də yaddaş: stack, heap və garbage collector (

C#-də yaddaş: stack, heap və garbage collector ( GC)

C# SELF
Səviyyə, Dərs
Mövcuddur

1. Yaddaşın əsasları: Stack və Heap (Stack & Heap)

Proqramınız işə düşəndə yaddaşa çıxış əldə edir. .NET kontekstində (CLR — Common Language Runtime) bu yaddaş avtomatik idarə olunur və iki əsas sahəyə bölünür: Stack və Heap. Məlumatların harada və necə saxlandığını anlamaq effektiv və stabil kod yazmaq üçün kritikdir.

Stack

Stack-i boşqab yığını kimi təsəvvür et: yeni boşqabı həmişə üstə qoyursan və boşqabı də üstdən götürürsən. Bu LIFO (Last In, First Out) prinsipi — «son gələn, ilk gedən». Stack çox sürətli, amma ölçüsü məhduddur.

Stack-də nələr saxlanılır:

  • Value types: birbaşa elan olunmuş dəyişənlərin öz dəyərləri.
  • Referanslara baxışlar (references): reference type-lar üçün stackdə yalnız obyektin heap-dəki ünvanı saxlanılır.
  • Method parameters: funksiyalara ötürülən dəyərlər.
  • Local variables: metod daxilində elan olunan dəyişənlər.
  • Return addresses: metod bitdikdən sonra hara qayıtmaq lazım olduğunu göstərən ünvanlar.

Stack-də yaddaş avtomatik və çox sürətlə ayrılır və azad edilir — uyğun metod bitəndə və ya dəyişən scope-dan çıxdıqda.

Nümunə: Value type-lar stack-də

void MyMethod()
{
    int a = 10;       // 'a' və dəyər 10 - stack-də
    bool flag = true; // 'flag' və dəyər true - stack-də
    char initial = 'Z'; // 'initial' və dəyər 'Z' - stack-də

    // ...
} // MyMethod bitdikdə 'a', 'flag', 'initial' stack-dən silinir.

Nümunə: Stack-də referanslar

class MyObject { }

void AnotherMethod()
{
    MyObject objRef; // 'objRef' (referans) - stack-də. Obyekt hələ yaradılmayıb.
    // ...
} // AnotherMethod bitdikdə 'objRef' (referans) stack-dən silinir.

Heap

Heap daha böyük və daha çevik yaddaş sahəsidir. Burada LIFO kimi sərt qayda yoxdur; məlumatlar istənilən boş yerə yerləşdirilə bilər. Heap daha böyük və/vəya uzunömürlü obyektlər üçün istifadə olunur.

Heap-də nələr saxlanılır:

  • Reference type obyektləri: class-ların, array-lərin, string-lərin məlumatları — new vasitəsilə yaradılır.
  • Nested value types: əgər value type (struct) reference type (class)-ın field-disədirsə, o obyektin içində heap-də saxlanılır.

Heap-də yaddaşın idarə olunması avtomatik olaraq GC (Garbage Collector) tərəfindən aparılır.

Nümunə: Class obyektləri heap-də

class Person { public string Name; public int Age; }

void CreatePerson()
{
    Person p = new Person(); // Person obyekti - heap-də. 'p' (referans) - stack-də.
    p.Name = "Alice";        // "Alice" string-i (o da obyekt) - heap-də.
    p.Age = 30;              // 30 (int, value type) - Person obyektinin içində heap-də.
    // ...
} // CreatePerson bitdikdə 'p' (referans) stack-dən silinir.
  // Person obyekti heap-də unreachable olur və GC tərəfindən toplana bilər.

Nümunə: Array-lar heap-də

void ProcessArray()
{
    int[] numbers = new int[5]; // 5 int-li massiv - heap-də. 'numbers' (referans) - stack-də.
    numbers[0] = 10;            // 10 elementin bir hissəsidir və heap-də saxlanılır.
    // ...
} // Massiv üzərində referans qalmadıqda GC tərəfindən təmizlənəcək.

2. Garbage Collector (Garbage Collection)

Heap-də yaddaşın idarə olunması avtomatik olaraq GC sayəsində baş verir. Bu .NET-in əsas xüsusiyyətlərindən biridir və yaddaşın manual boşaldılmasından (manual memory management) xilas edir.

Məqsəd və iş prinsipi

Əsas məqsəd GC — proqram tərəfindən artıq istifadə edilməyən heap obyektlərini avtomatik boşaltmaqdır.

  1. Obyekt yaradılması: new zamanı CLR heap-də yer ayırır.
  2. Referansların izlənməsi: GC aktiv referansları izləyir; obyekt "reachable" sayılırsa stack-dən, static field-dən və ya başqa reachable obyektlərdən referans var deməkdir.
  3. "Garbage" olaraq müəyyən etmə: referans yoxdursa obyekt "unreachable" və garbage hesab olunur.
  4. Toplama (Collection): lazım olduqda GC reachable obyektləri mark edir, unreachable-ların yaddaşını azad edir.
  5. Compaction: fragmentasiyanı azaltmaq üçün qalan obyektlər köçürülərək ardıcıl sahələr yaradılır.

GC-də nəsillər (Generational GC)

GC nəsillərdən istifadə edir, çünki əksər obyektlər "gənc vaxtlarında" ölür:

  • Generation 0 (Gen 0): yeni obyektlər; collection-lar tez-tez və sürətlidir.
  • Generation 1 (Gen 1): Gen 0-dan sağ çıxmış obyektlər; collection-lar daha nadir və daha uzun.
  • Generation 2 (Gen 2): uzunömürlü obyektlər; ən nadir və ən bahalı collection-lar.

GC nə zaman işə düşür?

  • Yeni ayırma üçün kifayət qədər boş yer olmadıqda.
  • Idle vaxtlarda müntəzəm yoxlamalar zamanı.
  • Explicit (ümumiyyətlə tövsiyə olunmayan) çağırışla: GC.Collect().

GC və performans

GC-nin işə düşməsi qısa pauzalara səbəb ola bilər, çünki istifadəçi kodunun icrası dayandırılır. Heap-də çox sayda kısaömürlü obyekt yaratmaq collection-ları tez-tez etdirir və performansı azaldır.

Nümunə: Obyekt unreachable olur

class DataBlock { public byte[] Data; public DataBlock() => Data = new byte[1024 * 1024]; } // 1 MB data

void AllocateAndLose()
{
    DataBlock block1 = new DataBlock(); // block1 obyekti heap-də, 'block1' referansı stack-də

    // ... bəzi kod bloqu ...

    block1 = null; // İndi DataBlock obyektinə aktiv referans yoxdur. O unreachable oldu.
    // Bu mərhələdə GC HƏLƏ ONU SİLМİR, amma o silinmə namizədi oldu.

    // Burada GC.Collect() çağırışı (yalnız demo üçün, production-da etməyin!)
    Console.WriteLine("Calling GC.Collect()");
    GC.Collect(); // GC indi yaddaşı təmizləyə bilər (amma zəmanət yoxdur)
}

AllocateAndLose();
// AllocateAndLose bitdikdə 'block1' referansı stack-dən silinir,
// və heap-dəki DataBlock obyekti garbage collection üçün namizəd olur.

3. Unmanaged resursların idarə olunması

GC .NET-in "managed" yaddaşını idarə edir. Amma unmanaged resurslar var ki, onlar GC-nin nəzarətindən kənardadır və onların əl ilə azad edilməsi tələb olunur:

  • File descriptors (açıq fayllar).
  • Network sockets.
  • Window/graphics handles (məsələn, GDI+).
  • OS-dən ayrılmış yaddaş (məsələn, P/Invoke vasitəsilə).

Finalizerlər (Destructors)

Finalizer — obyekt GC tərəfindən silinməzdən əvvəl unmanaged resursların son təmizlənməsini edən xüsusi metoddur. Sintaksis konstruktor kimi görünür, amma önündə tilde var:

class MyClassWithFinalizer
{
    ~MyClassWithFinalizer() { /* Unmanaged resursların azad edilməsi */ }
}

Finalizer GC tərəfindən qeyri-müəyyən vaxtda, finalizer thread-də çağırılır. Mənfi cəhətlər: qeyri-müəyyənlik, əlavə overhead (finalizer-li obyektlərə iki keçid lazım olur). Buna görə finalizerlər yalnız "sığorta" kimi istifadə olunmalıdır, əsas mexanizm deyillər.

Nümunə: Finalizer "sığorta" kimi

class UnmanagedResourceHolder
{
    private bool _resourceReleased = false;

    // Unmanaged resursun simuliasiyası
    public UnmanagedResourceHolder() => Console.WriteLine("Resurs yaradıldı.");
    
    public void ReleaseResource()
    {
        if (!_resourceReleased)
        {
            Console.WriteLine("Resurs AÇIQ-çəxildi (explicit metodla).");
            _resourceReleased = true;
        }
    }

    // Finalizer - GC tərəfindən son çarə kimi çağırılır
    ~UnmanagedResourceHolder()
    {
        Console.WriteLine("Finalizer ÇAĞIRILDI (resurs explicit azad edilmədi).");
        ReleaseResource(); // Resursu azad etməyə cəhd
    }
}

IDisposable interfeysi

IDisposable — resursların explicit azad edilməsi üçün üstünlük verilən mexanizmdir. Bir metoda sahibdir: void Dispose(). Tez-tez Dispose() çoxistifadəlidir və finalizer-i söndürmək üçün GC.SuppressFinalize(this) çağırışı edilir.

Nümunə: IDisposable-in implementasiyası

using System.IO;

class MyFileWriter : IDisposable
{
    private StreamWriter _writer;
    private bool _disposed = false;

    public MyFileWriter(string path)
    {
        _writer = new StreamWriter(path, true);
        Console.WriteLine($"Fayl '{path}' açıldı.");
    }

    public void WriteLog(string message) => _writer.WriteLine(message);

    // IDisposable implementasiyası
    public void Dispose()
    {
        Dispose(true); // Əsas Dispose məntiqini çağırırıq
        GC.SuppressFinalize(this); // GC-ə finalizer-in artıq lazım olmadığını deyirik
        _disposed = true;
    }

    // Protected virtual Dispose(bool) - ümumi pattern üçün
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Managed resursları azad et
                _writer?.Dispose();
            }
            // Unmanaged resursları azad et (əgər varsa)
            Console.WriteLine("Fayl Dispose vasitəsilə BAĞLANDI.");
        }
    }

    // Optional finalizer "sığorta" kimi
    ~MyFileWriter()
    {
        Console.WriteLine("Fayl finalizer vasitəsilə BAĞLANDI (Dispose() çağırılmamışdı).");
        Dispose(false); // Dispose-u explicit çağırılmış kimi yox, finalizer rejimində çağırırıq
    }
}

using operatoru

using operatoru sintaktik şəkərdir və obyekt scope-dan çıxdıqda Dispose()-un çağırılmasını təmin edir, hətta exception baş versə də. IDisposable implement edən obyektlərlə işləməyin ən tövsiyə olunan yoludur.

Nümunə: using ilə avtomatik təmizləmə

// Yuxarıdakı MyFileWriter nümunəsindən istifadə
void ProcessFile(string fileName)
{
    // MyFileWriter obyekti using bloku bitəndə avtomatik "dispose" olunacaq
    using (var writer = new MyFileWriter(fileName))
    {
        writer.WriteLog("Birinci sətir.");
        writer.WriteLog("İkinci sətir.");
        // Burada exception olsa belə, Dispose() çağırılacaq
    } // Burada avtomatik olaraq writer.Dispose() çağırılır

    Console.WriteLine("Using bloku bitdi, fayl bağlandı.");
}

ProcessFile("testlog.txt");
// Bu halda MyFileWriter-in finalizer-i çağırılmayacaq, çünki Dispose() explicit çağırıldı.

Bu konsepsiyaları anlamaq C# ilə performanslı, etibarlı və miqyaslana bilən tətbiqlər yazmaq üçün açardır.

2
Tapşırıq
C# SELF, səviyyə, dərs
Bağlanıb
Stack və heap arasındakı fərq
Stack və heap arasındakı fərq
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION