Sparround

Yaddaş modeli: stack və heap

Proqram işlədikcə yaddaş iki əsas nahiyəyə bölünür.

Stack (call stack) — funksiya çağırışlarının yaddaşı. Hər çağırış üçün stack frame yaradılır: parametrlər, lokal dəyişənlər, qayıdış ünvanı. Funksiya bitəndə frame dərhal silinir. Stack sürətlidir (sadəcə pointer irəli-geri sürüşür), amma ölçüsü məhduddur (adətən bir neçə MB).

Heap — obyektlərin, massivlərin, kolleksiyaların saxlandığı böyük, sərbəst nahiyə. Ayırma daha bahalıdır, ömür müddəti isə funksiyanın bitməsindən asılı deyil — obyekt ona istinad olduqca yaşayır.

Sadə qayda: kiçik və sabit ölçülü dəyərlər stack-də, dinamik ölçülü strukturlar heap-də.

Value semantics vs reference semantics — data strukturlarını anlamağın açarıdır.

  • Value (dəyər): dəyişəndə dəyərin özü saxlanılır. Mənimsətmə nüsxə yaradır. Nümunə: int, double, boolean.
  • Reference (istinad): dəyişəndə heap-dəki obyektin ünvanı saxlanılır. Mənimsətmə ünvanı kopyalayır — iki dəyişən eyni obyektə baxır.

Məsələn: const a = { n: 1 } yazıb sonra const b = a etsən, ünvan kopyalanır, obyekt yox. b.n = 2 sətrindən sonra a.n da 2-dir — çünki ab heap-də eyni obyektə baxır.

Buna görə funksiyaya obyekt ötürəndə funksiya onu dəyişə bilir: pointer nüsxələnir, obyekt yox. Bu, linked list, tree və graph strukturlarının işləmə prinsipidir — node-lar bir-birinə istinadlarla bağlanır.

AnlayışJavaScriptKotlinDart
Primitive dəyərlərnumber, string, boolean, null, undefined, symbol, bigint — value semanticsInt, Double, Boolean və s. — JVM-də mümkün olanda stack-də, boxing zamanı heap-dəTexniki olaraq hər şey obyektdir, amma int/double/bool/String immutable-dir və dəyər kimi davranır
ObyektlərObject, Array, Map, Set, funksiyalar — heap, reference semanticsBütün class instansları heap-də, reference semanticsBütün obyektlər heap-də, reference semantics
Null təhlükəsizliyiYoxdur — `undefined`/`null` runtime-da partlayırTip sistemində: `String` vs `String?`, `?.`, `?:`, kompilyator yoxlayırSound null safety: `String` vs `String?`, `!`, `??`, kompilyator yoxlayır
Dayaz nüsxə`{...obj}`, `[...arr]`, `structuredClone()` dərin nüsxə üçün`data class`-da `copy()` (dayazdır!), `toList()``List.of(x)`, `Map.of(m)`, `{...map}`
Garbage collectionGenerational, mark-and-sweep (V8 mühərriki)JVM-in GC-si — G1, ZGC və s.Generational; hər isolate-in öz GC-si var

Garbage collection (GC) — heap-də artıq heç kimin istinad etmədiyi obyektlərin avtomatik təmizlənməsi. Hər üç dil (JS, Kotlin/JVM, Dart) GC-lidir; free() çağırmaq lazım deyil.

Müasir GC-lər "reference counting" yox, reachability ilə işləyir: GC root-lardan (qlobal dəyişənlər, aktiv stack frame-lər) başlayaraq gəzir və çatıla bilməyən hər şeyi silir. Buna görə dövrəvi istinadlar (a.next = b; b.prev = a) problem yaratmır — hər ikisi root-dan qopubsa, hər ikisi silinir.

Yaddaş sızması (memory leak) GC-li dildə də olur: obyekt artıq lazım deyil, amma nəyəsə hələ də istinad edir. Tipik səbəblər: böyüyən qlobal keş, silinməyən event listener-lər, artıq bağlanmış obyektə baxan timer/subscription.

İnterview məsləhəti. Ən çox verilən sual: "`const arr = [1,2,3]` — `const` olduğu halda niyə `arr.push(4)` işləyir?" Güclü cavab: const binding-i (dəyişənin hansı ünvana baxdığını) dondurur, obyektin özünü yox; arr = [] xəta verər, arr.push(4) verməz. Bunu "stack-də ünvan, heap-də obyekt" şəkli ilə izah etmək çox təsirlidir.

İkinci klassik sual: "Stack overflow nə vaxt olur?" — çox dərin və ya sonsuz rekursiyada, çünki hər çağırış yeni frame tutur və stack məhduddur (JS-də təxminən 10 000 dərinlik). Əlavə et ki, bəzi dillərdə tail-call optimizasiyası bunu həll edir, amma JS mühitlərinin əksəriyyətində praktikada yoxdur — buna görə dərin rekursiyanı iterativ həllə və ya açıq stack-ə çevirmək lazımdır.

Ən çox rast gəlinən səhv: dayaz nüsxəni dərin nüsxə sanmaq. {...obj} yalnız birinci səviyyəni kopyalayır; içəridəki obyektlər hələ də paylaşılır.

📚 Mənbələr və sənədlər