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 a və b 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ış | JavaScript | Kotlin | Dart |
|---|---|---|---|
| Primitive dəyərlər | number, string, boolean, null, undefined, symbol, bigint — value semantics | Int, 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ər | Object, Array, Map, Set, funksiyalar — heap, reference semantics | Bütün class instansları heap-də, reference semantics | Bütün obyektlər heap-də, reference semantics |
| Null təhlükəsizliyi | Yoxdur — `undefined`/`null` runtime-da partlayır | Tip sistemində: `String` vs `String?`, `?.`, `?:`, kompilyator yoxlayır | Sound 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 collection | Generational, 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
- Yaddaşın idarə olunmasırəsmideveloper.mozilla.org