Baru-baru ini, saya membantu merancang aplikasi server Java yang menyerupai database dalam memori. Artinya, kami mencondongkan desain ke dalam cache banyak data dalam memori untuk memberikan kinerja kueri super cepat.
Setelah prototipe kami berjalan, kami secara alami memutuskan untuk membuat profil jejak memori data setelah diurai dan dimuat dari disk. Namun, hasil awal yang tidak memuaskan mendorong saya untuk mencari penjelasan.
Catatan: Anda dapat mengunduh kode sumber artikel ini dari Sumber.
Alat
Karena Java dengan sengaja menyembunyikan banyak aspek manajemen memori, menemukan berapa banyak memori yang dikonsumsi objek Anda memerlukan beberapa pekerjaan. Anda dapat menggunakan Runtime.freeMemory()
metode ini untuk mengukur perbedaan ukuran heap sebelum dan setelah beberapa objek dialokasikan. Beberapa artikel, seperti "Pertanyaan Minggu Ini No. 107" dari Ramchander Varadarajan (Sun Microsystems, September 2000) dan "Memory Matters" Tony Sintes ( JavaWorld, Desember 2001), merinci gagasan itu. Sayangnya, solusi artikel sebelumnya gagal karena penerapannya menggunakan Runtime
metode yang salah , sedangkan solusi artikel terakhir memiliki kekurangannya sendiri:
- Satu panggilan
Runtime.freeMemory()
terbukti tidak cukup karena JVM dapat memutuskan untuk meningkatkan ukuran heap saat ini kapan saja (terutama saat menjalankan pengumpulan sampah). Kecuali jika ukuran heap total sudah berada pada ukuran maksimum -Xmx, kita harus menggunakanRuntime.totalMemory()-Runtime.freeMemory()
ukuran heap bekas. - Menjalankan satu
Runtime.gc()
panggilan mungkin tidak terbukti cukup agresif untuk meminta pengumpulan sampah. Kita bisa, misalnya, meminta finalizer objek untuk dijalankan juga. Dan karenaRuntime.gc()
tidak didokumentasikan untuk diblokir hingga pengumpulan selesai, sebaiknya tunggu hingga ukuran heap yang dirasakan stabil. - Jika kelas yang diprofilkan membuat data statis sebagai bagian dari inisialisasi per kelasnya (termasuk kelas statis dan penginisialisasi kolom), memori heap yang digunakan untuk instance kelas pertama mungkin menyertakan data tersebut. Kita harus mengabaikan ruang heap yang digunakan oleh instance kelas pertama.
Mempertimbangkan masalah-masalah itu, saya menyajikan Sizeof
, alat yang saya gunakan untuk mengintip berbagai kelas inti dan aplikasi Java:
kelas publik Sizeof {public static void main (String [] args) melempar Exception {// Pemanasan semua kelas / metode kita akan menggunakan runGC (); usedMemory (); // Larik untuk mempertahankan referensi yang kuat ke objek yang dialokasikan final int count = 100000; Objek [] objek = Objek baru [hitungan]; panjang heap1 = 0; // Alokasikan count + 1 objek, buang yang pertama untuk (int i = -1; i = 0) objek [i] = objek; lain {object = null; // Buang objek pemanasan runGC (); heap1 = usedMemory (); // Ambil snapshot sebelum heap}} runGC (); panjang heap2 = usedMemory (); // Ambil snapshot setelah heap: final int size = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("'sebelum' heap:" + heap1 + ", 'after' heap:" + heap2); System.out.println ("delta heap:" + (heap2 - heap1) + ", {" + objek [0].getClass () + "} size =" + size + "bytes"); untuk (int i = 0; i <count; ++ i) objek [i] = null; objek = null; } private static void runGC () throws Exception {// Memanggil Runtime.gc () // menggunakan beberapa pemanggilan metode: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () melempar Exception {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; untuk (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Waktu Proses akhir statis pribadi s_runtime = Runtime.getRuntime (); } // Akhir kelassaya <menghitung; ++ i) objek [i] = null; objek = null; } private static void runGC () throws Exception {// Memanggil Runtime.gc () // menggunakan beberapa pemanggilan metode: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () melempar Exception {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; untuk (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Waktu Proses akhir statis pribadi s_runtime = Runtime.getRuntime (); } // Akhir kelassaya <menghitung; ++ i) objek [i] = null; objek = null; } private static void runGC () throws Exception {// Memanggil Runtime.gc () // menggunakan beberapa pemanggilan metode: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () melempar Exception {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; untuk (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Waktu Proses akhir statis pribadi s_runtime = Runtime.getRuntime (); } // Akhir kelasgc () // menggunakan beberapa pemanggilan metode: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () melempar Exception {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; untuk (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Waktu Proses akhir statis pribadi s_runtime = Runtime.getRuntime (); } // Akhir kelasgc () // menggunakan beberapa pemanggilan metode: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () melempar Exception {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; untuk (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Waktu Proses akhir statis pribadi s_runtime = Runtime.getRuntime (); } // Akhir kelasThread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Waktu Proses akhir statis pribadi s_runtime = Runtime.getRuntime (); } // Akhir kelasThread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Waktu Proses akhir statis pribadi s_runtime = Runtime.getRuntime (); } // Akhir kelas
Sizeof
metode kunci adalah runGC()
dan usedMemory()
. Saya menggunakan runGC()
metode pembungkus untuk memanggil _runGC()
beberapa kali karena tampaknya membuat metode lebih agresif. (Saya tidak yakin mengapa, tetapi mungkin saja membuat dan menghancurkan bingkai tumpukan panggilan metode menyebabkan perubahan dalam kumpulan akar reachability dan meminta pengumpul sampah untuk bekerja lebih keras. Selain itu, memakan sebagian besar ruang heap untuk membuat pekerjaan yang cukup agar pengumpul sampah menendang juga membantu. Secara umum, sulit untuk memastikan semuanya dikumpulkan. Detail pastinya bergantung pada JVM dan algoritme pengumpulan sampah.)
Perhatikan baik-baik tempat saya berdoa runGC()
. Anda dapat mengedit kode antara deklarasi heap1
dan heap2
untuk membuat instance apa pun yang menarik.
Perhatikan juga cara Sizeof
mencetak ukuran objek: penutupan transitif dari data yang dibutuhkan oleh semua count
instance kelas, dibagi count
. Untuk sebagian besar kelas, hasilnya adalah memori yang digunakan oleh satu instance kelas, termasuk semua kolom miliknya. Nilai footprint memori tersebut berbeda dengan data yang disediakan oleh banyak profiler komersial yang melaporkan footprint memori yang dangkal (misalnya, jika suatu objek memiliki int[]
kolom, konsumsi memorinya akan muncul secara terpisah).
Hasil
Mari terapkan alat sederhana ini ke beberapa kelas, lalu lihat apakah hasilnya sesuai dengan harapan kita.
Catatan: Hasil berikut didasarkan pada Sun's JDK 1.3.1 untuk Windows. Karena apa yang dijamin dan tidak dijamin oleh bahasa Java dan spesifikasi JVM, Anda tidak dapat menerapkan hasil khusus ini ke platform lain atau implementasi Java lainnya.
java.lang.Object
Jadi, root dari semua objek harus menjadi kasus pertama saya. Untuk java.lang.Object
, saya mendapatkan:
'before' heap: 510696, 'after' heap: 1310696 heap delta: 800000, ukuran {class java.lang.Object} = 8 byte
Jadi, sebuah dataran Object
membutuhkan 8 byte; tentu saja, tidak ada yang harus mengharapkan ukuran menjadi 0, karena setiap contoh harus membawa sekitar bidang yang mendukung operasi dasar seperti equals()
, hashCode()
, wait()/notify()
, dan sebagainya.
java.lang.Integer
Kolega saya dan saya sering menggabungkan native ints
ke dalam Integer
instance sehingga kami dapat menyimpannya di koleksi Java. Berapa biayanya untuk kita mengingat?
'before' heap: 510696, 'after' heap: 2110696 heap delta: 1600000, ukuran {class java.lang.Integer} = 16 byte
Hasil 16-byte sedikit lebih buruk dari yang saya harapkan karena sebuah int
nilai dapat masuk ke dalam hanya 4 byte tambahan. Menggunakan Integer
biaya overhead memori 300 persen saya dibandingkan ketika saya dapat menyimpan nilai sebagai tipe primitif.
java.lang.Long
Long
harus mengambil lebih banyak memori daripada Integer
, tetapi tidak:
'before' heap: 510696, 'after' heap: 2110696 heap delta: 1600000, ukuran {class java.lang.Long} = 16 byte
Jelasnya, ukuran objek sebenarnya di heap tunduk pada penyelarasan memori tingkat rendah yang dilakukan oleh implementasi JVM tertentu untuk jenis CPU tertentu. Sepertinya overhead Long
adalah 8 byte Object
, ditambah 8 byte lebih banyak untuk nilai panjang sebenarnya. Sebaliknya, Integer
memiliki lubang 4-byte yang tidak terpakai, kemungkinan besar karena JVM I menggunakan penyelarasan kekuatan objek pada batas kata 8-byte.
Array
Bermain dengan array tipe primitif terbukti instruktif, sebagian untuk menemukan overhead tersembunyi dan sebagian untuk membenarkan trik populer lainnya: membungkus nilai primitif dalam array ukuran-1 untuk menggunakannya sebagai objek. Dengan memodifikasi Sizeof.main()
agar memiliki loop yang menambah panjang array yang dibuat pada setiap iterasi, saya mendapatkan int
array:
panjang: 0, {kelas [I} ukuran = 16 byte panjang: 1, {kelas [I} ukuran = 16 byte panjang: 2, {kelas [I} ukuran = 24 byte panjang: 3, {kelas [I} ukuran = Panjang 24 byte: 4, {class [I} ukuran = 32 panjang byte: 5, {class [I} ukuran = 32 panjang byte: 6, {class [I} ukuran = 40 panjang byte: 7, {class [I} size = 40 byte panjang: 8, {class [I} size = 48 byte length: 9, {class [I} size = 48 byte length: 10, {class [I} size = 56 byte
dan untuk char
array:
panjang: 0, {kelas [C} ukuran = 16 byte panjang: 1, {kelas [C} ukuran = 16 byte panjang: 2, {kelas [C} ukuran = 16 byte panjang: 3, {kelas [C} ukuran = Panjang 24 byte: 4, {class [C} ukuran = 24 byte panjang: 5, {class [C} ukuran = 24 byte panjang: 6, {class [C} ukuran = 24 byte panjang: 7, {class [C} size = 32 byte panjang: 8, {class [C} size = 32 byte panjang: 9, {class [C} size = 32 byte panjang: 10, {class [C} size = 32 byte
Di atas, bukti penyelarasan 8-byte muncul lagi. Selain itu, selain Object
overhead 8-byte yang tak terhindarkan , array primitif menambahkan 8 byte lainnya (dari mana setidaknya 4 byte mendukung length
bidang). Dan penggunaan int[1]
tampaknya tidak menawarkan keuntungan memori apa pun dibandingkan sebuah Integer
instance, kecuali mungkin sebagai versi yang dapat berubah dari data yang sama.
Array multidimensi
Multidimensional arrays offer another surprise. Developers commonly employ constructs like int[dim1][dim2]
in numerical and scientific computing. In an int[dim1][dim2]
array instance, every nested int[dim2]
array is an Object
in its own right. Each adds the usual 16-byte array overhead. When I don't need a triangular or ragged array, that represents pure overhead. The impact grows when array dimensions greatly differ. For example, a int[128][2]
instance takes 3,600 bytes. Compared to the 1,040 bytes an int[256]
instance uses (which has the same capacity), 3,600 bytes represent a 246 percent overhead. In the extreme case of byte[256][1]
, the overhead factor is almost 19! Compare that to the C/C++ situation in which the same syntax does not add any storage overhead.
java.lang.String
Let's try an empty String
, first constructed as new String()
:
'before' heap: 510696, 'after' heap: 4510696 heap delta: 4000000, {class java.lang.String} size = 40 bytes
The result proves quite depressing. An empty String
takes 40 bytes—enough memory to fit 20 Java characters.
Before I try String
s with content, I need a helper method to create String
s guaranteed not to get interned. Merely using literals as in:
object = "string with 20 chars";
will not work because all such object handles will end up pointing to the same String
instance. The language specification dictates such behavior (see also the java.lang.String.intern()
method). Therefore, to continue our memory snooping, try:
public static String createString (final int length) { char [] result = new char [length]; for (int i = 0; i < length; ++ i) result [i] = (char) i; return new String (result); }
After arming myself with this String
creator method, I get the following results:
length: 0, {class java.lang.String} size = 40 bytes length: 1, {class java.lang.String} size = 40 bytes length: 2, {class java.lang.String} size = 40 bytes length: 3, {class java.lang.String} size = 48 bytes length: 4, {class java.lang.String} size = 48 bytes length: 5, {class java.lang.String} size = 48 bytes length: 6, {class java.lang.String} size = 48 bytes length: 7, {class java.lang.String} size = 56 bytes length: 8, {class java.lang.String} size = 56 bytes length: 9, {class java.lang.String} size = 56 bytes length: 10, {class java.lang.String} size = 56 bytes
The results clearly show that a String
's memory growth tracks its internal char
array's growth. However, the String
class adds another 24 bytes of overhead. For a nonempty String
of size 10 characters or less, the added overhead cost relative to useful payload (2 bytes for each char
plus 4 bytes for the length), ranges from 100 to 400 percent.
Of course, the penalty depends on your application's data distribution. Somehow I suspected that 10 characters represents the typical String
length for a variety of applications. To get a concrete data point, I instrumented the SwingSet2 demo (by modifying the String
class implementation directly) that came with JDK 1.3.x to track the lengths of the String
s it creates. After a few minutes playing with the demo, a data dump showed that about 180,000 Strings
were instantiated. Sorting them into size buckets confirmed my expectations:
[0-10]: 96481 [10-20]: 27279 [20-30]: 31949 [30-40]: 7917 [40-50]: 7344 [50-60]: 3545 [60-70]: 1581 [70-80]: 1247 [80-90]: 874 ...
That's right, more than 50 percent of all String
lengths fell into the 0-10 bucket, the very hot spot of String
class inefficiency!
Pada kenyataannya, String
s dapat mengkonsumsi lebih banyak memori daripada yang disarankan panjangnya: String
s dihasilkan dari StringBuffer
s (baik secara eksplisit atau melalui operator penggabungan '+') kemungkinan memiliki char
array dengan panjang lebih besar dari String
panjang yang dilaporkan karena StringBuffer
s biasanya dimulai dengan kapasitas 16 , lalu gandakan pada append()
operasi. Jadi, misalnya, createString(1) + ' '
berakhir dengan char
larik berukuran 16, bukan 2.
Apa yang kita lakukan?
"Ini semua baik-baik saja, tapi kami tidak punya pilihan selain menggunakan String
s dan tipe lain yang disediakan oleh Java, bukan?" Saya mendengar Anda bertanya. Mari kita cari tahu.