Tip Java 130: Apakah Anda tahu ukuran data Anda?

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 Runtimemetode 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 menggunakan Runtime.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 karena Runtime.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

Sizeofmetode 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 heap1dan heap2untuk membuat instance apa pun yang menarik.

Perhatikan juga cara Sizeofmencetak ukuran objek: penutupan transitif dari data yang dibutuhkan oleh semua countinstance 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 Objectmembutuhkan 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 intske dalam Integerinstance 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 intnilai dapat masuk ke dalam hanya 4 byte tambahan. Menggunakan Integerbiaya overhead memori 300 persen saya dibandingkan ketika saya dapat menyimpan nilai sebagai tipe primitif.

java.lang.Long

Longharus 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 Longadalah 8 byte Object, ditambah 8 byte lebih banyak untuk nilai panjang sebenarnya. Sebaliknya, Integermemiliki 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 intarray:

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 chararray:

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 Objectoverhead 8-byte yang tak terhindarkan , array primitif menambahkan 8 byte lainnya (dari mana setidaknya 4 byte mendukung lengthbidang). Dan penggunaan int[1]tampaknya tidak menawarkan keuntungan memori apa pun dibandingkan sebuah Integerinstance, 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 Strings with content, I need a helper method to create Strings 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 Strings 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, Strings dapat mengkonsumsi lebih banyak memori daripada yang disarankan panjangnya: Strings dihasilkan dari StringBuffers (baik secara eksplisit atau melalui operator penggabungan '+') kemungkinan memiliki chararray dengan panjang lebih besar dari Stringpanjang yang dilaporkan karena StringBuffers biasanya dimulai dengan kapasitas 16 , lalu gandakan pada append()operasi. Jadi, misalnya, createString(1) + ' 'berakhir dengan charlarik berukuran 16, bukan 2.

Apa yang kita lakukan?

"Ini semua baik-baik saja, tapi kami tidak punya pilihan selain menggunakan Strings dan tipe lain yang disediakan oleh Java, bukan?" Saya mendengar Anda bertanya. Mari kita cari tahu.

Kelas pembungkus