Memory Leak
Memory leak nima ekani va bu GC xatosi emas, reference boshqaruvi xatosi ekani, eng keng tarqalgan besh leak turi (timer, event listener, closure, global o'zgaruvchi, detached DOM), va WeakMap/WeakRef qanday yechim berishi.
63-dars
Memory Leak
Memory leak (xotira sızması) — dastur endi kerak bo'lmagan obyektni hali ham "root"dan yetib bo'ladigan holatda ushlab turishi, natijada Garbage Collector uni "tirik" deb hisoblab, xotiradan hech qachon tozalamasligi.
Diqqat qiling: bu GC buzilgani degani emas. GC to'g'ri ishlayapti — u faqat reachable (yetib boriladigan) obyektlarni tirik deb hisoblaydi, dedik oldingi mavzuda. Muammo shundaki — dasturchi o'zi bilmagan holda, kerak bo'lmagan obyektga reference (havola)ni unutib qoldirib ketadi, va shu havola orqali obyekt hamon root'ga bog'langan bo'lib qoladi.
let cache = [];
function ishla() {
const kattaObyekt = new Array(1000000).fill("data");
cache.push(kattaObyekt); // har chaqirilganda cache'ga qo'shiladi, hech qachon o'chmaydi
}
setInterval(ishla, 1000);
// Har soniyada 1 million elementli massiv cache'ga qo'shiladi
// cache — global o'zgaruvchi (root'ga bog'liq) — GC hech narsani tozalay olmaydi
// Xotira asta-sekin to'lib boradi → oxir-oqibat sahifa qotib qoladi yoki qulaydi
1. Nega bu muhim?
Memory leak darhol sezilmaydi — dastur ishga tushganda hammasi normal ko'rinadi. Lekin vaqt o'tishi bilan (soatlab, kunlab ishlaydigan SPA, dashboard, real-time ilova):
- Xotira sarfi asta-sekin oshib boradi ("sawtooth" emas, balki tobora ko'tarilib boruvchi grafik — DevTools Memory tab'da buni aniq ko'rish mumkin)
- Sahifa sekinlashadi (GC tobora ko'proq obyektni tekshirishga majbur bo'ladi)
- Oxir-oqibat brauzer tab qulab tushishi mumkin ("Out of memory")
Bu ayniqsa SPA (Single Page Application)larda jiddiy muammo — chunki oddiy ko'p sahifali saytda har sahifa o'tishida butun xotira tozalanadi (page reload), lekin SPA'da foydalanuvchi soatlab bitta sahifada qoladi, dastur hech qachon "reload" bo'lmaydi.
2. Eng keng tarqalgan memory leak turlari
1. Unutilgan timer / interval
class Komponent {
constructor() {
this.data = new Array(1000000).fill("x"); // katta obyekt
this.timer = setInterval(() => {
console.log(this.data.length); // arrow function `this`ni closure orqali ushlab turadi
}, 1000);
}
destroy() {
// clearInterval(this.timer) YOZILMAGAN!
}
}
const k = new Komponent();
k.destroy(); // komponent "o'chirildi" deb o'ylaymiz, lekin...
// setInterval ichidagi callback hamon `this`ni (demak, this.data'ni ham) ushlab turibdi
// Interval hech qachon to'xtamagani uchun — komponent butunlay "garbage" bo'la olmaydi
Bu yerda root — brauzerning ichki timer registrysi (bu ham bir xil root). Callback funksiya closure orqali thisga bog'langan, this esa butun data massivini ushlab turibdi.
2. Unutilgan event listener
function ulash() {
const kattaData = new Array(1000000).fill("x");
document.addEventListener('scroll', function handler() {
console.log(kattaData.length); // closure orqali kattaData'ni ushlab turibdi
});
}
ulash();
// Agar keyinroq removeEventListener chaqirilmasa,
// document (global, hech qachon "o'lmaydigan" root) handler'ni abadiy ushlab turadi
// handler esa kattaData'ni — demak kattaData ham hech qachon tozalanmaydi
Bu — SPA'larda eng ko'p uchraydigan leak turi: komponent DOM'dan o'chiriladi, lekin unga bog'langan event listener document yoki window'da qolib ketaveradi.
3. Unutilgan closure (yopilgan doiralar)
function tashqi() {
const kattaMalumot = new Array(1000000).fill("x");
function ichki() {
console.log("salom"); // kattaMalumot'ni ishlatmaydi
}
return ichki;
}
const funksiya = tashqi();
// V8'da (eski versiyalarda) butun `tashqi` scope'i — kattaMalumot bilan birga —
// closure orqali ushlanib qolishi mumkin edi, garchi `ichki` uni ishlatmasa ham
(Eslatma: zamonaviy V8 buni optimallashtirgan — closure faqat ishlatilgan o'zgaruvchilarni ushlab turadi, hammasini emas. Lekin eski dvigatellarda va ba'zi murakkab holatlarda bu hali ham xavf tug'diradi.)
4. Unutilgan global o'zgaruvchi
function xato() {
kattaObyekt = { data: new Array(1000000) }; // `let`/`const`/`var` yo'q!
// Bu avtomatik `window.kattaObyekt` bo'lib qoladi (strict mode'da xato beradi)
}
xato();
// window — hech qachon "o'lmaydigan" root
// Shuning uchun kattaObyekt UMRBOD xotirada qoladi, dastur tugagunicha
5. Detached DOM nodes (uzilgan, lekin JS'da ushlab turilgan DOM elementlar)
let saqlangan = null;
function elementYarat() {
const div = document.createElement('div');
document.body.appendChild(div);
saqlangan = div; // referensni tashqariga saqlab qo'ydik
}
elementYarat();
document.body.innerHTML = ''; // DOM'dan div FIZIK jihatdan olib tashlandi
console.log(saqlangan); // lekin div hali ham xotirada — chunki `saqlangan` uni ushlab turibdi!
Bu — "detached DOM tree" deyiladi: element vizual jihatdan sahifada yo'q, DOM tree'da ham yo'q, lekin JS o'zgaruvchisi uni hali ham ushlab turgani uchun GC uni tozalay olmaydi. Bu Chrome DevTools Memory profilida alohida, aniq ko'rinadigan kategoriya.
3. V8 darajasida — nega bu "leak" sifatida qoladi
Eslang: V8 GC faqat bitta savol beradi — "Root'dan bu obyektga yetib bo'ladimi?" GC obyektning semantik ma'nosi bilan qiziqmaydi — u faqat grafik bog'lanish bilan ishlaydi.
Root (window/global)
└── document
└── addEventListener orqali saqlangan handler
└── closure
└── kattaData (1 million elementli massiv)
Bu zanjir to'liq va uzluksiz — demak V8 nuqtai nazaridan kattaData 100% reachable, ya'ni 100% "tirik". GC algoritm sifatida to'g'ri ishlayapti — muammo shundaki, dasturchi niyatida bu obyekt allaqachon "kerak emas" edi, lekin kodda bu haqiqat ifodalanmagan (chunki reference hali ham mavjud).
Shuning uchun memory leak — GC xatosi emas, balki reference boshqaruvi xatosi. Yechim har doim bitta narsaga borib taqaladi: kerak bo'lmay qolgan referensni o'zingiz uzib qo'yish (clearInterval, removeEventListener, = null, AbortController.abort()).
4. WeakMap / WeakRef — V8'ning "kuchsiz havola" yechimi
Ba'zan siz obyektga havola saqlashni xohlaysiz, lekin shu havola GC'ga to'sqinlik qilishini xohlamaysiz. Buning uchun V8 (ES2015/ES2021) maxsus tuzilmalar taqdim etadi:
const cache = new WeakMap();
function keshla(obj, natija) {
cache.set(obj, natija); // obj kalit sifatida saqlanadi
}
let element = document.getElementById('btn');
keshla(element, { hisoblangan: true });
element = null;
// Oddiy Map bo'lganda — cache element'ni hali ham ushlab turgan bo'lardi (leak)
// WeakMap bo'lgani uchun — element boshqa hech yerda ishlatilmasa,
// GC uni ODDIY tozalaydi, va cache'dagi yozuv HAM AVTOMATIK yo'qoladi
Nega bu ishlaydi: WeakMap/WeakSetdagi kalit — "weak reference" (kuchsiz havola) hisoblanadi. V8 GC reachability'ni hisoblayotganda, weak reference'larni hisobga olmaydi — ya'ni faqat WeakMap orqali ushlab turilgan obyekt baribir "unreachable" deb hisoblanadi va tozalanadi.
Bu ayniqsa DOM elementlariga bog'liq metama'lumot (masalan, "bu elementga qachon bosilgan" kabi kesh) saqlashda foydali — element DOM'dan o'chirilsa, unga tegishli kesh yozuvi ham avtomatik, qo'lda tozalashsiz yo'qoladi.
5. DevTools'da qanday aniqlanadi (qisqacha, keyingi "Profiling" mavzusida chuqur ko'ramiz)
Hozircha faqat tushuncha darajasida: Chrome DevTools → Memory tab → "Heap snapshot" olib, vaqt oralig'ida ikkita snapshot solishtirsangiz (masalan, komponentni 10 marta ochib-yopib), agar obyektlar soni doim ortib borsa va hech qachon kamaymasa — bu leak signalidir. Bundan tashqari "Detached" deb belgilangan DOM node'lar ro'yxati — aynan yuqorida ko'rgan "detached DOM tree" holatini ko'rsatadi.