Node.js-taustakehityksen bootcamp · Oppitunti

V8:n heap, generatiivinen GC ja olioiden elinkaaret

Ymmärrä, miten V8 varaa ja kerää muistia nuoren ja vanhan sukupolven alueilla.

Oppitunti 1/413 vaihetta

V8:n heap, generatiivinen GC ja olioiden elinkaaret on ilmainen Node.js-taustakehityksen bootcamp-oppitunti CoddyKitissä. Tämä on oppitunti 1/4. Voit lukea koko oppitunnin alta ilmaiseksi ja harjoitella sen jälkeen käytännössä selaimessa sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla. Oppitunti kuuluu Node.js-taustakehityksen bootcamp-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. Node.js-taustakehityksen bootcamp-kurssilla on yhteensä 4 oppituntia.

Miksi kekomuistin toiminta on tärkeää Nodessa

Jokainen Node.js-palvelun varaama objekti, sulkeuma ja puskuri sijaitsee jossain V8:n hallinnoimassa muistissa. Kun ymmärrät, missä se sijaitsee ja milloin se vapautetaan, voit erottaa palvelun, joka pysyy vakaasti 200 megatavussa, palvelusta, jonka muistinkulutus kasvaa kuormituksen alla kohti OOM-kaatumista.

V8 jakaa hallinnoidun muistinsa kahteen pääalueeseen:

  • New space (nuori sukupolvi) – pieni ja nopea alue, jonne lähes jokainen objekti syntyy.
  • Old space (vanha sukupolvi) – suuri alue, jonne selviävät objektit siirtyvät.

Voit tarkastella ajantasaisia lukemia ajonaikaisesti kutsulla process.memoryUsage().

// heap_snapshot.js — print V8 heap usage in MB
function mb(bytes) {
  return (bytes / 1024 / 1024).toFixed(2) + ' MB';
}

const u = process.memoryUsage();
console.log('rss        ', mb(u.rss));        // total process memory
console.log('heapTotal  ', mb(u.heapTotal));  // V8 heap reserved
console.log('heapUsed   ', mb(u.heapUsed));   // V8 heap live
console.log('external   ', mb(u.external));   // C++ objects bound to JS
console.log('arrayBuffers', mb(u.arrayBuffers));

Heikko sukupolvihypoteesi

V8:n GC perustuu yhteen empiiriseen havaintoon, jota kutsutaan heikoksi sukupolvihypoteesiksi: useimmat objektit kuolevat nuorina.

Tyypillisessä pyyntökäsittelijässä varaat pyyntökohtaisia objekteja – jäsennettyjä JSON-runkoja, väliaikaisia taulukoita ja välimerkkijonoja – joista tulee roskaa heti vastauksen lähettämisen jälkeen. Pieni vähemmistö (välimuistit, yhteyspoolit ja moduulitason singletonit) säilyy koko prosessin eliniän.

Tämän vinouman ansiosta V8 voi optimoida toimintaansa tehokkaasti: se kerää nuoren sukupolven usein ja edullisesti ja vanhan sukupolven harvoin ja perusteellisesti. Kullekin sukupolvelle on oma sen säilymisprofiiliinsa optimoitu GC-algoritmi.

New space: scavenge-keräys ja puolitilat

New spacen kerää Scavenger, joka on Cheneyn algoritmia käyttävä kopioiva kerääjä. New space on jaettu kahteen yhtä suureen puolikkaaseen, joita kutsutaan puolitiloiksi: to-space ja from-space.

  • Uudet objektit varataan to-spaceen osoitinta siirtämällä, mikä on erittäin nopeaa.
  • Kun to-space täyttyy, suoritetaan minor GC eli scavenge-keräys: elossa olevat objektit kopioidaan from-spaceen, roolit vaihtuvat ja vanha to-space tyhjennetään kokonaan.

Koska kustannus on verrannollinen elossa säilyvien objektien määrään (ei roskan määrään) ja useimmat objektit ovat keräyshetkellä kuolleita, scavenge-keräykset ovat erittäin edullisia – tyypillisesti alle millisekunnin.

Varaaminen nuoressa sukupolvessa

Seuraa nuoren sukupolven toimintaa käytännössä. Alla oleva silmukka varaa miljoonia lyhytikäisiä objekteja. Jokainen niistä kuolee lähes välittömästi, joten ne eivät koskaan poistu new spacesta – Scavenger ottaa ne talteen tiheissä ja edullisissa sykleissä, ja heapUsed pysyy suunnilleen tasaisena.

Tämä on ihanteellinen varaamismalli pyyntökäsittelijälle: varaa vapaasti, anna objektien kuolla nopeasti ja anna GC:n tuskin huomata niitä.

// young_gen.js — millions of short-lived allocations
function work() {
  let acc = 0;
  for (let i = 0; i < 5_000_000; i++) {
    // {x, y} is born and dies within the iteration
    const point = { x: i, y: i * 2 };
    acc += point.x + point.y;
  }
  return acc;
}

const before = process.memoryUsage().heapUsed;
const result = work();
const after = process.memoryUsage().heapUsed;
console.log('result:', result);
console.log('heapUsed delta (MB):', ((after - before) / 1024 / 1024).toFixed(2));

Promootio: selviäminen old spaceen

Scavenge-keräyksestä selvinnyt objekti ei muutu heti vanhaksi. V8 seuraa kunkin objektin selviytymistä:

  • Ensimmäisestä scavenge-keräyksestä selvinnyt objekti kopioidaan from-spaceen (se on edelleen nuori, mutta nyt välitilassa).
  • Toisesta scavenge-keräyksestä selvinnyt objekti promotoidaan eli siirretään vanhaan sukupolveen.

Objektit, joihin viitataan riittävän pitkään, jotta ne selviävät kahdesta minor GC:stä, siirtyvät siis vanhaan sukupolveen. Käytössä on myös aggressiivinen promootio: jos to-space on scavenge-keräyksen jälkeen yli noin 80–90-prosenttisesti täynnä, V8 promotoi selvinneet objektit aikaisin välttääkseen pienen puolitilan jatkuvan kuormittamisen.

Promootion pakottaminen säilytetyllä viitteellä

Alla oleva malli säilyttää jokaisen varatun objektin pitkäikäisessä taulukossa. Objektit selviävät toistuvista scavenge-keräyksistä, ne promotoidaan old spaceen ja heapUsed kasvaa tasaisesti. Tämä näyttää täsmälleen rajoittamattomalta muistin sisäiseltä välimuistilta – ja näin hidas muistivuoto alkaa.

Opetus on tämä: old spacen täyttää säilyttäminen, ei varaamisnopeus. Miljoona objektia varaava mutta niistä yhtäkään säilyttämätön käsittelijä maksaa lähes olemattoman hinnan; tuhat objektia ikuisesti säilyttävä aiheuttaa vuodon.

// promotion.js — retained objects get tenured into old space
const retained = [];

for (let i = 0; i < 1_000_000; i++) {
  // pushing keeps a live reference -> survives scavenges -> promoted
  retained.push({ id: i, payload: 'item-' + i });
}

const u = process.memoryUsage();
console.log('retained length:', retained.length);
console.log('heapUsed (MB):', (u.heapUsed / 1024 / 1024).toFixed(2));
console.log('heapTotal (MB):', (u.heapTotal / 1024 / 1024).toFixed(2));

Old space: merkitse–pyyhi–tiivistä

Old space kerätään major GC -keräyksellä, joka käyttää Mark-Sweep-Compact-menetelmää:

  • Merkitse – käy objektigraafi läpi GC-juurista (globaali objekti, suorituspino ja natiivikahvat) ja merkitse kaikki saavutettavat objektit.
  • Pyyhi – lisää merkitsemättömien eli kuolleiden objektien muisti vapaisiin listoihin uudelleenkäyttöä varten.
  • Tiivistä – siirrä elossa olevia objekteja aika ajoin pirstoutumisen vähentämiseksi, jotta suuret varaukset löytävät yhtenäistä tilaa.

Major GC on paljon scavenge-keräystä kalliimpi, koska sen on käytävä koko old generation läpi. V8 välttää pitkiä taukoja suorittamalla suurimman osan merkitsemisestä samanaikaisesti taustasäikeissä ja asteittain JavaScript-suoritukseen lomitettuna. Tämä on Orinoco GC -arkkitehtuuri.

Saavutettavuus määrittää roskan

Objekti on roskaa täsmälleen silloin, kun se on saavuttamattomissa GC-juurista. V8 ei käytä viitelaskentaa, joten syklit kerätään oikein: kaksi toisiinsa viittaavaa objektia on edelleen roskaa, jos mikään saavutettava objekti ei viittaa kumpaankaan niistä.

Palvelinkoodin kannalta seuraus on tämä: muistivuoto on lähes aina vahingossa säilyvä viite, joka pitää objektin saavutettavana. Yleisiä syyllisiä ovat moduulitason Map-rakenteet, poistamatta jätetyt tapahtumankuuntelijat ja suuria skooppeja kaappaavat sulkeumat.

// reachability.js — an unintentional cycle is still collectable
function makePair() {
  const a = {};
  const b = {};
  a.peer = b;   // a -> b
  b.peer = a;   // b -> a (cycle)
  return a;
}

let root = makePair();
console.log('reachable via root:', root.peer.peer === root);

root = null; // drop the only root reference
// Both a and b are now unreachable despite the cycle;
// V8 will reclaim them on the next GC. No leak.
console.log('root dropped; cycle is now garbage');

Kekomuistin mitoittaminen: --max-old-space-size

Vanhalle sukupolvelle voi määrittää ylärajan. 64-bittisessä Nodessa old spacen oletusraja on noin 2 Gt (vanhemmissa versioissa oletus oli lähellä 1,5:tä gigatavua; moderni Node suhteuttaa rajan käytettävissä olevaan muistiin). Kun old space ei voi enää kasvaa ja major GC ei vapauta riittävästi muistia, prosessi päättyy virheeseen FATAL ERROR: ... JavaScript heap out of memory.

Voit nostaa ylärajaa V8:n lipulla --max-old-space-size (megatavuina):

  • node --max-old-space-size=4096 server.js asettaa old spacen rajaksi 4 Gt.

Rajan nostaminen antaa lisää liikkumavaraa, mutta ei korjaa vuotoa – vuoto saavuttaa myöhemmin vain uuden, korkeamman rajan. Yhdistä mitoitukseen aina vuodon selvittäminen.

GC:n tarkkailu perf_hooks-moduulilla

Voit tarkkailla minor- ja major GC -tapahtumia suoraan sisäänrakennetun perf_hooks-moduulin PerformanceObserver-toteutuksella käyttämällä arvoa entryTypes: ['gc']. Kunkin merkinnän detail.kind kertoo keräyksen tyypin:

  • NODE_PERFORMANCE_GC_MINOR – scavenge-keräys (nuori sukupolvi).
  • NODE_PERFORMANCE_GC_MAJOR – mark-sweep-compact-keräys (vanha sukupolvi).

Käytä tätä kuormitustesteissä selvittääksesi, hallitsevatko palvelua edulliset minor GC -keräykset (terve tilanne) vai usein toistuvat kalliit major GC -keräykset (säilymisongelma).

// gc_observer.js — log each GC event and its duration
const { PerformanceObserver, constants } = require('perf_hooks');

const kinds = {
  [constants.NODE_PERFORMANCE_GC_MINOR]: 'minor (scavenge)',
  [constants.NODE_PERFORMANCE_GC_MAJOR]: 'major (mark-sweep)',
  [constants.NODE_PERFORMANCE_GC_INCREMENTAL]: 'incremental',
  [constants.NODE_PERFORMANCE_GC_WEAKCB]: 'weak-callback',
};

const obs = new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    console.log(kinds[e.detail.kind] || 'other', '-', e.duration.toFixed(2), 'ms');
  }
});
obs.observe({ entryTypes: ['gc'] });

// generate garbage to trigger collections
let sink = [];
for (let i = 0; i < 2_000_000; i++) {
  sink.push({ i });
  if (sink.length > 50_000) sink = [];
}
console.log('done allocating');

Suunnittelu sukupolvien GC:tä varten

Käytännön toimintatapoja, jotka pitävät palvelun V8:n sukupolvikerääjälle sopivana:

  • Anna pyyntötietojen kuolla nuorina. Vältä pyyntökohtaisten objektien tallentamista moduulitason rakenteisiin; ne promotoidaan ja aiheuttavat major GC -kustannuksia jatkuvasti.
  • Rajaa välimuistien koko. Käytä enimmäiskoolla varustettua LRU-välimuistia rajoittamattoman Map-rakenteen sijaan, jotta old space pysyy tasaisena.
  • Suosi WeakMap- ja WeakRef-rakenteita assosiatiivisille metatiedoille, joiden avaimina ovat omistamiesi objektien sijaan muut objektit – merkinnät katoavat, kun avain muuttuu saavuttamattomaksi.
  • Käytä suuria puskureita uudelleen poolien avulla sen sijaan, että varaisit niitä uudelleen; suuret varaukset kuormittavat old spacea ja tiivistämistä.

Ajatusmalli on tämä: edullinen ja lyhytikäinen nuoren sukupolven vaihtuvuus on normaalia; vanhan spacen pitkäikäinen kasvu on se, jota on tarkkailtava.

// weakmap_metadata.js — metadata that auto-collects with its key
const lastSeen = new WeakMap();

function touch(session) {
  lastSeen.set(session, Date.now()); // no strong ref to session
  return lastSeen.get(session);
}

let session = { id: 'abc' };
console.log('touched at:', touch(session));

session = null; // session unreachable -> WeakMap entry eligible for GC
console.log('session released; WeakMap entry will be reclaimed');

Pikatarkistus: objektin elinikä

Pohdintatehtävä siitä, miten objekti kulkee V8:n sukupolvien läpi.

Kertaus: kekomuisti, sukupolvet ja eliniät

Keskeiset asiat:

  • Kaksi sukupolvea. Uusi tila (young) sisältää juuri varatut oliot, vanha tila (old) eloonjääneet oliot.
  • Heikko sukupolvihypoteesi. Useimmat oliot kuolevat nuorina, joten V8 kerää uuden tilan usein ja edullisesti, mutta vanhan tilan harvoin ja perusteellisesti.
  • Scavenge. Minor GC kopioi elävät oliot puolialueiden välillä; kustannus määräytyy eloonjääneiden, ei roskan, määrän mukaan.
  • Promotion. Kahdesta scavengesta selviäminen (tai to-tilan täyttyminen) siirtää olion vanhaan tilaan.
  • Mark-Sweep-Compact. Major GC käy juurista lähtien oliot läpi, vapauttaa saavuttamattomat oliot ja tiivistää muistia ajoittain — toiminta suoritetaan pääosin samanaikaisesti ja asteittain.
  • Vuodot ovat säilyttämistä. Saavuttamattomat oliot voidaan kerätä, myös syklien kautta; vanhan tilan täyttävät ja OOM-tilanteen aiheuttavat tahattomat pitkäikäiset viittaukset.
  • Työkalut. process.memoryUsage(), perf_hooks-moduulin GC-merkinnät ja --max-old-space-size auttavat mittaamaan, seuraamaan ja mitoittamaan kekoa.
Aloita maksutta

Opi JavaScript tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
22
Oppitunnit
92

Usein kysytyt kysymykset

Onko oppitunti ”V8:n heap, generatiivinen GC ja olioiden elinkaaret” ilmainen?

Kyllä – oppitunnin ”V8:n heap, generatiivinen GC ja olioiden elinkaaret” koko tekstin voi lukea täällä verkossa ilmaiseksi. Jos haluat harjoitella interaktiivisesti sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla sekä avata koko Node.js-taustakehityksen bootcamp-kurssin, päivitä CoddyKit PROhon. Node.js-taustakehityksen bootcamp-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”V8:n heap, generatiivinen GC ja olioiden elinkaaret”?

Ymmärrä, miten V8 varaa ja kerää muistia nuoren ja vanhan sukupolven alueilla. Harjoittelet Node.js-taustakehityksen bootcamp-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni Node.js-taustakehityksen bootcamp-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin Node.js-taustakehityksen bootcamp-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 1/4.

Kuinka kauan ”V8:n heap, generatiivinen GC ja olioiden elinkaaret”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä Node.js-taustakehityksen bootcamp-oppitunnilla?

Kyllä. Jokainen Node.js-taustakehityksen bootcamp-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. V8:n heap, generatiivinen GC ja olioiden elinkaaret
  2. Heap-snapshotien kaappaaminen ja vertaileminen
  3. Suorittimen profilointi ja flame graphit kuumille suorituspoluille
  4. Yleisten muistivuotokuvioiden tunnistaminen ja korjaaminen
← Takaisin: Node.js-taustakehityksen bootcamp