Kem Intensif Pembangunan Backend Node.js · Pelajaran

Timbunan V8, GC Generasi dan Jangka Hayat Objek

Fahami cara V8 memperuntukkan dan mengumpul memori merentas generasi muda dan tua.

Pelajaran 1 daripada 413 langkah

Timbunan V8, GC Generasi dan Jangka Hayat Objek ialah pelajaran Kem Intensif Pembangunan Backend Node.js percuma di CoddyKit. Ini ialah pelajaran 1 daripada 4. Anda boleh membaca keseluruhan pelajaran di bawah secara percuma — kemudian berlatih secara praktikal dalam pelayar menggunakan penyunting kod terbina dalam dan tutor kecerdasan buatan 24/7. Pelajaran ini merupakan sebahagian daripada laluan pembelajaran Kem Intensif Pembangunan Backend Node.js, dan kemajuan anda disegerakkan merentas web serta aplikasi CoddyKit. Kursus Kem Intensif Pembangunan Backend Node.js merangkumi sejumlah 4 pelajaran.

Mengapa Mekanik Heap Penting dalam Node

Setiap objek, penutupan dan penimbal yang diperuntukkan oleh perkhidmatan Node.js anda tinggal di suatu tempat dalam memori terurus V8. Memahami di mana ia tinggal dan bila ia dituntut semula membezakan antara perkhidmatan yang kekal stabil pada 200 MB dengan perkhidmatan yang merangkak menuju ranap OOM apabila menerima beban.

V8 membahagikan memori terurusnya kepada dua rantau utama:

  • Ruang baharu (generasi muda) — kecil dan pantas, tempat hampir semua objek dilahirkan.
  • Ruang lama (generasi tua) — besar, tempat objek yang terselamat terus hidup.

Anda boleh memeriksa angka semasa ketika pelaksanaan dengan 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));

Hipotesis Generasi Lemah

Pengutip sampah V8 dibina berdasarkan satu pemerhatian empirikal yang dipanggil hipotesis generasi lemah: kebanyakan objek mati muda.

Dalam pengendali permintaan biasa, anda memperuntukkan objek mengikut skop permintaan — isi JSON yang dihuraikan, tatasusunan sementara dan rentetan perantaraan — yang menjadi sampah sebaik sahaja respons dihantar. Sebilangan kecil sahaja (cache, kumpulan sambungan dan singleton peringkat modul) kekal sepanjang tempoh hayat proses.

Ketidakseimbangan ini membolehkan V8 mengoptimumkan secara agresif: ia mengutip generasi muda kerap dan dengan kos rendah, manakala generasi tua dikutip jarang tetapi menyeluruh. Setiap generasi mendapat algoritma pengutip sampah yang ditala mengikut corak kelangsungan hidupnya.

Ruang Baharu: Scavenge dan Ruang Separuh

Ruang baharu dikutip oleh Scavenger, iaitu pengutip penyalinan yang menggunakan algoritma Cheney. Ruang baharu dibahagikan kepada dua bahagian sama besar yang dipanggil ruang separuh: to-space dan from-space.

  • Objek baharu diperuntukkan secara berperingkat ke dalam to-space (hanya memajukan penuding — sangat pantas).
  • Apabila to-space penuh, GC kecil (scavenge) dijalankan: objek hidup disalin ke from-space, peranan kedua-duanya ditukar dan to-space lama dipadamkan sepenuhnya.

Oleh sebab kos berkadar dengan objek yang terselamat (bukan sampah), dan kebanyakan objek telah mati ketika pengutipan, proses scavenge sangat murah — biasanya kurang daripada satu milisaat.

Memperuntukkan dalam Generasi Muda

Perhatikan generasi muda beraksi. Gelung di bawah memperuntukkan berjuta-juta objek berjangka hayat pendek. Setiap objek mati hampir serta-merta, jadi objek tersebut tidak pernah meninggalkan ruang baharu — Scavenger menuntutnya semula dalam kitaran yang pantas dan murah, manakala heapUsed kekal lebih kurang rata.

Inilah corak peruntukan yang ideal untuk pengendali permintaan: peruntukkan dengan bebas, biarkan objek mati dengan pantas dan GC hampir tidak menyedarinya.

// 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));

Promosi: Terselamat ke Ruang Lama

Objek yang terselamat daripada proses scavenge tidak terus menjadi objek lama. V8 menjejaki kelangsungan hidup bagi setiap objek:

  • Objek yang terselamat daripada proses scavenge pertama disalin ke from-space (masih muda, tetapi kini dalam keadaan perantaraan).
  • Objek yang terselamat daripada proses scavenge kedua akan dipromosikan (dituakan) ke ruang lama.

Jadi, objek yang dirujuk cukup lama untuk melalui dua GC kecil akan naik ke generasi tua. Terdapat juga laluan promosi agresif: jika to-space lebih daripada kira-kira 80–90% penuh selepas proses scavenge, V8 mempromosikan objek yang terselamat lebih awal untuk mengelakkan ruang separuh kecil itu berulang-alik secara berlebihan.

Memaksa Promosi Dengan Rujukan yang Dikekalkan

Corak di bawah mengekalkan setiap objek yang diperuntukkan dalam tatasusunan berjangka hayat panjang. Objek tersebut terselamat daripada proses scavenge berulang, dipromosikan ke ruang lama dan heapUsed meningkat secara berterusan. Inilah rupa cache dalam memori tanpa had — dan cara kebocoran perlahan bermula.

Pengajarannya: pengekalan, bukannya kadar peruntukan, yang memenuhi ruang lama. Pengendali yang memperuntukkan sejuta objek tetapi tidak menyimpan satu pun hampir tidak menelan kos; pengendali yang menyimpan seribu objek selama-lamanya akan mengalami kebocoran.

// 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));

Ruang Lama: Tandakan-Sapu-Mampat

Ruang lama dikutip oleh GC utama menggunakan Mark-Sweep-Compact:

  • Tandakan — rentasi graf objek daripada akar GC (objek global, tindanan pelaksanaan dan pemegang natif) lalu tandakan segala yang boleh dicapai.
  • Sapu — tambahkan memori objek yang tidak ditandakan (mati) ke dalam senarai bebas untuk digunakan semula.
  • Mampat — pindahkan objek hidup secara berkala untuk mengurangkan pemecahan, supaya peruntukan besar dapat mencari ruang bersebelahan.

GC utama jauh lebih mahal berbanding scavenge kerana ia perlu merentasi seluruh generasi tua. Untuk mengelakkan jeda panjang, V8 menjalankan kebanyakan proses penandaan secara serentak pada bebenang latar dan secara berperingkat berselang-seli dengan JS anda — inilah seni bina GC Orinoco.

Kebolehcapaian Menentukan Sampah

Sesuatu objek ialah sampah jika dan hanya jika ia tidak boleh dicapai daripada akar GC. V8 tidak menggunakan pengiraan rujukan, jadi kitaran dikutip dengan betul — dua objek yang menunjuk satu sama lain tetap merupakan sampah jika tiada apa-apa yang boleh dicapai menunjuk kepada mana-mana daripadanya.

Kesimpulannya untuk kod bahagian belakang: kebocoran memori hampir selalu merupakan rujukan tidak sengaja yang memastikan sesuatu objek boleh dicapai. Punca lazim termasuk Map peringkat modul, pendengar peristiwa yang tidak pernah dibuang dan penutupan yang menangkap skop besar.

// 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');

Menentukan Saiz Heap: --max-old-space-size

Generasi tua mempunyai had atas yang boleh dikonfigurasikan. Pada Node 64-bit, had ruang lama lalai ialah kira-kira 2 GB (versi lama menggunakan nilai lalai hampir 1.5 GB; Node moden menskalakannya mengikut memori yang tersedia). Apabila ruang lama tidak boleh berkembang lagi dan GC utama gagal membebaskan ruang yang mencukupi, proses mati dengan FATAL ERROR: ... JavaScript heap out of memory.

Anda boleh menaikkan had tersebut dengan bendera V8 --max-old-space-size (dalam MB):

  • node --max-old-space-size=4096 server.js menetapkan had ruang lama sebanyak 4 GB.

Menaikkannya memberikan ruang tambahan tetapi bukan penyelesaian untuk kebocoran — kebocoran hanya akan mencapai had baharu yang lebih tinggi itu kemudian. Sentiasa sertakan penentuan saiz bersama diagnosis kebocoran.

Memerhati GC Dengan perf_hooks

Anda boleh memerhati secara langsung peristiwa GC kecil berbanding GC utama menggunakan PerformanceObserver terbina dalam perf_hooks dengan entryTypes: ['gc']. detail.kind bagi setiap entri memberitahu jenis pengutipan:

  • NODE_PERFORMANCE_GC_MINOR — scavenge (generasi muda).
  • NODE_PERFORMANCE_GC_MAJOR — tandakan-sapu-mampat (generasi tua).

Gunakan ini dalam ujian beban untuk mengesahkan sama ada perkhidmatan anda didominasi oleh GC kecil yang murah (sihat) atau GC utama yang kerap dan mahal (masalah pengekalan).

// 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');

Mereka Bentuk untuk GC Generasi

Amalan konkrit yang memastikan perkhidmatan anda serasi dengan pengutip generasi V8:

  • Biarkan data permintaan mati muda. Elakkan menyimpan objek mengikut permintaan dalam struktur peringkat modul; objek tersebut akan dipromosikan dan anda menanggung kos GC utama selama-lamanya.
  • Hadkan cache anda. Gunakan LRU dengan saiz maksimum berbanding Map tanpa had supaya ruang lama kekal rata.
  • Utamakan WeakMap/WeakRef untuk metadata bersekutu yang dikunci oleh objek yang bukan milik anda — entri hilang apabila kunci itu tidak lagi boleh dicapai.
  • Gunakan semula penimbal besar melalui kumpulan berbanding memperuntukkannya semula; peruntukan besar membebankan ruang lama dan pemampatan.

Model mentalnya: perubahan dalam generasi muda yang murah dan boleh dilupuskan tidak menjadi masalah; pertumbuhan jangka panjang dalam ruang lama ialah perkara yang perlu anda pantau.

// 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');

Semakan Pantas: Tempoh Hayat Objek

Soalan penaakulan tentang cara sesuatu objek bergerak melalui generasi V8.

Imbas Kembali: Heap, Generasi dan Tempoh Hayat

Perkara penting untuk diingati:

  • Dua generasi. Ruang baharu (muda) menyimpan objek yang baru diperuntukkan; ruang lama (tua) menyimpan objek yang masih hidup.
  • Hipotesis generasi lemah. Kebanyakan objek mati ketika masih muda, jadi V8 mengumpul ruang baharu dengan kerap dan kos yang rendah, manakala ruang lama dikumpul jarang-jarang tetapi dengan teliti.
  • Pengumpulan kecil. GC kecil menyalin objek yang masih hidup antara ruang separa; kosnya bergantung pada bilangan objek yang masih hidup, bukan sampah.
  • Naik taraf generasi. Objek yang terselamat daripada dua pengumpulan kecil (atau memenuhi ruang destinasi) akan dipindahkan ke ruang lama.
  • Tanda-Sapu-Padatkan. GC besar menjejaki objek daripada punca, membebaskan objek yang tidak boleh dicapai, dan kadangkala memadatkan ruang — kebanyakannya berjalan secara serentak atau berperingkat.
  • Kebocoran ialah pengekalan. Objek yang tidak boleh dicapai bermaksud objek itu boleh dikumpul, walaupun melalui kitaran; rujukan tidak sengaja yang berjangka hayat panjanglah yang memenuhi ruang lama dan menyebabkan OOM.
  • Alat. process.memoryUsage(), entri GC daripada perf_hooks, dan --max-old-space-size membolehkan anda mengukur, memerhati dan menetapkan saiz timbunan.
Percuma untuk bermula

Pelajari JavaScript dengan tutor kecerdasan buatan — percuma

Tulis dan jalankan kod sebenar dalam pelayar anda, dapatkan bantuan segera daripada tutor kecerdasan buatan yang tersedia 24/7, dan sambung semula dari tempat anda berhenti di web atau dalam aplikasi.

Kursus
22
Pelajaran
92

Soalan Lazim

Adakah pelajaran “Timbunan V8, GC Generasi dan Jangka Hayat Objek” percuma?

Ya — teks penuh “Timbunan V8, GC Generasi dan Jangka Hayat Objek” boleh dibaca secara percuma di web ini. Untuk berlatih secara interaktif menggunakan penyunting kod terbina dalam dan tutor kecerdasan buatan 24/7, serta membuka kunci baki kursus Kem Intensif Pembangunan Backend Node.js, tingkat taraf kepada CoddyKit PRO. Kursus Kem Intensif Pembangunan Backend Node.js merangkumi sejumlah 4 pelajaran.

Apakah yang akan saya pelajari dalam “Timbunan V8, GC Generasi dan Jangka Hayat Objek”?

Fahami cara V8 memperuntukkan dan mengumpul memori merentas generasi muda dan tua. Anda berlatih Kem Intensif Pembangunan Backend Node.js menggunakan kod praktikal yang dijalankan terus dalam pelayar, manakala tutor kecerdasan buatan 24/7 menjawab soalan anda semasa anda mengikuti pelajaran.

Adakah saya memerlukan pengalaman untuk memulakan Kem Intensif Pembangunan Backend Node.js?

Tiada pengalaman terdahulu diperlukan. Pembelajaran Kem Intensif Pembangunan Backend Node.js di CoddyKit disusun untuk pelajar daripada peringkat pemula hingga lanjutan, jadi anda boleh bermula di sini atau dari awal dan belajar mengikut kadar anda sendiri. Ini ialah pelajaran 1 daripada 4.

Berapa lamakah pelajaran “Timbunan V8, GC Generasi dan Jangka Hayat Objek” diambil?

Kebanyakan pelajaran CoddyKit mengambil masa kira-kira 5–10 minit. Setiap pelajaran ringkas dan interaktif, jadi anda boleh membuat kemajuan secara berterusan dan menyambung tepat dari tempat anda berhenti di web atau aplikasi.

Bolehkah saya menulis dan menjalankan kod dalam pelajaran Kem Intensif Pembangunan Backend Node.js ini?

Ya. Setiap pelajaran Kem Intensif Pembangunan Backend Node.js menyertakan penyunting kod terbina dalam, jadi anda boleh menulis dan menjalankan kod sebenar terus dalam pelayar serta menerima maklum balas kecerdasan buatan serta-merta — tanpa memerlukan persediaan setempat.

Semua pelajaran dalam kursus ini

  1. Timbunan V8, GC Generasi dan Jangka Hayat Objek
  2. Merakam dan Membandingkan Syot Kilat Timbunan
  3. Pemprofilan CPU dan Graf Api untuk Laluan Panas
  4. Mengesan dan Membetulkan Corak Kebocoran Biasa
← Kembali ke Kem Intensif Pembangunan Backend Node.js