IlmHamroh
JavaScript Full-stack/12-qism. JavaScript: ilg'or mavzular va kod sifati30/44-dars18 daqiqa
Mundarija (29)

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 null berish 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-gc va --trace-gc bilan 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

js
function buyurtmaQil() {
  const soni = 2;
  const taom = { nom: "Osh", narx: 35000 };
  return taom;
}

const tushlik = buyurtmaQil();
console.log(tushlik.nom); // Osh

buyurtmaQil 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 --> O

Soddalashtirilgan 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 --> C

menyu 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":

js
// 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):

text
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.mjs da stol = null qatorini o'chirsak (faqat mehmon = null qolsa), 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:

  1. Belgilash (mark). Ildizlardan boshlab, havolalar bo'ylab yuriladi. Har topilgan obyektga "tirik" belgisi qo'yiladi. Belgilangan obyektga qayta kirilmaydi — shuning uchun halqada aylanib qolmaydi.
  2. 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:

js
// 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):

text
      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: 300000

Bitta 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:

js
const xotira = process.memoryUsage();
console.log(Object.keys(xotira));

Konsolda:

text
[ '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

js
// 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):

text
boshida: 4 MB
1 mln buyurtma: 58 MB
null dan keyin: 58 MB
gc() dan keyin: 4 MB

Eng 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:

js
import { getHeapStatistics } from "node:v8";

const chegara = getHeapStatistics().heap_size_limit / 2 ** 20;
console.log(chegara > 0); // true

Bu 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):

text
<--- Last few GCs --->
…

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

JavaScript 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.

js
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); // Osh
Yechim

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) heapUsed grafigini 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-size ko'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-gc va --trace-gc bilan.

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
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Xotira boshqaruvi va garbage collection: stack, heap va axlat yig'uvchi — IlmHamroh