15 Desember 2021

Pengumpulan sampah (_Garbage collection_)

Manajemen memori di JavaScript dilakukan secara otomatis dan tak terlihat oleh kita. Kita membuat primitive, objek, fungsi… Semua yang membutuhkan memori.

Apa yang terjadi ketika sesuatu yang telah kita buat tersebut sudah tidak diperlukan? Bagaimana engine JavaScript menemukan dan membersihkannya?

Keterjangkauan (Reachability)

Konsep utama manajemen memori di JavaScript ialah keterjangkauan.

Sederhananya, sebuah nilai yang “terjangkau” adalah mereka yang masih dapat diakses atau dapat digunakan. Mereka dapat dipastikan tersimpan di memori.

  1. Ada sekumpulan nilai-nilai yang terjangkau secara inheren, yang tak dapat dihapus untuk alasan yang jelas.

    Contohnya:

    • Variabel lokal dan parameter-parameter dari fungsi (yang di eksekusi) saat ini.
    • Variabel-variabel dan parameter-parameter dari fungsi-fungsi lain yang terkait dengan rantai panggilan bersarang saat ini.
    • Variabel-variabel global.
    • (ada beberapa hal lain, yang internal juga)

    Nilai-nilai tadi disebut roots.

  2. Nilai lainnya dianggap terjangkau jika dapat dijangkau dari sebuah root melalui sebuah rujukkan atau rantai rujukkan.

    Contoh, jika terdapat sebuah objek didalam global variabel, dan objek tersebut memiliki sebuah properti yang mereferensi objek lain, objek itu dianggap dapat dijangkau. Dan referensinya juga bisa dijangkau. Contoh lengkap dibawah ini.

Ada sebuah background process di engine JavaScript yang disebut garbage collector. Ia mengamati seluruh objek dan menyingkirkan semua yang sudah tak terjangkau.

Contoh Sederhana

Berikut adalah contoh paling sederhana:

// user memiliki rujukkan terhadap objek
let user = {
  name: "John"
};

Tanda panah disini menggambarkan sebuah rujukkan objek. Variabel global "user" merujuk objek {name: "John"} (kita sebut John supaya singkat). Property "name" dari objek John menyimpan sebuah primitive, jadi itu disematkan di dalam objek.

Jika nilai dari user ditimpa, maka rujukkannya hilang:

user = null;

Sekarang John menjadi tak terjangkau. Tak ada cara untuk mengaksesnya, tak ada rujukkan terhadapnya. Garbage collector akan membuang data tersebut and membebaskan memori.

Dua rujukkan

Sekarang bayangkan kita menyalin rujukkan dari user ke admin:

// user memiliki rujukkan terhadap objek
let user = {
  name: "John"
};

let admin = user;

Sekarang jika kita melakukan hal yang sama:

user = null;

…Maka objek “John” tersebut masih bisa dijangkau lewat variabel global admin, jadi masih ada di memori. Jika kita menimpa admin juga, barulah dapat dihilangkan.

Objek-objek yang saling terkait

Sekarang contoh yang lebih kompleks. Keluarga:

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});

Fungsi marry “mengawinkan” dua objek dengan memberikan keduanya rujukkan satu sama lain dan mengembalikan sebuah objek baru yang berisikan kedua objek tersebut.

Hasil struktur memorinya ialah :

Disini, semua objek terjangkau.

Sekarang mari hapus dua rujukkan:

delete family.father;
delete family.mother.husband;

Tak cukup hanya dengan menghapus salah satu dari dua rujukkan tersebut, karena semua objek masih bisa dijangkau.

Tetapi jika kita menghapus keduanya, maka dapat kita lihat bahwa John tak lagi memiliki objek yang merujukkannya:

Rujukkan keluar (outgoing reference) tidak masalah. Hanya rujukkan masuk (incoming reference) yang dapat membuat sebuah objek terjangkau. Jadi, sekarang John tak terjangkau dan akan dihapus dari memori bersama semua datanya yang juga tak dapat diakses.

Setelah garbage collection:

Pulau tak terjangkau

Mungkin saja satu kumpulan (pulau) objek yang saling tertaut menjadi tak terjangkau dan dihapus dari memori.

Objeknya sama seperti diatas. Kemudian:

family = null;

Gambaran in-memory-nya menjadi:

Contoh ini menunjukkan bagaimana pentingnya konsep keterjangkauan (reachability).

Sudah jelas bahwa John dan Ann masih tertaut, keduanya memiliki rujukkan masuk. Tapi itu saja tak cukup.

Objek "family" diatas telah menjadi tak terhubung dengan root, tak ada lagi rujukkan terhadapnya, sehingga keseluruhan pulau kumpulan objek tersebut menjadi tak terjangkau dan akan dihapus.

Algoritma internal

Algoritma garbage collection dasar disebut “mark-and-sweep”.

Langkah “garbage collection” berikut dilakukan secara teratur:

  • Garbage collector mengambil objek roots dan “menandai” (marks / mengingat) mereka.
  • Kemudian ia mendatangi dan “menandai” semua rujukkannya.
  • Kemudian ia mendatangi objek yang telah ditandai tersebut dan menandai rujukkan mereka. Semua objek yang telah dikunjungi akan diingat, agar nantinya tidak mengunjungi objek yang sama dua kali.
  • …Dan seterusnya sampai semua rujukkan yang dapat dijangkau (dari roots) telah dikunjungi.
  • Semua objek kecuali yang ditandai akan dihapus.

Contohnya, semisal kita memiliki struktur objek seperti berikut:

Dapat kita lihat dengan jelas “pulau tak terjangkau” di sisi kanan. Sekarang mari kita lihat bagaimana “mark-and-sweep” garbage collector berurusan dengannya.

Langkah pertama menandai roots-nya:

Kemudian rujukkannya ditandai:

…Dan kemudian rujukkan dalamnya juga, jika masih ada:

Sekarang objek-objek yang tak dapat dikunjungi selama proses berlangsung dianggap tak terjangkau (unreachable) dan akan dihapus:

Kita juga bisa membayangkan proses tersebut sebagai menumpahkan ember cat dari roots, yang mengalir ke semua rujukkan dan menandai semua objek yang terjangkau. Yang tidak tertandai akan dihapus.

Itu merupakan konsep dari bagaimana cara kerja garbage collection. Engines JavaScript menerapkan banyak optimisasi untuk membuatnya berjalan lebih cepat dan tanpa mempengaruhi eksekusi.

Beberapa optimisasi:

  • Generational collection – objek-objek dibagi kedalam dua set: “yang baru” dan “yang lama”. Kebanyakan objek muncul, melakukan tugasnya dan mati dengan cepat, mereka dapat dibersihkan secara agresif. Mereka yang bertahan cukup lama, akan menjadi “yang lama” dan tak akan sering diperiksa.
  • Incremental collection – Jika terdapat banyak objek-objek, dan kita mencoba menapaki sambil menandai keseluruhan set objek sekaligus, itu dapat memakan waktu dan menimbulkan keterlambatan yang terlihat dalam eksekusi. Jadi engine akan mencoba untuk memecah proses garbage collection menjadi bagian-bagian kecil. Kemudian bagian-bagian kecil tersebut akan dieksekusi satu-persatu, secara terpisah. Itu memerlukan pencatatan ekstra diantara mereka untuk melacak perubahan, tetapi jadinya kta hanya mengalami keterlambatan kecil yang banyak daripada satuan yang besar.
  • Idle-time collectiongarbage collector akan mencoba untuk jalan hanya ketika CPU sedang idle, untuk mengurangi kemungkinan efek pada eksekusi.

Terdapat optimisasi-optimisasi dan tipe-tipe lain dari algoritma garbage collection. Sebesar apapun keinginan untuk menjelaskannya disini, harus kutahan, karena engines yang berbeda mengimplementasikan teknik dan tweaks yang berbeda pula. Dan, yang lebih penting, hal-hal tersebut akan berubah seiring dengan pengembangan engine, jadi mempelajarinya lebih dalam “di awal”, tanpa kebutuhan yang berarti mungkin akan sia-sia. Kecuali, tentu saja, jika itu merupakan murni masalah ketertarikan, maka ada beberapa tautan untukmu dibawah.

Ringkasan

Hal utama yang perlu diketahui:

  • Pengumpulan sampah (Garbage collection) dilakukan secara otomatis. Kita tidak bisa memaksa ataupun mencegahnya.
  • Objek-objek dipertahankan dalam memori selagi mereka terjangkau (reachable).
  • Menjadi yang dirujuk tidak sama dengan menjadi terjangkau (dari sebuah root): sekumpulan objek yang saling terkait dapat menjadi tak terjangkau sebagai keseluruhan.

Engine modern mengimplementasikan algoritma garbage collection canggih (advance).

Buku “The Garbage Collection Handbook: The Art of Automatic Memory Management” (R. Jones et al) mencakup beberapanya.

Jika kamu familiar dengan pemrograman low-level, informasi mendalam tentang garbage collector V8 terdapat pada artikel A tour of V8: Garbage Collection.

V8 blog juga mempublikasikan artikel-artikel tentang ubahan-ubahan dalam manajemen memori dari waktu ke waktu. Tentu saja, untuk belajar proses garbage collection, kamu lebih baik mempersiapkan diri dengan belajar tentang internals V8 secara umum dan membaca blog Vyacheslav Egorov yang merupakan salah seorang engineer V8. Saya bilang: “V8”, karena merupakan yang paling komprehensif di cover oleh artikel-artikel di internet. Untuk engine lainnya, pendekatannya kebanyakan mirip-mirip, tetapi garbage collection berbeda dalam banyak aspek.

Pengetahuan mendalam mengenai engines itu bagus ketika membutuhkan optimisasi low-level. Tapi akan lebih bijak untuk merencanakan itu sebagai langkah selanjutnya setelah kamu akrab dengan bahasanya (JavaScript).

Peta tutorial

komentar

baca ini sebelum berkomentar…
  • Jika Anda memiliki saran apa yang harus ditingkatkan - silakan kunjungi kirimkan Github issue atau pull request sebagai gantinya berkomentar.
  • Jika Anda tidak dapat memahami sesuatu dalam artikel – harap jelaskan.
  • Untuk menyisipkan beberapa kata kode, gunakan tag <code>, untuk beberapa baris – bungkus dengan tag <pre>, untuk lebih dari 10 baris – gunakan sandbox (plnkr, jsbin, < a href='http://codepen.io'>codepen…)