Topik ini menjelaskan apa yang terjadi antara kode sumber C# yang ditulis dan program yang berjalan. Pembahasan mencakup peran CLR, bentuk IL, kompilasi JIT, serta model memori terkelola yang dirawat oleh garbage collector. Setelah membacanya, eksekusi program C# dapat dibayangkan sebagai rangkaian tahap yang jelas, dari kode sumber sampai kode mesin.
Dari kode sumber ke IL#
Kompilator C# tidak mengubah kode sumber langsung menjadi kode mesin. Keluarannya adalah berkas .dll yang berisi Intermediate Language (IL), yakni bahasa perantara milik .NET yang tidak terikat pada arsitektur prosesor tertentu, beserta metadata, yaitu deskripsi tipe dan anggotanya. Kompilator yang disertakan dalam SDK .NET bernama csc, dengan implementasi modern yang dikenal sebagai Roslyn. IL inilah yang disimpan dan didistribusikan, sehingga berkas keluaran yang sama dapat berjalan di Windows, Linux, maupun macOS selama runtime .NET tersedia di tempat program itu dieksekusi.
Bentuk IL dapat dibayangkan sebagai instruksi tingkat rendah yang mirip assembly (bahasa tingkat rendah milik satu keluarga prosesor). Satu pernyataan C# biasanya menjadi beberapa instruksi IL, seperti pada potongan berikut untuk satu panggilan cetak:
ldstr "Hello, World!"
call void [System.Console]System.Console::WriteLine(string)
retTiga instruksi di atas memuat konstanta string ke tumpukan evaluasi, memanggil metode WriteLine, lalu kembali dari metode. IL tidak pernah ditulis secara manual dalam pekerjaan sehari-hari, tetapi keberadaannya perlu dikenali karena seluruh mekanisme .NET, termasuk runtime dan debugger, bekerja di atas bahasa perantara ini.
CLR dan JIT#
Berkas IL tidak berjalan sendiri. Ia dimuat oleh Common Language Runtime (CLR), yaitu runtime .NET yang mengurus eksekusi program: memuat berkas keluaran, memverifikasi keamanan IL, menyediakan sistem tipe yang seragam, dan mengelola memori. Karena semua bahasa .NET menargetkan CLR yang sama, layanan-layanan tersebut dinikmati secara seragam tanpa memandang bahasa asal kode sumbernya.
Saat sebuah metode dipanggil untuk pertama kalinya, CLR menerjemahkan IL metode tersebut menjadi kode mesin untuk prosesor yang sedang digunakan. Proses ini disebut kompilasi just-in-time (JIT), yaitu kompilasi yang dilakukan tepat pada saat program berjalan, bukan sebelumnya. Hasil terjemahan disimpan di memori, sehingga panggilan berikutnya ke metode yang sama tidak perlu diterjemahkan ulang. Pola dua tahap inilah yang membuat IL tetap portabel sekaligus eksekusi berjalan pada kecepatan kode mesin.
Diagram berikut merangkum seluruh alur, mulai dari kode sumber hingga kode mesin, beserta pembagian tahap pada waktu build dan waktu eksekusi.
Memori terkelola dan garbage collector#
Selama program berjalan, CLR menyediakan memori yang disebut memori terkelola (managed memory). Objek yang dibuat program dialokasikan pada heap (area memori untuk objek) yang dikelola runtime, dan program tidak membebaskan memorinya secara manual seperti pada bahasa C. Pekerjaan pembebasan itu diserahkan kepada garbage collector (GC), yaitu proses otomatis yang mengenali objek yang tidak lagi dirujuk dan mereklamasi memorinya.
Cara kerja dasarnya dapat diringkas dalam dua langkah. Pertama, GC menandai semua objek yang masih dapat dicapai dari variabel yang sedang hidup. Kedua, objek yang tidak tertanda dianggap sampah dan memorinya dilepaskan. Karena keputusan pembersihan diambil oleh GC, waktu pembersihan tidak dapat diprediksi secara pasti dari kode program. GC juga mengelompokkan objek ke dalam generasi agar objek berumur pendek diperiksa lebih sering, sebuah optimasi yang rinciannya berada di luar lingkup topik ini.
Program berikut membuat objek lalu mengamatinya melalui referensi lemah (weak reference), yakni rujukan yang tidak menjaga objek tetap hidup. Objek dibuat di dalam fungsi lokal agar setelah fungsi berakhir tidak ada lagi variabel biasa yang merujuknya, kemudian GC dipanggil secara eksplisit supaya pembersihannya dapat diamati:
WeakReference referensiLemah = BuatCatatan();
Console.WriteLine($"Objek Catatan masih dirujuk: {referensiLemah.IsAlive}");
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Console.WriteLine($"Objek Catatan masih dirujuk: {referensiLemah.IsAlive}");
WeakReference BuatCatatan()
{
Catatan catatan = new Catatan { Isi = "catatan singkat" };
return new WeakReference(catatan);
}
class Catatan
{
public string Isi = "";
}Objek Catatan masih dirujuk: True
Objek Catatan masih dirujuk: FalsePada pemeriksaan pertama, objek Catatan belum pernah diperiksa oleh GC sejak dibuat, sehingga IsAlive bernilai True. Setelah GC.Collect berjalan, tidak ada lagi variabel hidup yang merujuk objek tersebut, sehingga GC mereklamasi memorinya dan IsAlive berubah menjadi False. Perubahan dari True ke False itulah bukti bahwa pembebasan memori terjadi tanpa satu baris kode pembebasan pun yang ditulis.
Kesalahan umum#
- Menganggap GC berjalan seketika setelah objek tidak dirujuk. Pembersihan terjadi ketika GC memutuskan waktunya, bukan pada baris tempat referensi terakhir hilang, dan jarak waktunya bisa mencapai detik atau lebih. Karena itu, penutupan berkas dan koneksi jaringan tetap ditangani secara eksplisit oleh kode program dan tidak diserahkan kepada GC.
- Mengira berkas .dll buatan C# berisi kode mesin. Berkas keluaran berisi IL dan hanya dapat dijalankan bila runtime .NET terpasang. Kekeliruan ini muncul ketika program yang sudah dibangun disalin ke komputer tanpa .NET, lalu langsung menampilkan error saat dijalankan, padahal programnya sendiri tidak salah.
- Menyamakan IL dengan kode mesin. IL portabel dan belum terikat prosesor, sedangkan kode mesin hanya sah untuk satu arsitektur. Jembatan di antara keduanya adalah kompilasi JIT yang dikerjakan CLR, sehingga satu berkas IL cukup untuk prosesor Intel, AMD, maupun ARM.
Ringkasan#
- Kompilator C# (csc) mengubah kode sumber menjadi berkas .dll yang berisi IL dan metadata.
- CLR memuat IL, lalu JIT menerjemahkannya menjadi kode mesin per metode saat metode tersebut pertama kali dipanggil.
- Objek dialokasikan pada memori terkelola dan dibersihkan oleh garbage collector tanpa kode pembebasan manual.
- Waktu pembersihan oleh GC tidak deterministik, sehingga penutupan sumber daya non-memori tetap ditangani secara eksplisit.
Referensi#
- Managed Execution Process — Microsoft — Penjelasan resmi tahap kompilasi ke IL dan eksekusi oleh CLR.
- .NET garbage collection — Microsoft — Dokumentasi resmi cara kerja garbage collector .NET.