Memory leak bukanlah memori yang hilang. Itu memori yang masih milikmu, masih tercadang, tapi kamu sudah kehilangan kemampuan untuk mengembalikannya, karena pointer terakhir ke sana sudah tidak ada. Tidak ada yang crash. Program tetap berjalan, sedikit lebih berat setiap kali, sampai suatu saat ada yang gagal di tempat yang tidak berhubungan.
Halaman ini membahas bagaimana kebocoran muncul, disiplin yang mencegah sebagian besarnya, dan dua alat yang menemukan sisanya.
Seperti apa kebocoran itu
Setiap iterasi menimpa block dengan pointer baru. Blok sebelumnya masih teralokasi; tidak ada variabel yang menyimpan alamatnya; blok itu tidak akan pernah bisa dibebaskan. Tiga iterasi kehilangan dua belas kilobyte. Server yang melakukan ini sekali per request kehilangan memori itu selamanya, dengan laju sebanyak request yang datang.
Perbaikannya hanya satu baris, free(block); di akhir badan loop, tapi mengenali di mana baris itu seharusnya berada adalah keterampilan yang sebenarnya.
Bagaimana kebocoran terjadi
1. Pointer yang hilang
Penugasan apa pun ke pointer yang masih menyimpan satu-satunya referensi ke blok yang masih hidup akan membocorkan blok itu.
char *name = malloc(32);
name = malloc(64); /* the first 32 bytes are now unreachable */
Loop di atas adalah bug yang sama dalam bentuk loop. Begitu juga memberi nilai baru ke field struct, dan begitu juga singkatan realloc dari halaman calloc dan realloc:
p = realloc(p, n); /* on failure: p becomes NULL, old block orphaned */
2. Return lebih awal
Setiap jalur keluar dari sebuah fungsi harus melepaskan apa yang sudah diambil fungsi itu. Jalur yang terlupa selalu jalur error.
Jalur normalnya benar dan jalur error-nya bocor. Karena itulah bug ini lolos dari pengujian: cabang yang gagal hampir tidak pernah berjalan selama pengembangan. Perbaikannya adalah satu bagian pembersihan yang dituju oleh setiap jalur:
Inilah satu-satunya pemakaian goto yang justru disarankan programmer C berpengalaman. Cara ini berhasil karena setiap pointer diawali dengan NULL dan free(NULL) tidak melakukan apa-apa, jadi satu blok keluar sudah benar sejauh apa pun fungsinya berjalan.
3. Kepemilikan yang tidak jelas
Kebocoran yang paling halus sama sekali bukan kesalahan menulis kode: dua fungsi tidak sepakat tentang siapa yang seharusnya bertugas.
char *build_message(void); /* does the caller free this? */
void store(char *text); /* does store take ownership? */
Kalau build_message mengembalikan memori yang dialokasikan dan store menyalinnya, pemanggil harus membebaskannya. Kalau store menyimpan pointernya, pemanggil tidak boleh membebaskannya. Tidak ada apa pun di kode yang menyatakan yang mana, jadi salah satu asumsi itu terpakai dua kali, dan kamu mendapat kebocoran atau double free.
Obatnya adalah konvensi, yang ditulis di komentar di samping setiap fungsi yang mengalokasikan memori:
/* Returns a newly allocated string; the caller must free it. */
char *build_message(void);
/* Takes ownership of 'text'; it will be freed by store_free(). */
void store(char *text);
Tulis aturannya di fungsinya, bukan di dokumen desain. Inilah kebiasaan paling berharga dalam manajemen memori di C.
Disiplin kepemilikan
Empat aturan mencakup hampir semuanya:
- Setiap alokasi punya tepat satu pemilik: satu bagian kode yang bertanggung jawab membebaskannya.
- Pasangkan setiap fungsi yang mengalokasikan dengan fungsi yang melepaskan.
vec_init/vec_free,config_load/config_free. Simetri itu membuat pemanggilan yang hilang jadi kelihatan. - Bebaskan memori di lapisan yang sama dengan yang mengalokasikan, kecuali komentar fungsinya secara tegas memindahkan kepemilikan.
- Set pointer ke
NULLsetelah membebaskannya, supaya pemakaian tidak sengaja di kemudian hari langsung crash di tempat kesalahannya, bukan diam-diam merusak heap.
Menemukan kebocoran: valgrind
Di Linux, valgrind tidak butuh kompilasi ulang, meskipun simbol debug membuat laporannya lebih mudah dibaca:
gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program
Untuk loop yang bocor di awal halaman ini, laporannya diakhiri dengan kurang lebih seperti ini:
==12345== HEAP SUMMARY:
==12345== in use at exit: 12,000 bytes in 3 blocks
==12345== total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345== by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 12,000 bytes in 3 blocks
Baca dari bawah. "definitely lost" berarti tidak ada pointer ke blok itu saat program keluar: kebocoran sungguhan. Stack trace-nya menyebut baris malloc yang membuat blok itu, bukan baris tempat blok itu hilang, dan biasanya itu sudah cukup untuk menemukan free yang terlupa.
Ada dua kategori lain yang muncul:
- indirectly lost: blok yang hanya bisa dijangkau lewat blok yang sudah hilang, misalnya elemen-elemen linked list yang bocor. Perbaiki yang "definitely lost", dan kategori ini ikut hilang.
- still reachable: masih teralokasi saat keluar tapi masih ada pointer yang hidup, biasanya cache global. Bukan kebocoran dalam arti yang berbahaya, tapi layak dibebaskan supaya laporannya tetap kosong.
Valgrind juga menangkap pembacaan memori yang belum diinisialisasi dan penulisan melewati ujung blok, dan sering kali dari situlah kamu menemukan bug di balik kebocoran itu.
Menemukan kebocoran: AddressSanitizer
AddressSanitizer sudah ada di dalam GCC dan Clang, berjalan jauh lebih cepat daripada valgrind, dan bekerja di tempat valgrind tidak bisa (termasuk macOS versi sekarang):
gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program
Laporan kebocorannya otomatis dicetak saat program keluar:
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 12000 byte(s) in 3 object(s) allocated from:
#0 0x7f... in malloc
#1 0x1086... in main program.c:6
SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).
ASan juga mengubah use-after-free dan heap buffer overflow menjadi penghentian langsung yang labelnya jelas, bukan kerusakan misterius di kemudian hari. Build pengujianmu dengan ASan menyala; build rilis dengan ASan mati, karena ASan memakan memori dan kecepatan.
Kalau deteksi kebocoran tidak aktif di platformmu, set ASAN_OPTIONS=detect_leaks=1 di environment sebelum menjalankan program.
Memperbaiki program yang bocor langkah demi langkah
Ini program kecil dengan tiga kebocoran terpisah:
Valgrind melaporkan tiga catatan "definitely lost" dengan tiga nomor baris yang berbeda. Diperbaiki satu per satu:
Perbaikan 1 adalah komentar kepemilikan yang diwujudkan: shout mengalokasikan, main membebaskan. Perbaikan 2 menghapus alokasi ganda sepenuhnya, bukan membebaskan yang pertama: kode yang lebih sederhana juga kode yang benar. Perbaikan 3 menambahkan free yang terlupa di return lebih awal; kalau alokasinya lebih banyak, satu label cleanup: yang ditunjukkan sebelumnya lebih mudah diperbesar daripada mengulang pemanggilan free.
Kebiasaan yang mencegah kebocoran
- Tulis
freesegera setelah menulismalloc, lalu isi kode di antaranya. - Beri setiap fungsi yang mengalokasikan sebuah fungsi pembebas yang sepadan.
- Nyatakan kepemilikan di komentar pada setiap fungsi yang mengembalikan atau menerima pointer yang dialokasikannya.
- Pakai satu blok keluar
cleanup:di fungsi yang memegang beberapa alokasi. - Jalankan pengujianmu dengan
-fsanitize=addresssebagai kebiasaan, bukan hanya saat ada yang kelihatan salah. - Anggap "definitely lost: 0 bytes" sebagai bagian dari pengujian yang lulus.
Pertanyaan yang sering diajukan
Apa itu memory leak di C?
Memory leak adalah memori yang kamu alokasikan dengan malloc tapi tidak bisa lagi kamu bebaskan, karena tidak ada lagi bagian program yang menunjuk ke sana. Blok itu tetap tercadang selama proses berjalan. Ini bukan crash: program tetap bekerja, hanya saja memakai lebih banyak memori di setiap putaran sampai akhirnya kehabisan.
Bagaimana cara menemukan memory leak di C?
Jalankan program di bawah valgrind: valgrind --leak-check=full ./program. Valgrind melaporkan setiap blok yang masih teralokasi saat program keluar, lengkap dengan stack trace dari malloc yang membuatnya. Di macOS atau di tempat valgrind tidak tersedia, kompilasi dengan -fsanitize=address dan laporan yang sama akan muncul saat program keluar.
Apa penyebab memory leak di C?
Tiga pola mencakup hampir semuanya: menimpa satu-satunya pointer ke sebuah blok (termasuk p = realloc(p, n) saat gagal), return lebih awal dari fungsi yang sudah mengalokasikan memori, dan kepemilikan yang tidak jelas: dua fungsi sama-sama mengira yang lain akan membebaskannya, sehingga tidak ada yang melakukannya.
Apakah memory leak tetap penting kalau programnya toh akan keluar?
Untuk program yang berjalan sekali lalu keluar, sistem operasi mengambil kembali semuanya, jadi dampak praktisnya nol. Kebocoran jadi penting untuk apa pun yang berjalan lama, seperti server, game loop, atau daemon, di mana satu kebocoran per request terus bertambah tanpa batas. Tetap bebaskan memori secara konsisten: laporan kebocoran yang tidak penting hanya jadi gangguan yang menyembunyikan kebocoran yang penting.