Registrering og løsning af almindelige leak-mønstre
Find closures med leaks, caches uden grænser og vedvarende listeners, der gradvist øger heapens størrelse.
Registrering og løsning af almindelige leak-mønstre er en gratis Bootcamp i backendudvikling med Node.js-lektion på CoddyKit. Dette er lektion 4 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Bootcamp i backendudvikling med Node.js, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Bootcamp i backendudvikling med Node.js-kurset indeholder 4 lektioner i alt.
Sådan ser et ægte memory leak ud
Et memory leak i Node.js er ikke, når hukommelsesforbruget stiger – det er, når det stiger og aldrig falder igen mellem GC-cyklusser. V8-heapen vokser, old-space forbliver fastholdt, og til sidst rammer du grænsen for --max-old-space-size, hvorefter processen afsluttes af OOM.
- Normalt: Heap-forbruget går op og ned, når GC frigiver kortlivede objekter.
- Memory leak: Savtaks-baseline bevæger sig opad over flere timer, selv ved konstant trafik.
I denne lektion leder vi efter de tre klassiske syndere i backends: lækende closures, ubegrænsede caches og hængende event listeners.
Læsning af heapen med process.memoryUsage()
Dit første billige signal er process.memoryUsage(). Hold øje med heapUsed over tid. Hvis den stiger monotont under konstant belastning, har du et problem med fastholdte objekter.
Uddraget nedenfor simulerer et memory leak ved at indsætte i et array på modulniveau og logger heap-væksten ved hvert interval – en selvstændig reproduktion, du kan køre.
const leaky = [];
function tick(i) {
// Each call retains a 10k-element array forever.
leaky.push(new Array(10_000).fill(i));
const { heapUsed } = process.memoryUsage();
console.log(`tick ${i}: heapUsed=${(heapUsed / 1024 / 1024).toFixed(1)} MB, retained=${leaky.length}`);
}
for (let i = 1; i <= 5; i++) tick(i);
console.log('Heap keeps growing because `leaky` is never cleared.');Lækmønster 1: Den capturing closure
En closure holder alt i sin scope-kæde i live, selv variabler den ikke bruger, så længe noget har en reference til closuren. Hvis du gemmer sådanne closures i en struktur med lang levetid, fastholder du store objekter for altid.
Nedenfor lukker hver registrerede handler over en bigData-buffer på flere megabyte. Handlerne lever i et array på modulet, så bufferne aldrig kan indsamles.
const handlers = [];
function register(id) {
const bigData = Buffer.alloc(1024 * 1024); // 1 MB per registration
// The closure captures bigData even though it only logs id.
handlers.push(() => console.log(`handler ${id} fired`));
return bigData.length;
}
for (let i = 0; i < 3; i++) register(i);
console.log(`${handlers.length} handlers retained; each closure may pin its scope.`);Sådan retter du et closure-leak
To løsninger:
- Fang ikke det, du ikke har brug for. Hent den nødvendige primitive værdi ud, før du opretter closuren, så det store objekt falder ud af closurens scope.
- Gem ikke closures i containere med lang levetid, medmindre du også fjerner dem.
Her fanger closuren kun den lille primitive id; bigData bruges og kasseres, så GC frigiver den, efter register returnerer.
const handlers = [];
function register(id) {
const bigData = Buffer.alloc(1024 * 1024);
const summary = bigData.length; // use it now
// Closure captures only `id` and `summary` (primitives) — bigData is free to GC.
handlers.push(() => console.log(`handler ${id}: ${summary} bytes processed`));
}
register(1);
handlers[0]();
console.log('bigData is no longer reachable and will be collected.');Lækmønster 2: Den ubegrænsede cache
Det mest almindelige memory leak i Node: En Map eller et almindeligt objekt, der bruges som in-memory-cache, og som kun vokser. Hver unik nøgle (bruger-id, request-hash, session-token) tilføjer en post, og ingen poster fjernes.
Det ser uskyldigt ud i en code review, men lækker lineært med trafikken.
const cache = new Map();
function getUser(id) {
if (!cache.has(id)) {
cache.set(id, { id, profile: Buffer.alloc(50_000) }); // 50 KB each, never evicted
}
return cache.get(id);
}
for (let id = 0; id < 4; id++) getUser(id);
console.log(`cache size=${cache.size} — grows forever with unique ids.`);Sådan retter du cachen: Begrænset LRU + TTL
En in-memory-cache SKAL have en grænse. Brug en gennemprøvet LRU (lru-cache) med max-poster og en ttl, eller implementér en simpel Map, der fjerner poster ved kapacitetsgrænsen. Indsættelsesrækkefølgen i JS Maps gør en grundlæggende LRU enkel: Slet og indsæt igen for at opdatere senest brugt, og fjern den ældste nøgle, når kapaciteten overskrides.
class LRU {
constructor(max) { this.max = max; this.map = new Map(); }
get(key) {
if (!this.map.has(key)) return undefined;
const v = this.map.get(key);
this.map.delete(key); this.map.set(key, v); // mark most-recent
return v;
}
set(key, val) {
if (this.map.has(key)) this.map.delete(key);
this.map.set(key, val);
if (this.map.size > this.max) this.map.delete(this.map.keys().next().value); // evict oldest
}
}
const cache = new LRU(2);
cache.set('a', 1); cache.set('b', 2); cache.set('c', 3); // 'a' evicted
console.log('keys:', [...cache.map.keys()]); // [ 'b', 'c' ]Lækmønster 3: Hængende event listeners
Hver emitter.on(...) gemmer en reference. Hvis du tilknytter en listener på en varm sti (pr. request, socket eller timer) og aldrig kalder removeListener/off, vokser emitterens listener-array uden grænse – og hver listener kan lukke over data, der hører til den enkelte request.
Node advarer dig med MaxListenersExceededWarning, når en emitter passerer 10 listeners. Betragt advarslen som en alarm om memory leak, ikke som støj.
const { EventEmitter } = require('events');
const bus = new EventEmitter();
function handleRequest(reqId) {
// BUG: a new listener every request, never removed.
bus.on('shutdown', () => console.log(`drain req ${reqId}`));
}
for (let i = 0; i < 12; i++) handleRequest(i);
console.log('listenerCount(shutdown):', bus.listenerCount('shutdown'));Sådan retter du listeners: once, off og AbortSignal
Match hver on med en off, eller undgå ophobningen helt:
- Brug
emitter.once(...), når handleren kun skal udløses én gang. - Gem funktionsreferencen, så du kan kalde
emitter.off(event, fn)under oprydningen. - Moderne API'er accepterer en
AbortSignal– afbryd controlleren, så alle tilknyttede listeners fjernes på én gang.
const { EventEmitter } = require('events');
const bus = new EventEmitter();
function handleRequest(reqId) {
const controller = new AbortController();
bus.on('shutdown', () => console.log(`drain req ${reqId}`), { signal: controller.signal });
// When the request finishes, abort once — listener auto-removed.
return () => controller.abort();
}
const cleanups = [];
for (let i = 0; i < 12; i++) cleanups.push(handleRequest(i));
cleanups.forEach((done) => done());
console.log('after cleanup, listenerCount:', bus.listenerCount('shutdown')); // 0Timere og streams: De skjulte fastholdere
To fastholdere, som folk glemmer:
- setInterval holder sit callback (og sin closure) i live, indtil
clearIntervalkaldes. Et interval pr. forbindelse, som aldrig ryddes op, lækker hele forbindelsens scope. - Uforbrugte streams og sockets, der ikke destrueres, buffer data i hukommelsen. Kald altid
stream.destroy()ved fejl, og håndtér backpressure.
timer.unref() lader processen afslutte, men frigiver IKKE callbacket – du skal stadig rydde det for at stoppe fastholdelsen.
function startWorker(job) {
const buf = Buffer.alloc(500_000); // retained by the interval closure
const t = setInterval(() => {
if (job.done) {
clearInterval(t); // releases the closure -> buf can be collected
console.log('worker stopped, scope released');
}
}, 10);
return t;
}
const job = { done: false };
startWorker(job);
setTimeout(() => { job.done = true; }, 30);WeakMap og WeakRef: Caches, der giver slip
Når en cache-nøgle er et objekt, hvis levetid du ikke styrer, skal du bruge en WeakMap. Dens poster forhindrer ikke, at nøglen bliver garbage-collected, så tilknyttede data forsvinder automatisk, når nøglen dør – ingen fjernelsespolitik er nødvendig.
Brug WeakRef + FinalizationRegistry til avancerede caches, der holder værdier svagt. Bemærk: Du kan ikke iterere over en WeakMap, og den accepterer kun objektnøgler, så den egner sig ikke til primitive nøgler som streng-id'er.
const meta = new WeakMap();
function attachMeta(obj) {
meta.set(obj, { seen: Date.now() });
}
let session = { id: 'abc' };
attachMeta(session);
console.log('has meta:', meta.has(session)); // true
// Once `session` is unreachable, its WeakMap entry is collected automatically.
session = null;
console.log('session dropped; WeakMap entry becomes eligible for GC.');Bekræftelse af et memory leak: Heap-snapshots
Hukommelsestællere fortæller dig, AT du har et memory leak; heap-snapshots fortæller dig, HVAD der lækker. Arbejdsgang:
- Start processen med
node --inspect, og åbnchrome://inspect, ELLER kaldrequire('v8').writeHeapSnapshot()fra koden. - Tag snapshot A, kør endpointet N gange, og tag snapshot B.
- Brug visningen Comparison, og sortér efter Delta. Objekter, hvis antal vokser præcis med N, er dit memory leak.
- Undersøg Retainers for at se, hvilken rod (en Map, en closure eller et listener-array) der holder dem.
Til automatisk registrering i CI dumper node --heapsnapshot-near-heap-limit=2 et snapshot lige før OOM.
const v8 = require('v8');
const fs = require('fs');
const file = v8.writeHeapSnapshot();
console.log('wrote heap snapshot:', file);
console.log('size bytes:', fs.statSync(file).size);
console.log('Load this .heapsnapshot in Chrome DevTools > Memory > Load.');Hurtigt tjek: Vælg den rigtige cache
Du tilføjer en in-memory-cache med strengbaserede bruger-id'er som nøgler i en langvarigt kørende API. Trafikken er ubegrænset, og du skal garantere, at hukommelsesforbruget forbliver begrænset. Hvilken tilgang er korrekt?
Opsummering: En tjekliste til at finde hukommelseslækager
Du kan nu finde og løse de tre dominerende heap-lækager i Node.js:
- Closures: Indfang kun de primitiver, du har brug for, og gem aldrig closures i arrays på modulniveau uden at fjerne dem igen.
- Caches: Alle in-memory-caches skal have en grænse — LRU med
max+ttleller enWeakMap, når nøglerne er objekter, du ikke selv ejer. - Lyttere og timere: Par hver
onmedoff(eller brugonce/AbortSignal), og kaldclearIntervalfor hver timer. BetragtMaxListenersExceededWarningsom en alarm.
Arbejdsgang: Hold øje med heapUsed for at se, om grundniveauet stiger. Tag derefter sammenlignelige heap-snapshots, og følg kæden af objekter, der holder referencer, tilbage til roden. Mål i stedet for at gætte.
Lær JavaScript med en AI-underviser — gratis
Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.
- Kurser
- 22
- Lektioner
- 92
Ofte stillede spørgsmål
Er lektionen “Registrering og løsning af almindelige leak-mønstre” gratis?
Ja — hele teksten til “Registrering og løsning af almindelige leak-mønstre” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Bootcamp i backendudvikling med Node.js-kurset, skal du opgradere til CoddyKit PRO. Bootcamp i backendudvikling med Node.js-kurset indeholder 4 lektioner i alt.
Hvad lærer jeg i “Registrering og løsning af almindelige leak-mønstre”?
Find closures med leaks, caches uden grænser og vedvarende listeners, der gradvist øger heapens størrelse. Du øver dig i Bootcamp i backendudvikling med Node.js med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.
Skal jeg have erfaring for at begynde på Bootcamp i backendudvikling med Node.js?
Der kræves ingen tidligere erfaring. Bootcamp i backendudvikling med Node.js på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 4 af 4.
Hvor lang tid tager lektionen “Registrering og løsning af almindelige leak-mønstre”?
De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.
Kan jeg skrive og køre kode i denne Bootcamp i backendudvikling med Node.js-lektion?
Ja. Alle Bootcamp i backendudvikling med Node.js-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.
Alle lektioner i dette kursus
- V8-heapen, generational GC og objekters levetid
- Optagelse og sammenligning af heap snapshots
- CPU-profilering og flame graphs for hot paths
- Registrering og løsning af almindelige leak-mønstre