Sizeof untuk Java

26 Desember 2003

T: Apakah Java memiliki operator seperti sizeof () di C?

A: Sebuah jawaban yang dangkal adalah bahwa Java tidak memberikan apa-apa seperti C sizeof(). Namun, mari pertimbangkan mengapa pemrogram Java terkadang menginginkannya.

Pemrogram AC mengelola sendiri sebagian besar alokasi memori struktur data, dan sizeof()sangat diperlukan untuk mengetahui ukuran blok memori yang akan dialokasikan. Selain itu, pengalokasi memori C malloc()hampir tidak melakukan apa pun sejauh inisialisasi objek diperhatikan: programmer harus mengatur semua bidang objek yang merupakan penunjuk ke objek selanjutnya. Tetapi ketika semua dikatakan dan dikodekan, alokasi memori C / C ++ cukup efisien.

Sebagai perbandingan, alokasi dan konstruksi objek Java terikat bersama (tidak mungkin menggunakan instance objek yang dialokasikan tetapi tidak diinisialisasi). Jika kelas Java mendefinisikan bidang yang merupakan referensi ke objek lebih lanjut, itu juga umum untuk mengaturnya pada waktu konstruksi. Oleh karena itu, mengalokasikan objek Java sering mengalokasikan banyak instance objek yang saling berhubungan: grafik objek. Ditambah dengan pengumpulan sampah otomatis, semua ini terlalu nyaman dan dapat membuat Anda merasa tidak perlu khawatir tentang detail alokasi memori Java.

Tentu saja, ini hanya berfungsi untuk aplikasi Java sederhana. Dibandingkan dengan C / C ++, struktur data Java yang setara cenderung menempati lebih banyak memori fisik. Dalam pengembangan perangkat lunak perusahaan, mendekati memori virtual maksimum yang tersedia pada JVM 32-bit saat ini adalah kendala skalabilitas yang umum. Dengan demikian, seorang programmer Java bisa mendapatkan keuntungan dari sizeof()atau sesuatu yang serupa untuk mengawasi apakah struktur datanya terlalu besar atau mengandung hambatan memori. Untungnya, refleksi Java memungkinkan Anda untuk menulis alat semacam itu dengan cukup mudah.

Sebelum melanjutkan, saya akan memberikan beberapa jawaban yang sering tetapi salah untuk pertanyaan artikel ini.

Kekeliruan: Sizeof () tidak diperlukan karena ukuran tipe dasar Java sudah tetap

Ya, Java intadalah 32 bit di semua JVM dan di semua platform, tetapi ini hanya persyaratan spesifikasi bahasa untuk lebar yang dapat dilihat programmer dari tipe data ini. Pada intdasarnya, jenis data abstrak dan dapat didukung oleh, katakanlah, kata memori fisik 64-bit pada mesin 64-bit. Hal yang sama berlaku untuk tipe nonprimitif: spesifikasi bahasa Java tidak mengatakan apa pun tentang bagaimana bidang kelas harus diselaraskan dalam memori fisik atau bahwa array boolean tidak dapat diimplementasikan sebagai bitvector kompak di dalam JVM.

Kekeliruan: Anda dapat mengukur ukuran objek dengan membuat serialisasi menjadi aliran byte dan melihat panjang aliran yang dihasilkan

Alasan ini tidak berhasil adalah karena tata letak serialisasi hanya refleksi jarak jauh dari tata letak dalam memori yang sebenarnya. Salah satu cara mudah untuk melihatnya adalah dengan melihat bagaimana Strings mendapatkan serial: dalam memori setiap charsetidaknya 2 byte, tetapi dalam bentuk serial Stringadalah UTF-8 dikodekan sehingga setiap konten ASCII mengambil setengah dari banyak ruang.

Pendekatan kerja lain

Anda mungkin ingat "Tip Java 130: Apakah Anda Tahu Ukuran Data Anda?" yang mendeskripsikan teknik berdasarkan pembuatan sejumlah besar instance kelas yang identik dan dengan cermat mengukur peningkatan yang dihasilkan dalam ukuran heap yang digunakan JVM. Jika dapat diterapkan, ide ini bekerja dengan sangat baik, dan saya sebenarnya akan menggunakannya untuk mem-bootstrap pendekatan alternatif dalam artikel ini.

Perhatikan bahwa kelas Java Tip 130 Sizeofmemerlukan JVM diam (sehingga aktivitas heap hanya karena alokasi objek dan koleksi sampah yang diminta oleh thread pengukuran) dan memerlukan sejumlah besar instance objek yang identik. Ini tidak berfungsi saat Anda ingin mengukur satu objek besar (mungkin sebagai bagian dari keluaran pelacakan debug) dan terutama saat Anda ingin memeriksa apa yang sebenarnya membuatnya begitu besar.

Berapa ukuran sebuah benda?

Diskusi di atas menyoroti poin filosofis: mengingat bahwa Anda biasanya berurusan dengan grafik objek, apa definisi ukuran objek? Apakah hanya ukuran instance objek yang Anda periksa atau ukuran seluruh grafik data yang di-root pada instance objek? Yang terakhir inilah yang biasanya lebih penting dalam praktiknya. Seperti yang akan Anda lihat, segala sesuatunya tidak selalu begitu jelas, tetapi sebagai permulaan, Anda dapat mengikuti pendekatan ini:

  • Sebuah instance objek dapat berukuran (kurang-lebih) dengan menjumlahkan semua bidang data nonstatisnya (termasuk bidang yang ditentukan dalam superclass)
  • Tidak seperti, katakanlah, C ++, metode kelas dan virtualitasnya tidak berdampak pada ukuran objek
  • Antarmuka kelas tidak berdampak pada ukuran objek (lihat catatan di akhir daftar ini)
  • Ukuran objek penuh dapat diperoleh sebagai penutupan atas seluruh grafik objek yang berakar pada objek awal
Catatan: Mengimplementasikan antarmuka Java apa pun hanya menandai kelas yang dimaksud dan tidak menambahkan data apa pun ke definisinya. Nyatanya, JVM bahkan tidak memvalidasi bahwa implementasi antarmuka menyediakan semua metode yang diperlukan oleh antarmuka: ini sepenuhnya merupakan tanggung jawab penyusun dalam spesifikasi saat ini.

Untuk proses bootstrap, untuk tipe data primitif saya menggunakan ukuran fisik yang diukur dengan kelas Java Tip 130 Sizeof. Ternyata, untuk JVM 32-bit yang umum, sebuah dataran java.lang.Objectmembutuhkan 8 byte, dan tipe data dasar biasanya berukuran paling kecil secara fisik yang dapat mengakomodasi persyaratan bahasa (kecuali booleanmembutuhkan seluruh byte):

// java.lang.Object ukuran shell dalam byte: public static final int OBJECT_SHELL_SIZE = 8; OBJREF_SIZE public int static final = 4; public int static final LONG_FIELD_SIZE = 8; INT_FIELD_SIZE public int static final = 4; SHORT_FIELD_SIZE public int static final = 2; CHAR_FIELD_SIZE public int static final = 2; BYTE_FIELD_SIZE public int static final = 1; BOOLEAN_FIELD_SIZE public int static final = 1; DOUBLE_FIELD_SIZE public int static final = 8; FLOAT_FIELD_SIZE public int static final = 4;

(It is important to realize that these constants are not hardcoded forever and must be independently measured for a given JVM.) Of course, naive totaling of object field sizes neglects memory alignment issues in the JVM. Memory alignment does matter (as shown, for example, for primitive array types in Java Tip 130), but I think it is unprofitable to chase after such low-level details. Not only are such details dependent on the JVM vendor, they are not under the programmer's control. Our objective is to obtain a good guess of the object's size and hopefully get a clue when a class field might be redundant; or when a field should be lazily populated; or when a more compact nested datastructure is necessary, etc. For absolute physical precision you can always go back to the Sizeof class in Java Tip 130.

To help profile what makes up an object instance, our tool will not just compute the size but will also build a helpful datastructure as a byproduct: a graph made up of IObjectProfileNodes:

interface IObjectProfileNode { Object object (); String name (); int size (); int refcount (); IObjectProfileNode parent (); IObjectProfileNode [] children (); IObjectProfileNode shell (); IObjectProfileNode [] path (); IObjectProfileNode root (); int pathlength (); boolean traverse (INodeFilter filter, INodeVisitor visitor); String dump (); } // End of interface 

IObjectProfileNodes are interconnected in almost exactly the same way as the original object graph, with IObjectProfileNode.object() returning the real object each node represents. IObjectProfileNode.size() returns the total size (in bytes) of the object subtree rooted at that node's object instance. If an object instance links to other objects via non-null instance fields or via references contained inside array fields, then IObjectProfileNode.children() will be a corresponding list of child graph nodes, sorted in decreasing size order. Conversely, for every node other than the starting one, IObjectProfileNode.parent() returns its parent. The entire collection of IObjectProfileNodes thus slices and dices the original object and shows how data storage is partitioned within it. Furthermore, the graph node names are derived from the class fields and examining a node's path within the graph (IObjectProfileNode.path()) allows you to trace the ownership links from the original object instance to any internal piece of data.

You might have noticed while reading the previous paragraph that the idea so far still has some ambiguity. If, while traversing the object graph, you encounter the same object instance more than once (i.e., more than one field somewhere in the graph is pointing to it), how do you assign its ownership (the parent pointer)? Consider this code snippet:

 Object obj = new String [] {new String ("JavaWorld"), new String ("JavaWorld")}; 

Each java.lang.String instance has an internal field of type char[] that is the actual string content. The way the String copy constructor works in Java 2 Platform, Standard Edition (J2SE) 1.4, both String instances inside the above array will share the same char[] array containing the {'J', 'a', 'v', 'a', 'W', 'o', 'r', 'l', 'd'} character sequence. Both strings own this array equally, so what should you do in cases like this?

If I always want to assign a single parent to a graph node, then this problem has no universally perfect answer. However, in practice, many such object instances could be traced back to a single "natural" parent. Such a natural sequence of links is usually shorter than the other, more circuitous routes. Think about data pointed to by instance fields as belonging more to that instance than to anything else. Think about entries in an array as belonging more to that array itself. Thus, if an internal object instance can be reached via several paths, we choose the shortest path. If we have several paths of equal lengths, well, we just pick the first discovered one. In the worst case, this is as good a generic strategy as any.

Berpikir tentang traversal grafik dan jalur terpendek seharusnya membunyikan lonceng pada titik ini: penelusuran luas-pertama adalah algoritme lintas grafik yang menjamin untuk menemukan jalur terpendek dari node awal ke node grafik lain yang dapat dijangkau.

Setelah semua pendahuluan tersebut, berikut adalah implementasi buku teks dari traversal grafik tersebut. (Beberapa detail dan metode tambahan telah dihilangkan; lihat unduhan artikel ini untuk detail selengkapnya.):