Petua Java 130: Adakah anda tahu ukuran data anda?

Baru-baru ini, saya membantu merancang aplikasi pelayan Java yang menyerupai pangkalan data dalam memori. Artinya, kami memihak reka bentuk untuk menyimpan data dalam jumlah memori untuk memberikan prestasi pertanyaan yang sangat pantas.

Setelah prototaip berjalan, kami secara semula jadi memutuskan untuk memaparkan jejak memori data setelah diuraikan dan dimuat dari cakera. Hasil awal yang tidak memuaskan, bagaimanapun, mendorong saya untuk mencari penjelasan.

Catatan: Anda boleh memuat turun kod sumber artikel ini dari Sumber.

Alat tersebut

Oleh kerana Java sengaja menyembunyikan banyak aspek pengurusan memori, mengetahui berapa banyak memori yang digunakan oleh objek anda. Anda boleh menggunakan Runtime.freeMemory()kaedah untuk mengukur perbezaan ukuran timbunan sebelum dan sesudah beberapa objek diperuntukkan. Beberapa artikel, seperti "Soalan Minggu ke-107" Ramchander Varadarajan (Sun Microsystems, September 2000) dan "Memory Matters" Tony Sintes ( JavaWorld, Disember 2001), memperincikan idea itu. Sayangnya, penyelesaian artikel sebelumnya gagal kerana pelaksanaannya menggunakan Runtimemetode yang salah , sementara penyelesaian artikel terakhir memiliki ketidaksempurnaan tersendiri:

  • Satu panggilan untuk Runtime.freeMemory()membuktikan tidak mencukupi kerana JVM dapat memutuskan untuk meningkatkan ukuran timbunannya pada bila-bila masa (terutamanya ketika menjalankan pengumpulan sampah). Kecuali jumlah timbunan total sudah berada pada ukuran maksimum -Xmx, kita harus menggunakannya Runtime.totalMemory()-Runtime.freeMemory()sebagai ukuran timbunan yang digunakan.
  • Melaksanakan satu Runtime.gc()panggilan mungkin tidak cukup agresif untuk meminta pengumpulan sampah. Kita dapat, misalnya, meminta pemecah objek berjalan juga. Dan kerana Runtime.gc()tidak didokumentasikan untuk disekat sehingga pengumpulan selesai, ada baiknya menunggu sehingga ukuran timbunan yang dirasakan stabil.
  • Sekiranya kelas berprofil membuat data statik sebagai sebahagian daripada permulaan kelas per kelasnya (termasuk kelas statik dan pemula bidang), memori timbunan yang digunakan untuk contoh kelas pertama mungkin termasuk data tersebut. Kita harus mengabaikan ruang timbunan yang digunakan oleh contoh kelas pertama.

Mengingat masalah itu, saya tunjukkan Sizeof, alat yang saya gunakan untuk mengintip pelbagai kelas inti dan aplikasi Java:

kelas awam Sizeof {public static void main (String [] args) membuang Pengecualian {// Memanaskan semua kelas / kaedah yang akan kami gunakan runGC (); terpakaiMemory (); // Array untuk menyimpan rujukan yang kuat ke objek yang diperuntukkan jumlah akhir = 100000; Objek [] objek = Objek baru [kiraan]; timbunan panjang1 = 0; // Peruntukkan kiraan + 1 objek, buang yang pertama untuk (int i = -1; i = 0) objek [i] = objek; lain {objek = null; // Buang objek pemanasan runGC (); heap1 = terpakaiMemory (); // Ambil gambar sebelum timbunan}} runGC (); timbunan panjang2 = terpakaiMemory (); // Ambil snapshot selepas timbunan: ukuran int akhir = Math.round (((float) (heap2 - heap1)) / hitung); System.out.println ("'sebelum' heap:" + heap1 + ", 'after' heap:" + heap2); System.out.println ("heap delta:" + (heap2 - heap1) + ", {" + objek [0].getClass () + "} size =" + size + "byte"); untuk (int i = 0; i <count; ++ i) objek [i] = null; objek = batal; } private static void runGC () membuang Pengecualian {// Ini membantu untuk memanggil Runtime.gc () // menggunakan beberapa kaedah panggilan: untuk (int r = 0; r <4; ++ r) _runGC (); } kekosongan statik peribadi _runGC () melontarkan Pengecualian {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; untuk (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread (). Tahun (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} statik lama terpakaiMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime akhir statik peribadi s_runtime = Runtime.getRuntime (); } // Akhir kelassaya <mengira; ++ i) objek [i] = null; objek = batal; } private static void runGC () membuang Pengecualian {// Ini membantu untuk memanggil Runtime.gc () // menggunakan beberapa kaedah panggilan: untuk (int r = 0; r <4; ++ r) _runGC (); } kekosongan statik peribadi _runGC () melontarkan Pengecualian {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; untuk (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread (). Tahun (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} statik lama terpakaiMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime akhir statik peribadi s_runtime = Runtime.getRuntime (); } // Akhir kelassaya <mengira; ++ i) objek [i] = null; objek = batal; } private static void runGC () membuang Pengecualian {// Ini membantu untuk memanggil Runtime.gc () // menggunakan beberapa kaedah panggilan: untuk (int r = 0; r <4; ++ r) _runGC (); } kekosongan statik peribadi _runGC () melontarkan Pengecualian {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; untuk (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread (). Tahun (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} statik lama terpakaiMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime akhir statik peribadi s_runtime = Runtime.getRuntime (); } // Akhir kelasgc () // menggunakan beberapa kaedah panggilan: untuk (int r = 0; r <4; ++ r) _runGC (); } kekosongan statik peribadi _runGC () melontarkan Pengecualian {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; untuk (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread (). Tahun (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} statik lama terpakaiMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime akhir statik peribadi s_runtime = Runtime.getRuntime (); } // Akhir kelasgc () // menggunakan beberapa kaedah panggilan: untuk (int r = 0; r <4; ++ r) _runGC (); } kekosongan statik peribadi _runGC () melontarkan Pengecualian {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; untuk (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread (). Tahun (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} statik lama terpakaiMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime akhir statik peribadi s_runtime = Runtime.getRuntime (); } // Akhir kelasThread.currentThread (). Tahun (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} statik lama terpakaiMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime akhir statik peribadi s_runtime = Runtime.getRuntime (); } // Akhir kelasThread.currentThread (). Tahun (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} statik lama terpakaiMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime akhir statik peribadi s_runtime = Runtime.getRuntime (); } // Akhir kelas

SizeofKaedah utama adalah runGC()dan usedMemory(). Saya menggunakan runGC()kaedah pembungkus untuk memanggil _runGC()beberapa kali kerana nampaknya menjadikan kaedah itu lebih agresif. (Saya tidak pasti mengapa, tetapi mungkin membuat dan memusnahkan kaedah bingkai timbunan panggilan menyebabkan perubahan pada set root jangkauan dan mendorong pengumpul sampah untuk bekerja lebih keras. Lebih-lebih lagi, memakan sebahagian besar ruang timbunan untuk membuat kerja yang mencukupi agar pemungut sampah menendang juga membantu. Secara umum, sukar untuk memastikan semuanya dikumpulkan. Perincian tepat bergantung pada algoritma pengumpulan sampah dan JVM.)

Perhatikan dengan teliti tempat yang saya gunakan runGC(). Anda boleh mengedit kod antara heap1dan heap2perisytiharan untuk menunjukkan apa sahaja yang menarik.

Perhatikan juga bagaimana Sizeofmencetak ukuran objek: penutupan transitif data yang diperlukan oleh semua countcontoh kelas, dibahagi dengan count. Untuk kebanyakan kelas, hasilnya adalah memori yang dimakan oleh satu kelas, termasuk semua bidangnya. Nilai jejak memori itu berbeza dari data yang disediakan oleh banyak profiler komersial yang melaporkan jejak memori cetek (misalnya, jika objek mempunyai int[]medan, penggunaan memori akan muncul secara berasingan).

Keputusan

Mari gunakan alat mudah ini untuk beberapa kelas, kemudian lihat apakah hasilnya sesuai dengan jangkaan kami.

Catatan: Hasil berikut berdasarkan Sun JDK 1.3.1 untuk Windows. Kerana apa yang tidak dan tidak dijamin oleh spesifikasi bahasa Java dan JVM, Anda tidak dapat menerapkan hasil khusus ini ke platform lain atau implementasi Java lainnya.

java.lang.Object

Baiklah, akar semua objek mesti menjadi kes pertama saya. Kerana java.lang.Object, saya mendapat:

'sebelum' timbunan: 510696, timbunan 'setelah': 1310696 timbunan delta: 800000, {class java.lang.Object} size = 8 bait 

Jadi, dataran Objectmengambil 8 bait; sudah tentu, tidak boleh mengharapkan saiz untuk menjadi 0, kerana setiap masa perlu membawa sekitar medan yang operasi sokongan asas suka equals(), hashCode(), wait()/notify(), dan sebagainya.

java.lang.Integer

Saya dan rakan sekerja sering membungkus asli intske dalam Integercontoh sehingga kami dapat menyimpannya dalam koleksi Java. Berapakah harganya dalam ingatan kita?

'sebelum' timbunan: 510696, timbunan 'setelah': 2110696 timbunan delta: 1600000, {class java.lang.Integer} saiz = 16 bait 

Hasil 16-byte sedikit lebih buruk daripada yang saya jangkakan kerana intnilainya boleh memuatkan hanya 4 bait tambahan. Menggunakan Integerkos ingatan sebanyak 300 peratus berbanding dengan ketika saya dapat menyimpan nilainya sebagai jenis primitif.

java.lang.Long

Longharus mengambil lebih banyak memori daripada Integer, tetapi tidak:

timbunan 'sebelum': timbunan '510696', timbunan 'setelah': delta timbunan 2110696: 1600000, {class java.lang.Long} saiz = 16 bait 

Jelas, ukuran objek sebenarnya di timbunan tunduk pada penjajaran memori tingkat rendah yang dilakukan oleh pelaksanaan JVM tertentu untuk jenis CPU tertentu. Nampaknya Long8 byte Objectoverhead, ditambah 8 byte lebih banyak untuk nilai panjang sebenarnya. Sebaliknya, Integermempunyai lubang 4-byte yang tidak digunakan, kemungkinan besar kerana JVM I menggunakan kekuatan penjajaran objek pada batas kata 8-byte.

Susunan

Bermain dengan susunan jenis primitif membuktikan instruktif, sebahagiannya untuk menemui apa-apa overhead yang tersembunyi dan sebahagiannya untuk membenarkan satu lagi muslihat popular: membungkus nilai primitif dalam array ukuran-1 untuk menggunakannya sebagai objek. Dengan mengubahsuai Sizeof.main()untuk mempunyai gelung yang menambah panjang array yang dibuat pada setiap lelaran, saya mendapatkan intarray:

panjang: 0, {class [I} size = 16 bytes length: 1, {class [I} size = 16 bytes length: 2, {class [I} size = 24 bytees: 3, {class [I} size = Panjang 24 bait: 4, {class [I} size = 32 bytes length: 5, {class [I} size = 32 byte length: 6, {class [I} size = 40 bytes length: 7, {class [I} ukuran = panjang 40 bait: 8, {class [I} size = 48 byte panjang: 9, {class [I} size = 48 bytes length: 10, {class [I} size = 56 byte 

dan untuk chartatasusunan:

panjang: 0, {class [C} size = 16 bytes length: 1, {class [C} size = 16 bytes length: 2, {class [C} size = 16 byte length: 3, {class [C} size = Panjang 24 bait: 4, {class [C} size = 24 byte length: 5, {class [C} size = 24 bytes: 6, {class [C} size = 24 bytes length: 7, {class [C} ukuran = panjang 32 bita: 8, {class [C} size = 32 byte panjang: 9, {class [C} size = 32 bytes length: 10, {class [C} size = 32 byte 

Di atas, bukti penjajaran 8-bait muncul lagi. Selain itu, selain Objectoverhead 8-byte yang tidak dapat dielakkan , array primitif menambah 8 bait lain (di mana sekurang-kurangnya 4 bait menyokong lengthmedan). Dan penggunaan int[1]nampaknya tidak menawarkan kelebihan memori daripada sebuah Integerinstance, kecuali mungkin sebagai versi data yang sama yang dapat diubah.

Susunan pelbagai dimensi

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 hakikatnya, Strings dapat menggunakan lebih banyak memori daripada yang diceritakan panjangnya: Strings yang dihasilkan dari StringBuffers (sama ada secara eksplisit atau melalui pengendali gabungan '+') mungkin mempunyai chartatasusunan dengan panjang lebih besar daripada Stringpanjang yang dilaporkan kerana StringBuffers biasanya bermula dengan kapasiti 16 , kemudian gandakan pada append()operasi. Jadi, sebagai contoh, createString(1) + ' 'berakhir dengan charpelbagai ukuran 16, bukan 2.

Apa yang kita lakukan?

"Ini semua sangat baik, tetapi kita tidak punya pilihan selain menggunakan Stringjenis dan jenis lain yang disediakan oleh Java, bukan?" Saya dengar awak bertanya. Mari kita ketahui.

Kelas pembungkus