Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. Stack va heap
- 2.1 Ikki xil xotira
- 2.2 Qiymat qayerda yashaydi
- 3. Yetib boriladiganlik (reachability)
- 3.1 Ildizlar va havolalar grafi
- 3.2 Halqa — muammo emas
- 4. Mark-and-sweep: belgilash va supurish
- 5. V8 ning avlodli GC'si
- 5.1 Ko'p obyekt yosh o'ladi
- 5.2 --trace-gc bilan kuzatish
- 5.3 Pauzalar va Orinoco
- 6. Xotirani o'lchash
- 6.1 process.memoryUsage()
- 6.2 Tajriba: million buyurtma
- 6.3 Heap chegarasi
- 7. Ko'p uchraydigan xatolar
- 7.1 "null berdim — xotira bo'shadi"
- 7.2 delete xotirani bo'shatadi deb o'ylash
- 7.3 Real kodda gc() chaqirish
- 7.4 FinalizationRegistry ga mantiq bog'lash
- 8. Mashqlar
- 1-mashq (oson): Kim tirik?
- 2-mashq (o'rta): GC'ni sanang
- 3-mashq (qiyin): Bitta buyurtma necha bayt?
- 9. Real ishda
- Xulosa
- Manbalar
Xotira boshqaruvi va garbage collection: stack, heap va axlat yig'uvchi
Qisqacha: JavaScript'da xotirani qo'lda bo'shatmaysiz — buni axlat yig'uvchi (garbage collector, GC) qiladi. Qoida bitta: qiymatga ildizlardan (global o'zgaruvchilar, joriy funksiyalarning o'zgaruvchilari) havolalar orqali yetib borib bo'lmasa — u axlat va o'chiriladi. V8 xotirani ikki avlodga bo'ladi: ko'p obyektlar yosh o'ladi, shuning uchun yosh avlod tez-tez va tez tozalanadi (Scavenge), eski avlod — kamroq va og'irroq (Mark-Compact). Havolaga
nullberish obyektni darhol o'chirmaydi — faqat GC navbatdagi ishida o'chiradi.
Bu darsda
- Stack va heap farqini va qaysi qiymat qayerda yashashini tushuntira olasiz.
- "Yetib boriladiganlik" (reachability) qoidasi bo'yicha obyekt qachon axlat bo'lishini aniqlay olasiz — halqali havolalarda ham.
- Mark-and-sweep va V8 ning avlodli GC'sini (Scavenge, Mark-Compact) tushuntira olasiz.
process.memoryUsage(),--expose-gcva--trace-gcbilan xotirani o'lchab, natijani o'qiy olasiz.
Oldin bilishingiz kerak: JS dvigateli ichida, WeakMap, WeakSet, WeakRef va FinalizationRegistry, Havola semantikasi.
1. Nega bu kerak?
Havola semantikasi darsida kichik va'da bergan edik: "Havolasi qolmagan obyekt nima bo'ladi? JavaScript uni avtomatik ravishda xotiradan o'chiradi". Bugun o'sha "avtomatik"ning ichiga kiramiz.
C yoki C++ tillarida dasturchi xotirani o'zi so'raydi va o'zi qaytaradi. Qaytarishni unutsa — xotira tugaydi. Ikki marta qaytarsa — dastur qulaydi. JavaScript bu ishni sizdan oldi: siz faqat obyekt yaratasiz, o'chirishni dvigatel o'zi hal qiladi. Qulay. Lekin bu "bepul" emas:
- GC ishlaganda dastur bir lahzaga to'xtashi mumkin — animatsiya "qoqiladi".
- GC faqat yetib bo'lmaydigan narsani o'chiradi. Siz bilmasdan havolani saqlab qolsangiz — xotira to'lib boradi. Bu keyingi darsning mavzusi: Xotira sizishlari.
- Node serverida xotira chegarasi bor. Undan oshsa — server qulaydi.
«Bahor»dan o'xshatish. Oshxonada stollar bor. Mehmon ketdi — stolni tozalash kerak. Lekin ofitsiant har daqiqada har stolga yugurmaydi. U vaqti-vaqti bilan zalni aylanib chiqadi: kim hali o'tiribdi — tegmaydi, bo'sh stoldagi idishlarni yig'adi. Axlat yig'uvchi ham shunday: vaqti-vaqti bilan aylanib, "hali kimdir ishlatayotgan" narsalarni qoldiradi, qolganini yig'adi.
2. Stack va heap
2.1 Ikki xil xotira
Dastur ishlaganda xotira ikki joyga bo'linadi:
- Stack (stek) — funksiya chaqiruvlari uchun xotira. Har chaqiruvda unga "kadr" qo'shiladi: funksiyaning lokal o'zgaruvchilari. Funksiya tugadi — kadr olib tashlanadi. Bu chaqiruvlar steki (call stack) ning o'zi. Tez, tartibli, lekin kichik.
- Heap (uyum) — obyektlar, massivlar, funksiyalar yashaydigan katta, tartibsiz xotira. Ular qachon kerak bo'lmay qolishini oldindan bilib bo'lmaydi — shuning uchun ularni GC kuzatadi.
O'xshatish: stack — ofitsiantning qo'lidagi patnis. Narsa qo'yildi, olib ketildi — tez va tartibli. Heap — oshxonadagi katta omborxona. U yerdagi idishlar kerak bo'lmay qolganini kimdir tekshirib turishi kerak.
2.2 Qiymat qayerda yashaydi
function buyurtmaQil() {
const soni = 2;
const taom = { nom: "Osh", narx: 35000 };
return taom;
}
const tushlik = buyurtmaQil();
console.log(tushlik.nom); // OshbuyurtmaQil chaqirilganda stack'da kadr ochiladi. soni — oddiy son, kadrning o'zida. taom esa ikki qismdan iborat: obyektning o'zi heap'da yaratiladi, kadrda faqat unga havola turadi. Funksiya tugadi — kadr va soni yo'q bo'ldi. Obyekt esa tirik: uning havolasi tushlik ga qaytdi.
flowchart LR
subgraph S["Stack"]
G["global kadr: tushlik"]
end
subgraph H["Heap"]
O["{ nom: 'Osh', narx: 35000 }"]
end
G --> OSoddalashtirilgan rasm: haqiqatda V8 ba'zi qiymatlarni optimallashtirib boshqacha joylaydi (oldingi darslardagi Smi, closure muhiti). Lekin qoida o'sha: obyektlar heap'da yashaydi, o'zgaruvchilarda — havola. Shuning uchun Havola semantikasi darsida nusxa "o'zgarib ketgan" edi: ikki o'zgaruvchi heap'dagi bitta obyektga qaragan.
Tekshirib ko'ring:
function f() { const m = [1, 2, 3]; return m.length; }—f()tugagach massivga nima bo'ladi?
Javob
Massiv heap'da qoladi, lekin unga hech qanday havola yo'q: m stack kadri bilan birga yo'qoldi, qaytgan qiymat — son 3, massiv emas. Massiv endi axlat: GC navbatdagi ishida uni o'chiradi.
3. Yetib boriladiganlik (reachability)
3.1 Ildizlar va havolalar grafi
GC "bu obyekt kerakmi?" degan savolga qanday javob beradi? U dastur kelajakda nima qilishini bilmaydi. Shuning uchun aniq va xavfsiz qoidaga tayanadi: yetib boriladiganlik (reachability).
- Ildizlar (roots) — har doim tirik deb hisoblanadiganlar: global o'zgaruvchilar, hozir bajarilayotgan funksiyalarning lokal o'zgaruvchilari, closure ushlab turgan o'zgaruvchilar, ro'yxatdan o'tgan tinglovchi va taymerlar.
- Ildizdan havolalar zanjiri orqali yetib borsa bo'ladigan har bir qiymat — tirik.
- Qolgani — axlat.
Qadamlarda obyekt qachon axlatga aylanishini kuzating:
5-qadamga e'tibor bering: buyurtma = null obyektni o'chirmadi. U faqat bitta yo'lni kesdi. Obyekt oxirgi yo'l kesilgandagina axlatga aylandi.
3.2 Halqa — muammo emas
Ikki obyekt bir-biriga havola qilsa-chi? Stol mehmonni "eslaydi", mehmon esa stolni. Havolalarni sanaydigan sodda usulda (har obyektga nechta havola borligini sanash) ular hech qachon o'chmasdi: har birida bitta havola bor. Lekin yetib boriladiganlik qoidasi boshqa savol beradi: "ildizdan yetib borsa bo'ladimi?"
flowchart LR
R["Ildizlar: global, stack"] --> A["menyu"]
A --> B["osh"]
R -. "havola uzildi" .-> C["stol"]
C --> D["mehmon"]
D --> Cmenyu va osh — ildizdan yo'l bor, tirik. stol va mehmon bir-biriga ushlashib turibdi, lekin ildizdan ularga yo'l yo'q — ikkalasi ham axlat.
Buni o'lchab ko'ramiz. FinalizationRegistry obyekt o'chirilganda bizga xabar beradi. gc() funksiyasi esa odatda yo'q — uni Node'ning --expose-gc bayrog'i ochadi: "GC'ni hozir ishga tushir":
// node --expose-gc halqa.mjs
const ochirilganlar = [];
const kuzatuvchi = new FinalizationRegistry((nom) => {
ochirilganlar.push(nom);
if (ochirilganlar.length === 2) {
// tartib kafolatlanmaydi — saralab chiqaramiz
console.log("o'chirildi:", ochirilganlar.sort());
}
});
let stol = { raqam: 5 };
let mehmon = { ism: "Jasur" };
stol.mehmon = mehmon; // stol → mehmon
mehmon.stol = stol; // mehmon → stol: halqa!
kuzatuvchi.register(stol, "stol");
kuzatuvchi.register(mehmon, "mehmon");
stol = null;
mehmon = null;
console.log("havolalar uzildi");
gc();Konsolda (Node 24.21):
havolalar uzildi
o'chirildi: [ 'mehmon', 'stol' ]Halqa GC'ga to'sqinlik qilmadi. Ikki eslatma. Birinchidan, FinalizationRegistry xabarlari gc() dan keyin, keyinroq alohida task sifatida keladi — ularning tartibi va vaqti kafolatlanmaydi (bizda ham bir safar stol birinchi, bir safar mehmon birinchi keldi). Shuning uchun nomlarni yig'ib, saralab chiqardik. Ikkinchidan, gc() — faqat tajriba uchun (JS dvigateli ichida darsidagi bayroqlar kabi).
Tekshirib ko'ring:
halqa.mjsdastol = nullqatorini o'chirsak (faqatmehmon = nullqolsa), qaysi obyekt o'chiriladi?
Javob
Hech biri. stol o'zgaruvchisi — ildiz, u stol obyektiga yetib boradi, stol esa stol.mehmon orqali mehmonga. Ikkalasi ham yetib boriladigan, demak tirik.
4. Mark-and-sweep: belgilash va supurish
GC yetib boriladiganlikni qanday tekshiradi? Asosiy algoritm — mark-and-sweep ("belgila va supur"). U ikki bosqichdan iborat:
- Belgilash (mark). Ildizlardan boshlab, havolalar bo'ylab yuriladi. Har topilgan obyektga "tirik" belgisi qo'yiladi. Belgilangan obyektga qayta kirilmaydi — shuning uchun halqada aylanib qolmaydi.
- Supurish (sweep). Butun heap ko'rib chiqiladi. Belgisiz obyektlar egallagan joy "bo'sh" deb e'lon qilinadi.
Oshxonada: ofitsiant kechqurun zalni aylanadi va har band stolga "band" yorlig'ini qo'yadi (belgilash). Keyin yorliqsiz hamma stolni yig'ishtiradi (supurish). Ertasiga yorliqlar olib tashlanadi va hisob yangidan boshlanadi.
Ko'pincha uchinchi qadam ham bo'ladi — zichlash (compact): tirik obyektlar bir joyga suriladi, orada mayda bo'sh tirqishlar qolmasin. Ofitsiant stollarni yig'ib, hamma mehmonni zalning bir tomoniga o'tkazgandek — keyin katta guruhga joy topish oson. V8 ning to'liq GC'si shuning uchun Mark-Compact deb ataladi.
5. V8 ning avlodli GC'si
5.1 Ko'p obyekt yosh o'ladi
Butun heap'ni har safar belgilash — qimmat. V8 bir kuzatuvga tayanadi. U avlod gipotezasi (generational hypothesis) deb ataladi: "ko'p obyektlar yosh o'ladi" (V8 blogi, "Trash talk: the Orinoco garbage collector", 2019-01-03). Sikl ichidagi vaqtinchalik obyekt, map qaytargan oraliq massiv, satrlar birlashmasi — yaratiladi va bir zumda keraksiz bo'ladi. Oshxonadagi choy piyolalari kabi: ko'pi bir necha daqiqada bo'shaydi.
Shuning uchun heap ikki avlodga bo'lingan:
flowchart LR
N["Yangi obyekt"] --> Y["Yosh avlod: nursery"]
Y -- "1-GC dan omon" --> I["Yosh avlod: intermediate"]
I -- "2-GC dan omon" --> O["Eski avlod"]
Y -- "o'ldi" --> X["bo'shatildi"]
I -- "o'ldi" --> X- Yosh avlod (young generation) — kichik. Hamma yangi obyekt shu yerda tug'iladi. Ichida ikki bo'lim bor: nursery ("bolalar bog'chasi" — eng yangilari) va intermediate ("oraliq" — bir marta GC'dan omon qolganlar). Yosh avlodni Scavenge (yosh avlod GC'si, "minor GC") tez-tez tozalaydi. U faqat tirik obyektlarni boshqa joyga ko'chiradi, qolgan hamma joy birdaniga bo'sh deb hisoblanadi. Tiriklar oz bo'lgani uchun — juda tez.
- Ikki marta GC'dan omon qolgan obyekt eski avlodga (old generation) o'tadi. Bu yerda Mark-Compact ("major GC") ishlaydi — kamroq, lekin og'irroq.
Oshxonada Scavenge shunday bo'lardi. Patnisda 20 ta piyola bor, ulardan faqat ikkitasida hali choy ichilyapti. Ofitsiant o'sha ikkitasini toza patnisga o'tkazadi, eski patnisni esa butunlay yuvishga beradi. 18 ta bo'sh piyolani birma-bir sanab o'tirmaydi. Qozon, stol, samovar esa yillab turadi — ular "eski avlod". Ularni har soatda tekshirish shart emas.
5.2 --trace-gc bilan kuzatish
Node'da --trace-gc bayrog'i har GC haqida bitta qator chiqaradi. Uch million vaqtinchalik chek yaratamiz, har o'ninchisini saqlaymiz:
// gc-kuzat.mjs — node --trace-gc gc-kuzat.mjs
const saqlangan = [];
for (let i = 0; i < 3_000_000; i++) {
const chek = { id: i, jami: 35000 * (i % 3) }; // vaqtinchalik
if (i % 10 === 0) {
saqlangan.push(chek); // har 10-chisi qoladi
}
}
console.log("saqlandi:", saqlangan.length);Haqiqiy chiqishdan parcha (Node 24.21; boshidagi jarayon raqami olib tashlandi, … — o'tkazib yuborilgan o'xshash qatorlar):
26 ms: Scavenge 4.9 6.5-bob -> 4.1 7.5-bob MB, pooled: 0 MB, 0.54 / 0.00 ms (average mu = 1.000, current mu = 1.000) allocation failure;
…
62 ms: Mark-Compact 14.5 29.6-bob -> 10.7 27.9-bob MB, pooled: 0 MB, 4.98 / 0.00 ms (+ 0.8 ms in 24 steps since start of marking, biggest step 0.1 ms, walltime since start of marking 6 ms) (average mu = 0.897, current mu = 0.897) finalize incremental marking via stack guard; GC in old space requested
saqlandi: 300000Bitta qatorni bo'laklab o'qiymiz:
| Qism | Ma'nosi |
|---|---|
26 ms: |
dastur boshlanganidan beri o'tgan vaqt |
Scavenge / Mark-Compact |
qaysi GC: yosh avlod yoki to'liq |
4.9 … -> 4.1 … MB |
ishlatilgan xotira GC'dan oldin → keyin; qavsdagi son — heap uchun ajratilgan joy |
0.54 / 0.00 ms |
pauza — dastur shuncha to'xtadi |
allocation failure |
sabab: yosh avlodda joy tugadi |
allocation failure qo'rqitmasin — bu xato emas. "Yangi obyektga joy qolmadi, tozalash vaqti keldi" degani.
Yetti marta ishga tushirdik — har safar 23 ta Scavenge va 1 ta Mark-Compact. Eng uzun pauza taxminan 5–8 ms, hammasi yig'indisi taxminan 40 ms (i5-12500H, Windows 11). Ko'rinib turibdi: yosh avlod GC'si tez-tez va qisqa, to'liq GC — bir marta va uzunroq.
5.3 Pauzalar va Orinoco
GC ishlayotganda obyektlar o'zgarib tursa, belgilash buziladi. Eng oddiy yechim — dasturni to'xtatib turish (stop-the-world, "dunyoni to'xtat"). Oshxonada bu — tozalash uchun zalni yopib, mehmonlarni eshik oldida kutdirishga o'xshaydi. Brauzerda bu xavfli: ekran sekundiga 60 marta yangilanadi, ya'ni har yangilanishga taxminan 16 ms bor. Uzun pauza — "qotgan" animatsiya.
V8 ning GC loyihasi Orinoco bu pauzalarni qisqartirish uchun ishni bo'ladi. To'rt usul bor — har birining oshxonadagi o'xshashi bilan:
| Usul | Nima qiladi | Oshxonada |
|---|---|---|
| parallel | bir nechta yordamchi thread GC ishini birga qiladi (dastur shu payt to'xtaydi) | zal yopiq, lekin uch ofitsiant birga tozalaydi — tezroq tugaydi |
| inkremental (incremental) | belgilash mayda bo'laklarga bo'linib, dastur bilan almashib bajariladi (trace'dagi 24 steps — shu) |
ofitsiant mehmonlar orasida bittadan stol tozalaydi |
| konkurent (concurrent) | belgilash fondagi thread'da, dastur to'xtamasdan (V8 blogi, "Concurrent marking in V8", 2018-06-11) | zal ishlayveradi, alohida ishchi bir chetdan tozalab boradi |
| bo'sh vaqtda (idle-time) | brauzer ikki ekran yangilanishi orasida bo'sh qolganda GC ishi qilinadi | mehmon kam paytda tozalash |
Thread — Sinxron va asinxron kod darsidagi "bajarilish ipi". JavaScript kodingiz bitta thread'da ishlaydi, lekin V8 o'z ichki ishlari uchun yordamchi thread'lardan foydalanadi.
Natijada odatdagi saytda GC pauzalarini sezmaysiz. Ular faqat juda ko'p obyekt yaratiladigan issiq kodda muammo bo'ladi.
Tekshirib ko'ring: Nega yosh avlod GC'si tiriklar ozligida tez, ko'pligida esa sekin?
Javob
Scavenge faqat tirik obyektlarni ko'chiradi; o'liklar uchun hech ish qilmaydi — ular egallagan joy shunchaki bo'sh deb e'lon qilinadi. Tiriklar oz bo'lsa — ko'chirish ham oz. Hamma obyekt uzoq yashasa, Scavenge ularni ko'chiraverib vaqt sarflaydi, keyin baribir eski avlodga o'tkazadi.
6. Xotirani o'lchash
6.1 process.memoryUsage()
Node'da joriy xotira holatini process.memoryUsage() beradi. U besh maydonli obyekt qaytaradi, qiymatlar baytda:
const xotira = process.memoryUsage();
console.log(Object.keys(xotira));Konsolda:
[ 'rss', 'heapTotal', 'heapUsed', 'external', 'arrayBuffers' ]| Maydon | Ma'nosi |
|---|---|
rss |
jarayonning operativ xotiradagi umumiy hajmi (Node'ning o'zi ham) |
heapTotal |
V8 heap uchun ajratilgan joy |
heapUsed |
shundan haqiqatan band qismi — eng ko'p kuzatiladigani |
external, arrayBuffers |
V8 tashqarisidagi xotira (masalan, fayl buferlari) |
Baytni megabaytga o'girish: bayt / 2 ** 20 (1 MB = 1 048 576 bayt).
6.2 Tajriba: million buyurtma
// node --expose-gc olchov.mjs
const mb = () =>
Math.round(process.memoryUsage().heapUsed / 2 ** 20);
gc();
console.log("boshida:", mb(), "MB");
let buyurtmalar = Array.from({ length: 1_000_000 }, (_, i) => ({
id: i,
taom: "Osh",
narx: 35000,
}));
console.log("1 mln buyurtma:", mb(), "MB");
buyurtmalar = null;
console.log("null dan keyin:", mb(), "MB");
gc();
console.log("gc() dan keyin:", mb(), "MB");Konsolda (Node 24.21, 5 marta ishga tushirildi — beshalasida ham bir xil; sizda raqamlar boshqacha bo'lishi mumkin):
boshida: 4 MB
1 mln buyurtma: 58 MB
null dan keyin: 58 MB
gc() dan keyin: 4 MBEng muhim qator — uchinchisi. buyurtmalar = null dan keyin xotira kamaymadi. Massiv axlatga aylandi, lekin GC hali kelmagan edi. gc() dan keyingina 58 MB qaytdi.
Yana bir hisob: (58 − 4) MB ÷ 1 000 000 ≈ 56 bayt bitta buyurtmaga. Uch maydonli kichik obyekt va massivdagi havola — har biri bir necha o'n bayt. Million marta ko'paytirilsa — o'nlab megabayt.
6.3 Heap chegarasi
Heap cheksiz emas. Node 24 da standart chegara kompyuter xotirasiga bog'liq. Bizning 16 GB li kompyuterda:
import { getHeapStatistics } from "node:v8";
const chegara = getHeapStatistics().heap_size_limit / 2 ** 20;
console.log(chegara > 0); // trueBu blok faqat chegara borligini tekshiradi: aniq son har kompyuterda boshqa. Bizda Math.round(chegara) — 4288 MB chiqdi. Chegarani --max-old-space-size=<MB> bayrog'i bilan o'zgartirish mumkin. Undan oshsa, Node mana bunday qulaydi (64 MB chegara bilan cheksiz o'sadigan kesh — haqiqiy chiqish, qisqartirilgan):
<--- Last few GCs --->
…
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryJavaScript heap out of memory — "JavaScript heap'ida xotira tugadi". GC tozalashga urindi, lekin hamma narsa tirik edi — o'chiradigan narsa topilmadi. Bu xatoni try/catch bilan ushlab bo'lmaydi: jarayon to'xtaydi. Node serverining xotira sozlamalarini V8 xotira modeli va garbage collection darsida (backend qismida) ko'ramiz.
7. Ko'p uchraydigan xatolar
7.1 "null berdim — xotira bo'shadi"
null faqat havolani uzadi. Obyekt o'chishi — GC qachon kelishiga bog'liq, va boshqa havola qolgan bo'lsa — umuman o'chmaydi. Tuzatish: "havolani uzish" va "xotirani bo'shatish"ni farqlang. O'lchaganda — gc() dan keyin o'lchang.
7.2 delete xotirani bo'shatadi deb o'ylash
delete obyekt.katta xususiyatni olib tashlaydi. Agar katta dagi qiymatga boshqa havola bo'lmasa, u axlatga aylanadi — lekin obyekt.katta = null ham aynan shunday qiladi. delete esa obyektni sekin lug'at rejimiga ham o'tkazadi (Hidden classes). Tuzatish: null.
7.3 Real kodda gc() chaqirish
"Xotira ko'payib ketyapti — har daqiqada gc() chaqiraman". gc() — faqat --expose-gc bilan bor, va u to'liq, uzun pauzali GC'ni majburlaydi. Tuzatish: GC'ni dvigatelga qo'ying. Xotira o'sayotgan bo'lsa — sababi sizish, uni topish kerak (keyingi dars).
7.4 FinalizationRegistry ga mantiq bog'lash
"Obyekt o'chganda faylni yopaman". GC umuman ishlamasligi mumkin (dastur xotira yetarli bo'lib tugasa) — xabar hech qachon kelmaydi. Tuzatish: resursni aniq yoping (yop() metodi, try/finally). FinalizationRegistry — faqat qo'shimcha xavfsizlik to'ri (WeakRef va FinalizationRegistry).
8. Mashqlar
1-mashq (oson): Kim tirik?
Quyidagi kod oxirida qaysi obyektlar axlat? Avval o'ylang, keyin yechimni o'qing.
let a = { nom: "Osh" };
let b = { nom: "Manti", qoshni: a };
let c = { nom: "Choy" };
const royxat = [b];
a = null;
b = null;
c = null;
console.log(royxat[0].qoshni.nom); // OshYechim
Faqat { nom: "Choy" } axlat. royxat — ildiz (global const). U b obyektiga yetib boradi, b esa qoshni orqali a ga. a = null va b = null faqat o'zgaruvchilarni uzdi, obyektlarga boshqa yo'l qoldi. c ga esa yagona yo'l uzildi. Oxirgi qatordagi console.log — isbot: Osh hali yetib boriladigan.
2-mashq (o'rta): GC'ni sanang
«--trace-gc bilan kuzatish» bo'limidagi gc-kuzat.mjs ni ko'chiring. Ikki o'zgarish bilan sinang: (a) i % 10 o'rniga i % 2 — ko'proq chek saqlanadi; (b) hech narsa saqlamang (if ni olib tashlang). Har holatda Scavenge va Mark-Compact sonini sanang. Git Bash'da sanash: node --trace-gc gc-kuzat.mjs | grep -c Scavenge.
Yechim
Bizning o'lchov (Node 24.21, har holat 5 marta):
| Holat | Scavenge | Mark-Compact | Eng uzun pauza |
|---|---|---|---|
| asl (har 10-chisi) | 23 | 1 | ≈ 6 ms |
| (a) har 2-chisi | 12 | 2 | ≈ 60 ms |
| (b) hech biri | 7–11 | 0 | ≈ 1 ms |
(a) da tirik obyektlar ko'p: ular eski avlodga o'tdi, to'liq GC ikki marta ishladi va eng uzun pauza o'n barobar oshdi. (b) da hamma chek yosh o'ldi: faqat qisqa Scavenge'lar, birorta ham Mark-Compact yo'q. Scavenge soni har ishga tushirishda biroz farq qildi — GC vaqti deterministik emas.
Bu avlod gipotezasining jonli ko'rinishi: yosh o'ladigan obyektlar GC uchun deyarli bepul, uzoq yashaydiganlar esa qimmat.
3-mashq (qiyin): Bitta buyurtma necha bayt?
olchov.mjs ni o'zgartiring: buyurtmaga yana ikki maydon qo'shing — stol: 5 va vaqt: "12:30". Bitta buyurtma necha bayt bo'ldi? Keyin taom: "Osh" ni har buyurtmada boshqa satrga almashtiring: taom: `Osh-${i}`. Qancha oshdi va nega?
Yechim
Formula o'sha: (gc() dan oldingi MB − boshidagi MB) × 2 ** 20 ÷ 1 000 000. Bizda (Node 24.21, 3 marta, bir xil natija): uch maydon — taxminan 56 bayt, besh maydon — taxminan 72 bayt. Har yangi maydon — obyekt ichida yana bir o'rin, taxminan 8 bayt.
`Osh-${i}` bilan esa taxminan 104 bayt — 45 % ko'p. Sabab: har buyurtmada yangi satr yaratiladi — million alohida satr, har biri heap'da o'z joyiga ega. "Osh" literalida esa million obyekt bitta satrga havola qilardi. Amaliy xulosa: takrorlanadigan qiymatlarni (holat, turkum nomi) bitta satr sifatida qayta ishlating.
9. Real ishda
- Node serverlari. Monitoring tizimlari (Grafana, Datadog)
heapUsedgrafigini chizadi. Normal server grafigi "arra tishi" kabi: o'sadi, GC — tushadi. Doim faqat o'sib borayotgan grafik — sizish belgisi. - Konteynerlar. Docker'da 512 MB berilgan konteynerda Node standart chegarasi undan katta bo'lishi mumkin. Shuning uchun
--max-old-space-sizeko'pincha qo'lda beriladi. - Brauzer. Chrome DevTools'ning Memory panelidagi heap snapshot — shu darsdagi graf: obyektlar va ularni tirik ushlab turgan havolalar. Keyingi darsda ishlatamiz.
- Intervyu: "JavaScript'da xotira qanday boshqariladi?", "Mark-and-sweep nima?", "Halqali havolalar sizishga olib keladimi?" (javob: yo'q) — klassik savollar.
Xulosa
- Primitiv lokal qiymatlar va havolalar — stack'da, obyektlar — heap'da; heap'ni GC kuzatadi.
- Qoida — yetib boriladiganlik: ildizlardan havolalar orqali yetib bo'lmaydigan qiymat — axlat; halqali havola bunga to'sqinlik qilmaydi.
- Mark-and-sweep: tiriklarni belgila, qolganini supur (V8 da — zichlash bilan, Mark-Compact).
- V8 avlodli: yosh avlod — tez-tez, tez Scavenge; ikki GC'dan omon qolgan — eski avlodga, u yerda Mark-Compact. Orinoco pauzalarni parallel, inkremental va konkurent ish bilan qisqartiradi.
null— havolani uzadi, xotirani darhol bo'shatmaydi; o'lchash —process.memoryUsage().heapUsed,--expose-gcva--trace-gcbilan.
Keyingi dars: Xotira sizishlari va ularni topish — GC'ni "aldaydigan" unutilgan tinglovchi, taymer va keshlarni topamiz va o'lchaymiz.
Manbalar
- V8 blogi: "Trash talk: the Orinoco garbage collector" (2019-01-03), "Orinoco: young generation garbage collection" (2017-11-29), "Concurrent marking in V8" (2018-06-11) — v8.dev/blog
- MDN: "Memory management" — developer.mozilla.org
- Node.js hujjatlari:
process.memoryUsage(),v8.getHeapStatistics()— nodejs.org/api
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!