Mundarija (33)
- Bu darsda
- 1. Nega bu kerak?
- 2. Heap snapshot nima
- 2.1 Xotiraning surati
- 2.2 Narxi
- 3. Surat olishning uch yo'li
- 3.1 Koddan: v8.writeHeapSnapshot()
- 3.2 Signal bilan: --heapsnapshot-signal (Windows'da yo'q)
- 3.3 Qulashdan oldin: --heapsnapshot-near-heap-limit
- 3.4 DevTools bilan
- 4. Ikki suratni solishtirish
- 4.1 Nima ko'paydi
- 4.2 Nega konstruktor nomi muhim
- 5. Retainer: kim ushlab turibdi
- 5.1 Ildizdan yo'l
- 5.2 Zanjirni o'qiymiz
- 5.3 Tuzatish va isbot
- 6. Node'ga xos sizishlar
- 6.1 Uzoq yashaydigan EventEmitter
- 6.2 Taymerlar va modul darajasidagi holat
- 7. WeakRef va FinalizationRegistry
- 7.1 Zaif havola
- 7.2 Qachon ishlatmaslik kerak
- 8. CLI qadami: holat yo'q — tajriba
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): bo'sh joylarni to'ldiring
- 2-mashq (o'rta): tinglovchi sizishini tuzating
- 3-mashq (qiyin): chegarali va muddatli kesh
- 4-mashq: CLI qadami — interaktiv rejim sizadimi?
- 11. Real ishda
- Xulosa
- Manbalar
Node.js memory leak: heap snapshot bilan xotira sizishini topish
Qisqacha: Node jarayonining xotirasi soatlab o'sib borsa — sizish (memory leak) bor. Uni topish uchun ikki marta heap snapshot olasiz:
v8.writeHeapSnapshot()yoki--heapsnapshot-near-heap-limitbayrog'i bilan. Keyin ikkala suratni solishtirasiz: qaysi obyekt turi ko'paygan bo'lsa — shubhali. Oxirgi qadam — retainer zanjiri: GC ildizidan o'sha obyektgacha qaysi o'zgaruvchi uni ushlab turibdi. Ko'pincha javob bitta nom bo'ladi:kesh,tinglovchilar,navbat.
Bu darsda
- Node jarayonidan heap snapshot olishning uch yo'lini bilasiz va Windows'dagi cheklovini tushunasiz.
- Ikki snapshot'ni o'zingiz yozgan kichik skript bilan solishtirib, "nima ko'paydi" savoliga javob olasiz.
- Retainer zanjirini o'qiysiz:
kesh → Map → table → KursYozuvi. - Node'ga xos sizishlarni (
EventEmittertinglovchisi, modul darajasidagi kesh, taymer) taniysiz va tuzatasiz. WeakRefvaFinalizationRegistryqachon yordam berishi va qachon xavfli ekanini bilasiz.
Oldin bilishingiz kerak: Xotira sizishlari va ularni topish, Node xotira limitlari va heap statistikasi, CPU profiling va flamegraph, EventEmitter chuqur.
1. Nega bu kerak?
Xotira sizishlari darsida besh manbani ko'rgansiz: unutilgan tinglovchi, to'xtatilmagan taymer, closure, detached DOM va chegarasiz kesh. U yerda sizishni Chrome DevTools'ning Memory panelida topgan edingiz.
Endi vaziyat boshqa. Kod brauzerda emas, serverda yoki terminalda ishlaydi. Oyna yo'q, "Memory" tugmasi yo'q. Server esa haftalab to'xtamaydi. Kuniga 2 MB sizish — bir oyda 60 MB, keyin heap limitiga yetib, JavaScript heap out of memory bilan qulash.
Brauzerda foydalanuvchi sahifani yangilab qo'yadi va sizish "o'z-o'zidan" tozalanadi. Serverda bunday yordamchi yo'q. Shuning uchun backend dasturchi sizishni Node vositalari bilan topa olishi kerak.
O'xshatish: omborxona. Har kuni yuk kiradi va chiqadi. Bir kuni omborchi ko'radi: javonlar to'lib boryapti. Qaysi yuk chiqmayapti? Bilish uchun ikki marta ro'yxat qiladi — dushanba va juma. Juma kuni qaysi qutidan ko'p bo'lsa — o'sha. Keyin "bu qutini kim saqlashni buyurgan?" deb hujjatni qidiradi. Heap snapshot — ro'yxat, retainer — hujjat.
2. Heap snapshot nima
2.1 Xotiraning surati
Heap snapshot (xotira surati) — bir lahzadagi butun JS heap'ning to'liq tavsifi: har obyekt, uning turi, hajmi va kimga havola qilishi. Fayl kengaytmasi .heapsnapshot, ichi JSON.
Ichida ikkita katta massiv bor: tugunlar (nodes) — obyektlar, va qirralar (edges) — "A obyekt B ga havola qiladi" degan bog'lanishlar. Qirraning nomi bo'ladi: xususiyat nomi (kesh), massiv indeksi ([3]) yoki ichki nom (table).
flowchart LR
R["GC ildizi"] -->|"kesh"| M["Map"]
M -->|"table"| T["ichki massiv"]
T -->|"[0]"| A["KursYozuvi"]
T -->|"[1]"| B["KursYozuvi"]
A -->|"qiymatlar"| X["Array"]Nimaga qarang: GC ildizidan strelka bo'ylab yetib boriladigan har obyekt — tirik. Garbage collection darsidagi qoida shu: yetib bo'lmasa — o'chiriladi. Sizish — yetib boriladigan, lekin keraksiz obyekt. Snapshot aynan shu strelkalarni ko'rsatadi.
2.2 Narxi
Surat olish bepul emas:
- Jarayon to'xtaydi. Hujjatga ko'ra surat olish — sinxron amal, u event loop'ni bloklaydi. Bu vaqtda JS kodi ishlamaydi — server so'rovlarga javob bermaydi.
- Xotira kerak. Node hujjati ogohlantiradi: surat uchun heap hajmidan taxminan ikki baravar xotira kerak bo'ladi.
- Fayl katta. Biz o'lchaganda 25 MB heap — 27 MB fayl.
Shuning uchun prodakshnda surat ataylab, bir-ikki marta olinadi, har daqiqada emas.
Tekshirib ko'ring: Snapshot'da
A → Bqirrasi bor. Bu "A B ni o'chirishga xalaqit beradi" degani bo'ladimi?
Javob
Faqat A ning o'zi tirik bo'lsa. Agar A ga GC ildizidan yo'l bo'lmasa, A ham, B ham o'chiriladi. Havola faqat ildizdan boshlangan zanjirning bir bo'lagi bo'lsa ushlab turadi. Shuning uchun retainer zanjiri ildizdan boshlab o'qiladi.
3. Surat olishning uch yo'li
3.1 Koddan: v8.writeHeapSnapshot()
Eng oddiy yo'l — node:v8 modulidagi funksiya. U faylni yozadi va nomini qaytaradi. Sizishni qayta yaratamiz: «server» har so'rovda kurs yozuvini keshga qo'shadi va hech qachon o'chirmaydi.
// sizish.mjs — har "so'rov" kurs yozuvini keshga qo'shadi
import v8 from "node:v8";
class KursYozuvi {
constructor(i) {
this.kod = "USD";
this.sana = `2026-10-${i}`;
this.qiymatlar = new Array(20).fill(i);
}
}
const kesh = new Map(); // hech qachon tozalanmaydi
function sorovniBajar(i) {
kesh.set(`USD:${i}`, new KursYozuvi(i));
}
const mb = () =>
(process.memoryUsage().heapUsed / 2 ** 20).toFixed(1);
for (let i = 0; i < 10_000; i++) sorovniBajar(i);
gc();
console.log("1-surat oldidan:", mb(), "MB");
console.log(v8.writeHeapSnapshot("1.heapsnapshot"));
for (let i = 10_000; i < 60_000; i++) sorovniBajar(i);
gc();
console.log("2-surat oldidan:", mb(), "MB");
console.log(v8.writeHeapSnapshot("2.heapsnapshot"));gc() faqat --expose-gc bayrog'i bilan mavjud (Garbage collection darsida ko'rgansiz). Biz node --expose-gc sizish.mjs ni ishga tushirdik (Node 24.21.0, Windows 11, i5-12500H; uch marta — raqamlar 0.1 MB aniqlikda bir xil):
1-surat oldidan: 7.6 MB
1.heapsnapshot
2-surat oldidan: 25.3 MB
2.heapsnapshotFayllar: 8.1 MB va 27.1 MB. Blokni nocheck qildik — xotira raqamlari Node versiyasi va mashinaga qarab biroz farq qiladi.
Nega surat oldidan gc()? Aks holda suratda "axlat" ham bo'ladi — o'chirilishi kerak, lekin hali o'chirilmagan obyektlar. Aslida writeHeapSnapshot o'zi ham surat oldidan to'liq GC qiladi, lekin o'lchov (mb()) uchun gc() kerak.
Grafikda kanondagi xuddi shunday tajriba (Map kesh, 10 000 dan 60 000 gacha yozuv):
| keshdagi yozuvlar | cheksiz Map |
|---|---|
| 0 | 4,4 |
| 10 000 | 7,3 |
| 60 000 | 21,6 |
Manba: IlmHamroh kanon tajribasi t48, 2026-10-23: Node 24.21.0, Windows 11, i5-12500H, --expose-gc
Nimaga qarang: chiziq to'g'ri. Har 10 000 yozuv ~2.8 MB. To'g'ri chiziq — sizishning tipik "imzosi".
3.2 Signal bilan: --heapsnapshot-signal (Windows'da yo'q)
Ishlab turgan serverni qayta ishga tushirmasdan surat olish kerak bo'lsa, Node hujjati bayroq taklif qiladi:
node --heapsnapshot-signal=SIGUSR2 server.mjs
# boshqa terminalda: kill -USR2 <pid>Linux va macOS'da SIGUSR2 signali kelganda Node surat yozadi (hujjat bo'yicha; biz Linux'da sinamadik). process va signallar darsidan eslang: Windows'da signallar boshqacha. Biz Windows'da sinab ko'rdik — dastur umuman ishga tushmadi:
TypeError [ERR_UNKNOWN_SIGNAL]: Unknown signal: SIGUSR2Tarjimasi: "noma'lum signal: SIGUSR2". Windows'da SIGUSR2 yo'q, Node esa noto'g'ri bayroqni jim o'tkazib yubormaydi — ishga tushishdan oldin xato beradi. Bu yaxshi: server bayroqsiz ishlab, kerakli paytda surat ololmay qolmaydi.
3.3 Qulashdan oldin: --heapsnapshot-near-heap-limit
Eng foydali yo'l — sizish o'zini ko'rsatgan paytda avtomatik surat. Bayroq: --heapsnapshot-near-heap-limit=N — heap limitga yaqinlashganda N tagacha surat yoziladi. Biz cheksiz massivli dasturni kichik limit bilan ishga tushirdik:
node --max-old-space-size=64 \
--heapsnapshot-near-heap-limit=1 limit.mjsHaqiqiy chiqish (qisqartirilgan; fayl yo'li … bilan):
Wrote snapshot to …\Heap.20261023.140944.23348.0.001.heapsnapshot
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memorySurat yozildi, keyin baribir qulash. Lekin endi qulash sababi qo'lingizda — 152 MB fayl. Kutilmagan ikki narsa ko'rdik:
- Vaqt. Qulash ~31 soniyada bo'ldi. Bayroqsiz shunday dastur bir soniyaga yetmay yiqiladi. Surat olish uchun V8 limitni vaqtincha ko'taradi (oxirgi GC qatorida heap 248 MB — 64 emas). Hujjat ham shuni aytadi: Node surat narxini ko'tarish uchun heap'ni moslaydi.
- Joy. Fayl 152 MB. Serverda disk to'lib qolmasin —
--diagnostic-dir=papkabilan alohida joyga yo'naltiring.
Diqqat: Heap snapshot ichida xotiradagi hamma satrlar bor: parollar, tokenlar, foydalanuvchilarning ismi va telefoni,
.envdan o'qilgan kalitlar. Suratni chatga yoki ochiq issue'ga yuklamang. Uni sir kabi saqlang va ish tugagach o'chiring.
3.4 DevTools bilan
To'rtinchi yo'l — node --inspect bilan ishga tushirib, Chrome'da chrome://inspect sahifasi orqali ulanish. Keyin Memory panelida xuddi brauzerdagidek surat olinadi. Node'ni debug qilish darsida ulanish qismini ko'rgansiz. Faylga yozilgan .heapsnapshot ni ham Memory panelidagi Load tugmasi bilan ochsa bo'ladi.
Bu yo'lni biz bu safar sinamadik — zavod muhitida brauzer ochilmaydi. Panelning ko'rinishi Xotira sizishlari darsidagi rasmdagidek.
Tekshirib ko'ring: Server prodakshnda haftasiga bir marta qulaydi. Qaysi yo'l bilan surat olasiz va nega?
Javob
--heapsnapshot-near-heap-limit=1 (va --diagnostic-dir). Qulash qachon bo'lishini bilmaysiz — bayroq aynan kerakli paytni o'zi tutadi. writeHeapSnapshot ni qachon chaqirishni bilish kerak, signal esa Windows'da yo'q va kimdir hushyor turishi kerak. Faqat diskda joy borligini va surat olish vaqtida server javob bermasligini hisobga oling.
4. Ikki suratni solishtirish
4.1 Nima ko'paydi
Bitta surat ko'p narsa demaydi — har dasturda minglab obyekt bor. Usul — ikki surat: biri amaldan oldin, biri ko'p takrordan keyin. Ikkinchisida ko'paygan obyekt turi — gumondor.
DevTools'da bu "Comparison" ko'rinishi. Lekin surat oddiy JSON — uni Node'ning o'zi bilan ham o'qisa bo'ladi. Kichik skript yozamiz: har suratda turi object bo'lgan tugunlarni konstruktor nomi bo'yicha sanaydi va farqni chiqaradi.
// farq.mjs — ikki heap snapshot: qaysi obyekt turi ko'paydi
// node farq.mjs 1.heapsnapshot 2.heapsnapshot
import { readFileSync } from "node:fs";
function turlarniSana(fayl) {
const s = JSON.parse(readFileSync(fayl, "utf8"));
const maydon = s.snapshot.meta.node_fields;
const qadam = maydon.length;
const turI = maydon.indexOf("type");
const nomI = maydon.indexOf("name");
const turlar = s.snapshot.meta.node_types[0];
const soni = new Map();
for (let i = 0; i < s.nodes.length; i += qadam) {
if (turlar[s.nodes[i + turI]] !== "object") continue;
const nom = s.strings[s.nodes[i + nomI]];
soni.set(nom, (soni.get(nom) ?? 0) + 1);
}
return soni;
}
const [a, b] = process.argv.slice(2).map(turlarniSana);
const farq = [...b]
.map(([nom, n]) => [nom, n - (a.get(nom) ?? 0)])
.filter(([, n]) => n > 0)
.sort((x, y) => y[1] - x[1])
.slice(0, 5);
for (const [nom, n] of farq) {
console.log(`${nom.padEnd(12)} +${n}`);
}Tuzilmani tushunish uchun bitta narsa kifoya: nodes — bitta uzun sonlar massivi. Har tugun ketma-ket bir nechta sondan iborat (type, name, id, self_size …). Ularning tartibi snapshot.meta.node_fields da yozilgan. Nomlar esa alohida strings massivida, tugunda faqat nom raqami turadi. Bu — faylni ixcham qilish uchun.
sizish.mjs dan keyin biz ishga tushirgan natija:
Array +50000
KursYozuvi +50000Ikkinchi surat oldidan 50 000 so'rov bo'lgan edi. Har so'rov — bitta KursYozuvi va uning ichidagi bitta qiymatlar massivi. Raqamlar so'rovlar soniga aynan teng. Bu — eng aniq belgi: "har so'rovda bittadan qoladi".
Kanonda xuddi shu usul (t48-heap.txt) bir xil javob bergan: KursYozuvi +50 000, Array +50 000. Kanon suratlari 7.5 va 19.3 MB edi — obyekt ichidagi maydonlar boshqacha bo'lgani uchun.
4.2 Nega konstruktor nomi muhim
KursYozuvi nomini suratda ko'rdik, chunki obyekt klass orqali yaratilgan. Agar { kod, sana, qiymatlar } oddiy obyekt literali bo'lganida, suratda nomi Object bo'lardi — minglab boshqa Object lar orasida yo'qolib ketardi.
Qoida: uzoq yashaydigan va ko'p yaratiladigan ma'lumot uchun nomli klass — sizish qidirishni ancha osonlashtiradi. Bu tezlikka ham zarar bermaydi (Hidden classes darsidagi bir xil shakl qoidasi).
Tekshirib ko'ring: Farqda
Array +50000vaKursYozuvi +50000chiqdi. Ikkalasidan qaysi biri asl sabab?
Javob
KursYozuvi. Massivlar uning ichida (this.qiymatlar) — yozuv o'chsa, massiv ham o'chadi. Farq ro'yxatida bir xil sonli qatorlar ko'pincha "ota va bola" bo'ladi. Sababni topish uchun eng "yuqoridagi" — o'z nomi bilan tanilgan obyektdan boshlang va uning retainer'ini qidiring.
5. Retainer: kim ushlab turibdi
5.1 Ildizdan yo'l
Retainer ("ushlab turuvchi") — obyektga havola qilib, uni tirik saqlayotgan obyekt. Bizga bitta retainer emas, butun zanjir kerak: GC ildizidan gumondor obyektgacha.
Zanjirni topish — graf bo'ylab kenglik bo'yicha qidiruv (Graflarda BFS va DFS): ildizdan boshlaymiz, har tugunning qirralarini ochamiz va kim qayerdan kelganini yozib boramiz. KursYozuvi ga yetganda, orqaga yurib yo'lni tiklaymiz. Zaif qirralarni (weak) tashlab ketamiz — ular obyektni ushlab turmaydi.
To'liq skript: ushlovchi.mjs (55 qator)
// ushlovchi.mjs — GC ildizidan obyektgacha eng qisqa yo'l
// node ushlovchi.mjs 2.heapsnapshot KursYozuvi
import { readFileSync } from "node:fs";
const [fayl, qidiruv] = process.argv.slice(2);
const s = JSON.parse(readFileSync(fayl, "utf8"));
const { node_fields: nf, edge_fields: ef } = s.snapshot.meta;
const NF = nf.length;
const EF = ef.length;
const tugunTur = s.snapshot.meta.node_types[0];
const qirraTur = s.snapshot.meta.edge_types[0];
const tur = (t) => tugunTur[s.nodes[t * NF + nf.indexOf("type")]];
const nom = (t) => s.strings[s.nodes[t * NF + nf.indexOf("name")]];
// har tugun qirralari edges massivining qaysi joyidan boshlanadi
const soni = s.nodes.length / NF;
const boshi = new Uint32Array(soni + 1);
for (let t = 0; t < soni; t++) {
const n = s.nodes[t * NF + nf.indexOf("edge_count")];
boshi[t + 1] = boshi[t] + n * EF;
}
function qirraNomi(e) {
const t = qirraTur[s.edges[e + ef.indexOf("type")]];
const v = s.edges[e + ef.indexOf("name_or_index")];
return t === "element" || t === "hidden" ? `[${v}]` : s.strings[v];
}
// 0-tugundan (ildiz) kenglik bo'yicha qidiruv; zaif qirralar — yo'q
const ota = new Int32Array(soni).fill(-1);
const otaQirra = new Int32Array(soni);
const navbat = [0];
ota[0] = 0;
let topildi = -1;
for (let i = 0; i < navbat.length && topildi < 0; i++) {
const t = navbat[i];
for (let e = boshi[t]; e < boshi[t + 1]; e += EF) {
const qt = qirraTur[s.edges[e + ef.indexOf("type")]];
if (qt === "weak") continue;
const u = s.edges[e + ef.indexOf("to_node")] / NF;
if (ota[u] !== -1) continue;
ota[u] = t;
otaQirra[u] = e;
navbat.push(u);
if (tur(u) === "object" && nom(u) === qidiruv) topildi = u;
}
}
const yol = [];
for (let t = topildi; t > 0; t = ota[t]) {
const nomi = nom(t) || `(${tur(t)})`;
yol.unshift(` —${qirraNomi(otaQirra[t])}→ ${nomi}`);
}
console.log(yol.join("\n"));Qirralar ham tugunlar kabi bitta uzun massivda. Har tugunning qirralari ketma-ket turadi, nechta ekani tugunning edge_count maydonida. Shuning uchun avval boshi massivini hisoblaymiz — "N-tugunning qirralari qayerdan boshlanadi".
Biz node ushlovchi.mjs 2.heapsnapshot KursYozuvi ni ishga tushirdik (27 MB fayl, 0.4 s):
—[1]→ (GC roots)
—[19]→ (Stack roots)
—44→ system / Context
—kesh→ Map
—table→ (array)
—212769→ KursYozuvi5.2 Zanjirni o'qiymiz
Yuqoridan pastga:
| Qadam | Ma'nosi |
|---|---|
(GC roots), (Stack roots) |
V8 ning ildizlari — bular doim tirik |
system / Context |
Modul darajasidagi o'zgaruvchilar "qutisi" |
—kesh→ Map |
O'sha qutidagi kesh o'zgaruvchisi — Map |
—table→ (array) |
Map ning ichki jadvali |
—212769→ KursYozuvi |
Jadvaldagi bitta yozuv |
Eng muhim qator — —kesh→. Bu sizning koddagi o'zgaruvchi nomi. Sizish manbai topildi: modul darajasidagi const kesh = new Map(). Modul hech qachon "tugamaydi" — uning o'zgaruvchilari jarayon yashaguncha tirik.
Ichki nomlar (44, 212769) har ishga tushirishda boshqacha bo'ladi — ular V8 ichidagi joy raqami. Ularga e'tibor bermang, nomli qirralarni qidiring.
flowchart TD
A["Xotira o'sadi"] --> B["Surat 1"]
B --> C["Amalni N marta"]
C --> D["Surat 2"]
D --> E["Farq: +N ta KursYozuvi"]
E --> F["Retainer: kesh → Map"]
F --> G["Tuzatish va qayta o'lchov"]5.3 Tuzatish va isbot
Tuzatish — keshni chegaralash. Memoization darsidagi oddiy LRU: chegaradan oshsa, eng eski yozuv chiqadi (Map qo'shilish tartibini saqlaydi):
const kesh = new Map(); // 1000 yozuvdan oshmaydi
function sorovniBajar(i) {
kesh.set(`USD:${i}`, new KursYozuvi(i));
if (kesh.size > 1000) {
kesh.delete(kesh.keys().next().value); // eng eskisi
}
}Qolgan kod o'zgarmadi. Biz qayta ishga tushirdik:
1-surat oldidan: 4.5 MB
1.heapsnapshot
2-surat oldidan: 4.5 MB
2.heapsnapshotfarq.mjs esa hech narsa chiqarmadi — ko'paygan obyekt turi yo'q. Mana isbot: tuzatishdan keyin xuddi shu usul bilan qayta o'lchash. "Tuzatdim" deyish uchun o'lchov kerak, taxmin emas.
Tekshirib ko'ring: Retainer zanjirida
—kesh→o'rniga—listeners→chiqdi. Qayerdan qidirasiz?
Javob
Tinglovchilardan. Ehtimol, qaysidir EventEmitter ga har safar .on(...) qo'shilyapti va hech qachon .off(...) qilinmayapti. Zanjirda listeners dan bir qadam yuqoridagi obyekt — o'sha emitter. Keyingi bo'limda aynan shunday holat.
6. Node'ga xos sizishlar
6.1 Uzoq yashaydigan EventEmitter
Brauzerdagi EventTarget misolini Xotira sizishlari darsida ko'rgansiz. Node'da ko'proq EventEmitter (EventEmitter chuqur) uchraydi. Tipik xato: butun dastur uchun bitta emitter, va har so'rov unga tinglovchi qo'shadi.
import { EventEmitter } from "node:events";
const kurslar = new EventEmitter(); // butun dastur uchun bitta
process.on("warning", (w) => {
console.log(`${w.name}: ${w.message.slice(0, 60)}…`);
});
function sorovniBajar(id) {
// har so'rov tinglovchi qo'shadi, lekin olib tashlamaydi
kurslar.on("yangilandi", () => console.log(`so'rov ${id}`));
}
for (let i = 1; i <= 11; i++) sorovniBajar(i);
console.log("tinglovchilar:", kurslar.listenerCount("yangilandi"));Konsolda:
tinglovchilar: 11
MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 yangilandi li…process.on("warning") — ogohlantirishni o'zimiz ushlab, qisqa chiqardik. Ogohlantirish keyin keladi: Node uni asinxron yuboradi. To'liq matni:
(node:20780) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 yangilandi listeners added to [EventEmitter]. MaxListeners is 10. Use emitter.setMaxListeners() to increase limitTarjimasi: "EventEmitter'da ehtimoliy xotira sizishi aniqlandi: yangilandi hodisasiga 11 tinglovchi qo'shildi, chegara — 10". Har tinglovchi closure orqali o'z id sini ushlaydi. Server bir kunda millionta so'rov oladi — million closure.
Tuzatish — so'rov tugaganda tinglovchini olib tashlash. Eng qulay usuli — AbortSignal bilan (Node EventEmitter ham buni biladi — events.on, once ning signal opsiyasi) yoki aniq kurslar.off("yangilandi", f). setMaxListeners(100) — haroratni tushirish, kasallikni emas.
6.2 Taymerlar va modul darajasidagi holat
Ikki manba ko'p uchraydi:
setIntervalto'xtatilmagan. Interval closure'i ushlagan hamma narsa tirik qoladi. timers darsidagi qoida: harsetIntervalgaclearIntervalyokiAbortSignal. Fon taymeri jarayonni ushlab turmasligi uchun —.unref().- Modul darajasidagi to'plam.
const navbat = [],const sessiyalar = {},const kesh = new Map()— faylning eng yuqorisida. Biz yuqorida ko'rdik: ular jarayon umri bo'yi yashaydi. Har biriga savol: "bu qachon kichrayadi?" Javob yo'q bo'lsa — chegara yoki muddat (TTL) qo'ying.
Maslahat: Sizishga shubha bo'lsa, avval
process.memoryUsage().heapUsedni vaqti-vaqti bilan logga yozing (Node xotira limitlari). Arra tishi (o'sadi, GC'dan keyin tushadi) — normal. Har GC'dan keyingi "eng past nuqta" ham ko'tarilib borsa — sizish.
7. WeakRef va FinalizationRegistry
7.1 Zaif havola
WeakMap, WeakRef darsida tanishgansiz: zaif havola obyektni GC'dan to'smaydi. Node'da buni aniq ko'rish mumkin — gc() ni o'zimiz chaqiramiz:
// node --expose-gc
const royxat = new FinalizationRegistry((kalit) => {
console.log(`GC o'chirdi: ${kalit}`);
});
let yozuv = { kod: "USD", kurs: 11903.15 };
const havola = new WeakRef(yozuv);
royxat.register(yozuv, "USD yozuvi");
console.log("deref:", havola.deref()?.kurs);
yozuv = null; // kuchli havola yo'q
await new Promise((r) => setTimeout(r, 0));
gc();
await new Promise((r) => setTimeout(r, 0));
console.log("deref:", havola.deref());Konsolda:
deref: 11903.15
GC o'chirdi: USD yozuvi
deref: undefinedBirinchi qatorda obyekt tirik. yozuv = null dan keyin unga faqat zaif havola qoldi. gc() uni o'chirdi, FinalizationRegistry chaqiruvi keldi, deref() endi undefined qaytardi.
Nega gc() dan oldin ham setTimeout? V8 deref() qilingan obyektni joriy vazifa tugaguncha tirik saqlaydi — bu spetsifikatsiya qoidasi. Kutishsiz gc() uni o'chirmasligi mumkin.
7.2 Qachon ishlatmaslik kerak
Bu blokni biz besh marta ishga tushirdik — natija bir xil. Lekin bu faqat --expose-gc va qo'lda gc() tufayli. Oddiy dasturda GC qachon ishlashini hech kim aytolmaydi: bir daqiqada ham, hech qachon ham bo'lishi mumkin. Shuning uchun:
FinalizationRegistryda muhim ish qilmang (faylni yopish, pul hisobini yozish). U chaqirilmasligi mumkin — hujjat buni ochiq aytadi.- Zaif havola sizishni "tuzatmaydi". Chegarasiz kesh
WeakRefbilan ham chegarasiz qoladi — faqat GC'ga ko'proq ish beradi. To'g'ri yo'l — aniq chegara (LRU) va muddat. WeakMap— obyektga qo'shimcha ma'lumot biriktirish uchun yaxshi: kalit-obyekt o'lsa, yozuv ham ketadi.
Tekshirib ko'ring:
const kesh = new Map()o'rniganew WeakMap()qo'ysak,sizish.mjsdagi muammo yo'qoladimi?
Javob
Kod umuman ishlamaydi: WeakMap kaliti faqat obyekt bo'la oladi, bizda esa "USD:1" satri. TypeError: Invalid value used as weak map key chiqadi. Kalit obyekt bo'lganida ham savol qoladi: kalitni kim ushlaydi? Agar kalitlar boshqa joyda tirik bo'lsa, yozuvlar ham qoladi. Satr kalitli kesh uchun to'g'ri yechim — chegara.
8. CLI qadami: holat yo'q — tajriba
valyuta da kod o'zgarmaydi. Sabab o'lchovda: CLI qisqa yashaydi. valyuta kurs USD ~100 ms ishlaydi va tugaydi — jarayon bilan birga butun xotira operatsion tizimga qaytadi. Kesh faylda, xotirada emas. Interaktiv rejim (valyuta i) kurslarni bir marta oladi va har qatorda yangi narsa saqlamaydi.
Kanon tajribasi (t48-heap.txt) aynan shu darsdagi usulni sinadi: cheksiz Map kesh, ikki surat (7.5 va 19.3 MB), farq — KursYozuvi +50 000, Array +50 000. Bu usul HTTP server yozganingizda kerak bo'ladi: u jarayon haftalab yashaydi. Commit yo'q.
9. Ko'p uchraydigan xatolar
| Belgi yoki xato | Sabab | Davosi |
|---|---|---|
"Suratda 100 000 ta Object — sizish!" |
Bitta surat o'sishni ko'rsatmaydi | Har doim ikki surat va farq |
ERR_UNKNOWN_SIGNAL: SIGUSR2 |
Windows'da SIGUSR2 yo'q («Signal bilan» bo'limi) |
writeHeapSnapshot ni o'zingiz chaqiring yoki --heapsnapshot-near-heap-limit |
| Server diski to'lib qoldi | --heapsnapshot-near-heap-limit=3 — uchta yuzlab MB lik fayl |
N=1 va alohida papka (--diagnostic-dir) |
| Suratdagi token chatga tushdi | Suratda xotiradagi hamma satrlar bor | Ulashmang; test ma'lumoti bilan qayta yarating |
gc is not defined |
Bayroqsiz gc yo'q |
gc() — faqat o'lchov uchun, prodakshn kodiga qo'shmang |
Oxirgi qator haqida: bayroq bilan ham gc() ni prodakshnda chaqirmang. Majburiy GC dasturni to'xtatadi va V8 ning o'z rejasini buzadi.
10. Mashqlar
Mashqlar kurs/mashqlar/24/48-memory-leak/ da.
1-mashq (oson): bo'sh joylarni to'ldiring
- Koddan surat olish:
v8.() - Limitga yaqinlashganda avtomatik surat:
--heapsnapshot-near-=1 - Obyektni tirik ushlab turgan obyekt —
- Windows'da
--heapsnapshot-signal=SIGUSR2beradigan xato kodi —ERR_UNKNOWN_ - Tinglovchi chegarasi sukut bo'yicha —
Yechim
- writeHeapSnapshot —
node:v8modulida, fayl nomini qaytaradi. - heap-limit — qulashdan oldin surat yoziladi.
- retainer — "ushlab turuvchi"; zanjiri GC ildizidan boshlanadi.
- SIGNAL — Windows'da
SIGUSR2yo'q. - 10 —
MaxListenersExceededWarning11-tinglovchida chiqadi.
2-mashq (o'rta): tinglovchi sizishini tuzating
«Uzoq yashaydigan EventEmitter» bo'limidagi kodni o'zgartiring: har so'rov tinglovchini qo'shsin, ishini qilsin va so'rov tugaganda olib tashlasin. Buning uchun sorovniBajar(id) ichida tinglovchini o'zgaruvchiga saqlab kurslar.on bilan qo'shing, kurslar.emit("yangilandi") qiling va oxirida kurslar.off bilan olib tashlang. 11 so'rovdan keyin tinglovchilar soni 0 bo'lsin va ogohlantirish chiqmasin. Yordam: off(hodisa, funksiya) — aynan o'sha funksiyani oladi (EventEmitter asoslari).
Yechim
import { EventEmitter } from "node:events";
const kurslar = new EventEmitter();
process.on("warning", (w) => console.log("ogohlantirish:", w.name));
function sorovniBajar(id) {
const tinglovchi = () => console.log(`so'rov ${id}`);
kurslar.on("yangilandi", tinglovchi);
// ... so'rov ishi ...
kurslar.emit("yangilandi");
kurslar.off("yangilandi", tinglovchi); // so'rov tugadi
}
for (let i = 1; i <= 11; i++) sorovniBajar(i);
console.log("tinglovchilar:", kurslar.listenerCount("yangilandi"));Konsolda:
so'rov 1
so'rov 2
so'rov 3
so'rov 4
so'rov 5
so'rov 6
so'rov 7
so'rov 8
so'rov 9
so'rov 10
so'rov 11
tinglovchilar: 0tinglovchi o'zgaruvchiga saqlangani uchun off uni topa oladi. Anonim funksiyani ikki marta yozsangiz (on(..., () => …) va off(..., () => …)) — ikki har xil funksiya, olib tashlanmaydi. Ogohlantirish chiqmadi: bir vaqtda ko'pi bilan bitta tinglovchi bor.
3-mashq (qiyin): chegarali va muddatli kesh
ChegaraliKesh klassini yozing: new ChegaraliKesh({ chegara, muddatMs, hozir }). hozir — vaqtni qaytaradigan funksiya (sinash uchun soxta soat). Metodlar: set(kalit, qiymat) — chegaradan oshsa eng eskisini o'chiradi; get(kalit) — muddati o'tgan bo'lsa yozuvni o'chirib undefined qaytaradi; size. Sinov: chegara 2, muddat 1000 ms. USD, EUR, RUB qo'shing va hajm 2 ekanini, USD yo'qligini ko'rsating. Keyin soatni 1500 ms oldinga suring — EUR ham yo'q bo'lsin. Yordam: Map qo'shilish tartibini saqlaydi, birinchi kalit — keys().next().value.
Yechim
class ChegaraliKesh {
#yozuvlar = new Map();
constructor({ chegara, muddatMs, hozir = Date.now }) {
this.chegara = chegara;
this.muddatMs = muddatMs;
this.hozir = hozir;
}
set(kalit, qiymat) {
this.#yozuvlar.delete(kalit); // yangilansa — oxiriga
this.#yozuvlar.set(kalit, { qiymat, vaqt: this.hozir() });
if (this.#yozuvlar.size > this.chegara) {
const eski = this.#yozuvlar.keys().next().value;
this.#yozuvlar.delete(eski);
}
}
get(kalit) {
const y = this.#yozuvlar.get(kalit);
if (!y) return undefined;
if (this.hozir() - y.vaqt > this.muddatMs) {
this.#yozuvlar.delete(kalit);
return undefined;
}
return y.qiymat;
}
get size() {
return this.#yozuvlar.size;
}
}
let soat = 0;
const kesh = new ChegaraliKesh({
chegara: 2,
muddatMs: 1000,
hozir: () => soat,
});
kesh.set("USD", 11903.15);
kesh.set("EUR", 13329.15);
kesh.set("RUB", 146.75);
console.log(kesh.size, kesh.get("USD"), kesh.get("EUR"));
soat += 1500;
console.log(kesh.get("EUR"), kesh.size);Konsolda:
2 undefined 13329.15
undefined 1Ikki himoya birga ishlaydi: chegara xotirani yuqoridan cheklaydi, muddat eski ma'lumotni olib tashlaydi. Diqqat: muddati o'tgan yozuv faqat get chaqirilganda o'chadi — hech kim so'ramasa, chegara baribir uni oxiri siqib chiqaradi. Soatni parametr qilib berish (Legacy kod darsidagi "tikuv" g'oyasi) testni 1.5 soniya kutishdan qutqardi.
4-mashq: CLI qadami — interaktiv rejim sizadimi?
valyuta loyihangizda o'lchang: 20 000 qatorli fayl yarating (100 usd eur qatori takrorlansin) va uni valyuta i ga quvur bilan bering. NODE_OPTIONS=--expose-gc bilan ishga tushirish shart emas — interaktiv.ts oxirida process.memoryUsage().heapUsed ni stderr'ga yozing (vaqtincha). 2 000 va 20 000 qatorda solishtiring.
Yechim
Kutilgan xulosa: heapUsed qatorlar soniga proporsional o'smaydi — interaktiv rejim har qatorni hisoblab chiqaradi va hech narsa saqlamaydi (readline historySize: 100 faqat TTY'da ishlaydi). Agar 20 000 da 2 000 dagidan ancha ko'p bo'lsa — GC hali ishlamagan bo'lishi mumkin: o'lchovni bir necha marta takrorlang yoki --expose-gc bilan gc() dan keyin o'lchang. Vaqtinchalik qatorni commit qilmang — bu o'lchov, loyiha kodi emas.
11. Real ishda
- Prodakshn insidenti: "server har 3 kunda qayta ishga tushyapti" — eng ko'p uchraydigan sabab sizish. Jamoalar
--heapsnapshot-near-heap-limitni oldindan yoqib qo'yadi: qulash bo'lsa, surat tayyor. - Konteyner va xotira limiti: Docker yoki Kubernetes'da jarayonga masalan 512 MB ajratiladi. Xotira limitlari darsidagi
--max-old-space-sizebilan birga sozlanadi — keyingi qismlarda (Docker) ko'rasiz. - APM vositalari: Datadog, New Relic, Sentry kabi xizmatlar heap'ni vaqt bo'yicha grafik qilib ko'rsatadi — "eng past nuqta" ko'tarilayotganini ular birinchi sezadi. Ulanish uchun hisob kerak, biz ishlatmadik.
- Intervyu: "Node'da memory leak'ni qanday topasiz?" — yaxshi javob: o'lchash (heapUsed trendi), ikki snapshot, farq, retainer zanjiri, tuzatish va qayta o'lchash. Misollar: global kesh, unutilgan listener, interval.
Xulosa
- Sizish — yetib boriladigan, lekin keraksiz obyekt; serverda u haftalar ichida qulashga olib keladi.
- Surat olish:
v8.writeHeapSnapshot(),--heapsnapshot-near-heap-limit=1,--inspect+ DevTools.--heapsnapshot-signalWindows'daERR_UNKNOWN_SIGNALberadi. - Surat — jarayonni to'xtatadi, katta fayl va ichida sirlar; ehtiyot bilan.
- Usul: ikki surat → ko'paygan tur (+50 000
KursYozuvi) → retainer zanjiri (kesh → Map) → tuzatish → qayta o'lchov. - Node'da tipik manbalar: modul darajasidagi kesh, uzoq yashaydigan
EventEmittertinglovchilari, to'xtatilmagan interval. WeakRef/FinalizationRegistry— GC'ga bog'liq va noaniq; sizishga davo emas, chegara va muddat — davo.
Keyingi dars: Diagnostika vositalari — debugger'siz tahlil: process.report, ogohlantirish va eskirish izlari, diagnostics_channel va perf_hooks.
Manbalar
- Node.js hujjati — Using heap snapshot: https://nodejs.org/en/learn/diagnostics/memory/using-heap-snapshot
- Node.js hujjati —
v8.writeHeapSnapshot(): https://nodejs.org/api/v8.html#v8writeheapsnapshotfilenameoptions - Node.js hujjati — CLI:
--heapsnapshot-near-heap-limit,--heapsnapshot-signal: https://nodejs.org/api/cli.html - MDN — WeakRef va FinalizationRegistry: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!