Model memori

Model memori Python berupa referensi, garbage collection, __slots__, dan weakref.

Daftar isi

Model memori Python berangkat dari dua hal sederhana, yaitu objek yang tinggal di heap dan nama yang hanya berupa referensi kepadanya. Topik ini membahas bagaimana objek hidup dan mati pada CPython, mulai dari penghitungan referensi, garbage collection untuk siklus referensi, pengukuran ukuran objek, penghematan dengan __slots__, hingga referensi lemah (weak reference) yang diakses lewat modul weakref. Pemahaman ini menjelaskan dari mana konsumsi memori program berasal dan bagaimana mengendalikannya.

Referensi dan masa hidup objek#

Setiap objek CPython membawa penghitung referensi. Menempelkan nama, memasukkan objek ke list, atau menjadikannya argumen menambah hitungan, sedangkan melepas nama dengan del, keluar dari lingkup, atau menghapus elemen menguranginya. Apabila hitungan mencapai nol, CPython membebaskan objeknya seketika, dan perilaku ini dapat diamati lewat metode __del__ yang dipanggil saat objek dimusnahkan.

python Contoh: pembebasan saat penghitung mencapai nol
class Laporan:
    def __del__(self):
        print("objek dibebaskan")

x = Laporan()
print("sebelum del")
del x
print("sesudah del")
text Keluaran
sebelum del
objek dibebaskan
sesudah del

Urutan keluaran menunjukkan pembebasan terjadi tepat ketika del melepas satu-satunya nama, sebelum baris berikutnya berjalan. Kepastian semacam ini milik CPython dan bukan janji bahasa Python secara umum, sehingga kode yang bergantung pada waktu pemanggilan __del__ tidak portabel antar implementasi Python.

Penghitungan referensi menjelaskan pula mengapa konsumsi memori program naik turun mengikuti struktur data yang aktif. List yang memuat jutaan entri menahan jutaan referensi, dan melepas listnya melepas seluruh referensi itu dalam satu langkah, meskipun objek yang dirujuknya mungkin masih dipegang pihak lain. Dari sini terlihat bahwa melepas wadah dan melepas isinya adalah dua kejadian yang berbeda.

Siklus referensi dan modul gc#

Penghitungan referensi mati langkah pada siklus, yaitu dua objek yang saling menunjuk. Setiap objeknya masih memiliki satu referensi meski tidak ada yang dapat menjangkaunya dari program, sehingga hitungannya tidak pernah mencapai nol. Modul gc menambal celah itu dengan garbage collection generasi, yaitu objek dikelompokkan ke tiga generasi dan diperiksa berkala ketika jumlah alokasi melewati ambang tertentu.

python Contoh: ambang pemicu garbage collection
import gc

print(gc.get_threshold())
print(gc.isenabled())
text Keluaran
(700, 10, 10)
True

Nilai (700, 10, 10) adalah ambang alokasi untuk generasi 0, 1, dan 2, dan koleksi generasi berikutnya dipicu sepuluh kali lebih jarang per tingkatnya. Fungsi gc.collect() dapat dipanggil secara manual dan mengembalikan jumlah objek yang terkumpul, angka yang bervariasi antar sesi sehingga tidak dicetak di sini. Contoh berikut menunjukkan siklus yang dikumpulkan, dengan referensi lemah sebagai alat pengamatan.

python Contoh: siklus referensi yang dikumpulkan gc
import gc
import weakref

class Node:
    def __init__(self):
        self.pasangan = None

a = Node()
b = Node()
a.pasangan = b
b.pasangan = a
wr = weakref.ref(a)
del a, b
print(wr() is not None)
gc.collect()
print(wr() is None)
text Keluaran
True
True

Setelah kedua nama dihapus, siklus masih hidup karena kedua objeknya saling menunjuk, dan referensi lemah wr masih dapat memanggil objeknya. Setelah gc.collect() berjalan, siklus dikumpulkan dan wr() mengembalikan None. Referensi lemah dipakai di sini karena tidak ikut menahan hidupnya objek yang diamati.

Mengukur ukuran objek#

Fungsi sys.getsizeof() melaporkan ukuran objek dalam byte pada tingkat objek itu sendiri. Ukuran objek yang dirujuknya tidak ikut dihitung, dan angkanya spesifik pada versi serta arsitektur interpreter, sehingga nilai di mesin lain dapat berbeda. Karena itu angka berikut dibaca sebagai perbandingan relatif antar objek, bukan sebagai konstanta.

python Contoh: ukuran beberapa objek
import sys

print(sys.getsizeof(0))
print(sys.getsizeof(""))
print(sys.getsizeof([]))
print(sys.getsizeof(list(range(100))))
print(sys.getsizeof((1, 2, 3)))
text Keluaran
28
41
56
856
64

List kosong memakai 56 byte untuk strukturnya, dan list seratus bilangan memakai 856 byte karena Python menyediakan kapasitas lebih agar append() tidak sering mengalokasikan ulang. Tuple tiga elemen lebih ringkas daripada list tiga elemen karena ukurannya tidak perlu menyisakan ruang tumbuh. Seluruh selisih di atas memperlihatkan biaya struktur, sementara biaya elemennya dihitung terpisah seperti pada bagian Kesalahan umum. Perbandingan semacam ini berguna saat memilih struktur data untuk data berukuran besar, misalnya tuple untuk koleksi tetap yang tidak memerlukan ruang tumbuh.

slots untuk instans berjumlah besar#

Secara bawaan, instans kelas membawa __dict__, yaitu dictionary kecil yang menyimpan atributnya dan memberi keleluasaan menambah atribut kapan saja. Deklarasi __slots__ mengganti dictionary itu dengan daftar nama tetap, sehingga memori per instans menyusut dan penghematannya terasa untuk kelas yang diinstansiasi dalam jumlah besar.

python Contoh: biaya instans dengan dan tanpa __slots__
import sys

class TitikBebas:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class TitikSlots:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y

a = TitikBebas(1, 2)
b = TitikSlots(1, 2)
print(sys.getsizeof(a), sys.getsizeof(a.__dict__))
print(sys.getsizeof(b))
text Keluaran
48 296
48

Instans biasa memakai 48 byte untuk objeknya ditambah 296 byte untuk __dict__-nya, sedangkan instans slots cukup 48 byte. Selisihnya tampak kecil per objek, tetapi dikalikan jutaan instans, hasilnya berarti. Konsekuensinya, penambahan atribut di luar daftar menghasilkan error, dan rantai pewarisan perlu dijaga agar __dict__ tidak kembali hadir pada subkelas. Pemanfaatan slots paling terasa pada objek yang berumur panjang dan berjumlah besar, misalnya jutaan baris data yang dimuat ke memori. Aturan praktisnya tetap sama, yaitu mengukur konsumsi memori dahulu sebelum memilih slots.

Weakref untuk cache#

Cache yang menyimpan objek besar dalam dictionary biasa ikut menahan objeknya hidup selama entrinya ada. Referensi lemah menunjuk objek tanpa menambah penghitung referensinya, sehingga objek tetap dapat dibebaskan apabila tidak ada lagi referensi kuat yang menunjuknya. Fungsi weakref.ref() mengembalikan objek yang dirujuk, atau None apabila objek itu sudah musnah.

python Contoh: referensi lemah tidak menahan objek
import weakref

class Data:
    pass

obj = Data()
wr = weakref.ref(obj)
print(wr() is obj)
del obj
print(wr() is None)
text Keluaran
True
True

Referensi lemah berhasil memanggil objek selama objeknya masih hidup, dan setelah satu-satunya nama dihapus, referensi lemah melaporkan None. Modul weakref juga menyediakan WeakValueDictionary, yaitu dictionary yang entrinya hilang sendiri apabila nilainya musnah, sehingga cocok sebagai cache yang mengikuti pemakaian yang sebenarnya. Satu batasan yang perlu diingat, yaitu sebagian tipe bawaan seperti list dan dictionary tidak mendukung referensi lemah secara langsung, sedangkan subkelasnya mendukung, sehingga cache biasanya menyimpan instans kelas buatan sendiri.

Kesalahan umum#

Kesalahan pertama adalah menganggap del membebaskan objek. Pernyataan itu hanya melepas satu nama, dan objek hanya musnah apabila hitungan referensinya mencapai nol. Apabila masih ada nama lain, elemen list, atau siklus yang menunjuknya, objek tetap hidup, dan pada siklus pembebasannya menunggu garbage collection berjalan.

Kesalahan kedua adalah membaca sys.getsizeof() seolah menghitung seluruh isi. Pengukurannya dangkal, sehingga ukuran list harus dijumlahkan dengan ukuran elemen-elemennya apabila konsumsi yang sebenarnya ingin diketahui.

python Contoh: ukuran list beserta elemennya
import sys

def ukuran_total(objek):
    total = sys.getsizeof(objek)
    if isinstance(objek, list):
        total += sum(sys.getsizeof(i) for i in objek)
    return total

data = list(range(100))
print(sys.getsizeof(data))
print(ukuran_total(data))
text Keluaran
856
3656

List sendirinya 856 byte, sedangkan totalnya bersama seratus elemen bilangan mencapai 3656 byte. Selisih yang jauh ini menegaskan bahwa getsizeof() hanya mengukur objeknya sendiri. Kesalahan ketiga adalah menampung hasil perhitungan besar dalam cache tanpa batas, yang membuat memori tumbuh tanpa henti, dan jawabannya adalah penyimpanan berbasis referensi lemah, batas ukuran, atau strategi pembuangan entri yang tua.

Ringkasan#

  • CPython membebaskan objek tepat saat penghitung referensinya mencapai nol, dan del hanya melepas nama, bukan memusnahkan objek.
  • Siklus referensi tidak terjangkau penghitungan referensi dan dikumpulkan garbage collection generasi dengan ambang bawaan (700, 10, 10).
  • sys.getsizeof() mengukur objeknya saja, sehingga ukuran kontainer perlu dijumlahkan dengan isinya.
  • __slots__ mengganti __dict__ instans dengan daftar nama tetap untuk menghemat memori, sedangkan referensi lemah menyediakan pemantauan dan cache yang tidak menahan hidup objeknya.

Referensi#