Mengukur Ukuran Objek di JVM

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan melihat berapa banyak ruang yang digunakan setiap objek di timbunan Java.

Pertama, kita akan membiasakan dengan metrik yang berbeza untuk mengira ukuran objek. Kemudian, kita akan melihat beberapa cara untuk mengukur saiz contoh.

Biasanya, susun atur memori kawasan data runtime bukan sebahagian dari spesifikasi JVM dan diserahkan kepada budi bicara pelaksana. Oleh itu, setiap pelaksanaan JVM mungkin mempunyai strategi yang berbeza untuk menyusun objek dan susunan dalam memori. Ini pada gilirannya, akan mempengaruhi ukuran contoh pada waktu runtime.

Dalam tutorial ini, kami memfokuskan pada satu pelaksanaan JVM khusus: The HotSpot JVM.

Kami juga menggunakan istilah JVM dan HotSpot JVM secara bergantian sepanjang tutorial.

2. Ukuran Objek Cetek, Dikekalkan, dan Dalam

Untuk menganalisis ukuran objek, kita dapat menggunakan tiga metrik yang berbeza: Ukuran cetek, tertahan, dan dalam.

Semasa mengira ukuran objek yang cetek, kita hanya mempertimbangkan objek itu sendiri. Artinya, jika objek tersebut memiliki rujukan ke objek lain, kita hanya mempertimbangkan ukuran rujukan ke objek sasaran, bukan ukuran objek sebenarnya. Contohnya:

Seperti yang ditunjukkan di atas, ukuran dangkal contoh Triple hanyalah jumlah tiga rujukan. Kami mengecualikan ukuran sebenar objek yang disebut, iaitu A1, B1, dan C1, dari ukuran ini.

Sebaliknya, ukuran objek yang mendalam merangkumi ukuran semua objek yang disebut, selain ukuran yang cetek:

Di sini ukuran mendalam contoh Triple mengandungi tiga rujukan ditambah ukuran sebenar A1, B1, dan C1. Oleh itu, saiz yang mendalam bersifat rekursif.

Apabila GC mengambil semula memori yang ditempati oleh objek, ia akan membebaskan sejumlah memori tertentu. Jumlah itu adalah ukuran objek yang dikekalkan:

Saiz contoh Triple yang dikekalkan hanya merangkumi A1 dan C1 sebagai tambahan kepada contoh Triple itu sendiri. Sebaliknya, ukuran yang dikekalkan ini tidak termasuk B1, kerana contoh Pair juga memiliki referensi ke B1.

Kadang kala rujukan tambahan ini dibuat secara tidak langsung oleh JVM itu sendiri. Oleh itu, mengira ukuran yang dipertahankan boleh menjadi tugas yang rumit.

Untuk lebih memahami ukuran yang dikekalkan, kita harus memikirkan dari segi pengumpulan sampah. Mengumpulkan contoh Triple menjadikan A1 dan C1 tidak dapat dicapai, tetapi B1 masih dapat dicapai melalui objek lain. Bergantung pada keadaan, ukuran yang dipertahankan boleh berada di antara ukuran dangkal dan dalam.

3. Kebergantungan

Untuk memeriksa susunan memori objek atau tatasusunan di JVM, kita akan menggunakan alat Java Object Layout (JOL). Oleh itu, kita perlu menambahkan kebergantungan jol-core :

 org.openjdk.jol jol-core 0.10 

4. Jenis Data Ringkas

Untuk lebih memahami ukuran objek yang lebih kompleks, pertama-tama kita harus mengetahui berapa banyak ruang yang digunakan oleh setiap jenis data sederhana. Untuk melakukan itu, kita dapat meminta Java Memory Layout atau JOL untuk mencetak maklumat VM:

System.out.println(VM.current().details());

Kod di atas akan mencetak ukuran jenis data sederhana seperti berikut:

# Running 64-bit HotSpot VM. # Using compressed oop with 3-bit shift. # Using compressed klass with 3-bit shift. # Objects are 8 bytes aligned. # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

Jadi, inilah keperluan ruang untuk setiap jenis data mudah di JVM:

  • Rujukan objek menggunakan 4 bait
  • nilai boolean dan bait menggunakan 1 bait
  • nilai pendek dan char menggunakan 2 bait
  • nilai int dan apungan menggunakan 4 bait
  • nilai panjang dan berganda menggunakan 8 bait

Ini berlaku dalam seni bina 32-bit dan juga seni bina 64-bit dengan rujukan termampat yang berlaku.

Perlu disebutkan bahawa semua jenis data menggunakan jumlah memori yang sama ketika digunakan sebagai jenis komponen array.

4.1. Rujukan Tidak Mampat

Sekiranya kita mematikan rujukan yang dimampatkan melalui -XX: -UseCompressedOops tuning flag, maka keperluan ukuran akan berubah:

# Objects are 8 bytes aligned. # Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

Sekarang rujukan objek akan menggunakan 8 bait dan bukannya 4 bait. Jenis data yang tinggal masih menggunakan jumlah memori yang sama.

Lebih-lebih lagi, HotSpot JVM juga tidak dapat menggunakan rujukan yang dimampatkan apabila ukuran timbunan lebih dari 32 GB (kecuali kita mengubah penjajaran objek).

Intinya adalah jika kita menonaktifkan rujukan yang dimampatkan secara eksplisit atau ukuran timbunan lebih dari 32 GB, rujukan objek akan memakan 8 bait.

Setelah mengetahui penggunaan memori untuk jenis data asas, mari kita menghitungnya untuk objek yang lebih kompleks.

5. Objek Kompleks

Untuk mengira ukuran objek kompleks, mari kita pertimbangkan profesor biasa untuk hubungan kursus:

public class Course { private String name; // constructor }

Setiap Profesor, selain maklumat peribadi, boleh mempunyai senarai Kursus :

public class Professor { private String name; private boolean tenured; private List courses = new ArrayList(); private int level; private LocalDate birthDay; private double lastEvaluation; // constructor }

5.1. Saiz Cetek: Kelas Kursus

Saiz cetek dari contoh kelas Kursus harus merangkumi rujukan objek 4-byte (untuk bidang nama ) ditambah dengan beberapa overhead objek. Kami dapat memeriksa andaian ini menggunakan JOL:

System.out.println(ClassLayout.parseClass(Course.class).toPrintable());

Ini akan mencetak perkara berikut:

Course object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 java.lang.String Course.name N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

Seperti yang ditunjukkan di atas, ukuran dangkal adalah 16 bait, termasuk objek 4 bait yang merujuk ke bidang nama ditambah tajuk tajuk.

5.2. Saiz Cetek: Kelas Profesor

Sekiranya kita menjalankan kod yang sama untuk kelas Profesor :

System.out.println(ClassLayout.parseClass(Professor.class).toPrintable());

Kemudian JOL akan mencetak penggunaan memori untuk kelas Profesor seperti berikut:

Professor object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Professor.level N/A 16 8 double Professor.lastEvaluation N/A 24 1 boolean Professor.tenured N/A 25 3 (alignment/padding gap) 28 4 java.lang.String Professor.name N/A 32 4 java.util.List Professor.courses N/A 36 4 java.time.LocalDate Professor.birthDay N/A Instance size: 40 bytes Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

Seperti yang kami jangkakan, medan yang dikemas menggunakan 25 bait:

  • Tiga rujukan objek, masing-masing menggunakan 4 bait. Jadi 12 bait keseluruhan untuk merujuk kepada objek lain
  • Satu int yang menggunakan 4 bait
  • Satu boolean yang menggunakan 1 bait
  • Satu dua kali ganda yang menggunakan 8 bait

Menambah overhead 12 byte header objek ditambah 3 bait padding penjajaran, ukuran cetek adalah 40 bait.

The key takeaway here is, in addition to the encapsulated state of each object, we should consider the object header and alignment paddings when calculating different object sizes.

5.3. Shallow Size: an Instance

The sizeOf() method in JOL provides a much simpler way to compute the shallow size of an object instance. If we run the following snippet:

String ds = "Data Structures"; Course course = new Course(ds); System.out.println("The shallow size is: " + VM.current().sizeOf(course));

It'll print the shallow size as follows:

The shallow size is: 16

5.4. Uncompressed Size

If we disable the compressed references or use more than 32 GB of the heap, the shallow size will increase:

Professor object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 16 (object header) N/A 16 8 double Professor.lastEvaluation N/A 24 4 int Professor.level N/A 28 1 boolean Professor.tenured N/A 29 3 (alignment/padding gap) 32 8 java.lang.String Professor.name N/A 40 8 java.util.List Professor.courses N/A 48 8 java.time.LocalDate Professor.birthDay N/A Instance size: 56 bytes Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

When the compressed references are disabled, the object header and object references will consume more memory. Therefore, as shown above, now the same Professor class consumes 16 more bytes.

5.5. Deep Size

To calculate the deep size, we should include the full size of the object itself and all of its collaborators. For instance, for this simple scenario:

String ds = "Data Structures"; Course course = new Course(ds);

The deep size of the Course instance is equal to the shallow size of the Course instance itself plus the deep size of that particular String instance.

With that being said, let's see how much space that String instance consumes:

System.out.println(ClassLayout.parseInstance(ds).toPrintable());

Each String instance encapsulates a char[] (more on this later) and an int hashcode:

java.lang.String object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 4 4 (object header) 00 00 00 00 8 4 (object header) da 02 00 f8 12 4 char[] String.value [D, a, t, a, , S, t, r, u, c, t, u, r, e, s] 16 4 int String.hash 0 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

The shallow size of this String instance is 24 bytes, which include the 4 bytes of cached hash code, 4 bytes of char[] reference, and other typical object overhead.

To see the actual size of the char[], we can parse its class layout, too:

System.out.println(ClassLayout.parseInstance(ds.toCharArray()).toPrintable());

The layout of the char[] looks like this:

[C object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 4 4 (object header) 00 00 00 00 8 4 (object header) 41 00 00 f8 12 4 (object header) 0f 00 00 00 16 30 char [C. N/A 46 2 (loss due to the next object alignment) Instance size: 48 bytes Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

So, we have 16 bytes for the Course instance, 24 bytes for the String instance, and finally 48 bytes for the char[]. In total, the deep size of that Course instance is 88 bytes.

With the introduction of compact strings in Java 9, the String class is internally using a byte[] to store the characters:

java.lang.String object internals: OFFSET SIZE TYPE DESCRIPTION 0 4 (object header) 4 4 (object header) 8 4 (object header) 12 4 byte[] String.value # the byte array 16 4 int String.hash 20 1 byte String.coder # encodig 21 3 (loss due to the next object alignment)

Therefore, on Java 9+, the total footprint of the Course instance will be 72 bytes instead of 88 bytes.

5.6. Object Graph Layout

Instead of parsing the class layout of each object in an object graph separately, we can use the GraphLayout. With GraphLayot, we just pass the starting point of the object graph, and it'll report the layout of all reachable objects from that starting point. This way, we can calculate the deep size of the starting point of the graph.

For instance, we can see the total footprint of the Course instance as follows:

System.out.println(GraphLayout.parseInstance(course).toFootprint());

Which prints the following summary:

[email protected] footprint: COUNT AVG SUM DESCRIPTION 1 48 48 [C 1 16 16 com.baeldung.objectsize.Course 1 24 24 java.lang.String 3 88 (total)

That's 88 bytes in total. The totalSize() method returns the total footprint of the object, which is 88 bytes:

System.out.println(GraphLayout.parseInstance(course).totalSize());

6. Instrumentation

To calculate the shallow size of an object, we can also use the Java instrumentation package and Java agents. First, we should create a class with a premain() method:

public class ObjectSizeCalculator { private static Instrumentation instrumentation; public static void premain(String args, Instrumentation inst) { instrumentation = inst; } public static long sizeOf(Object o) { return instrumentation.getObjectSize(o); } }

As shown above, we'll use the getObjectSize() method to find the shallow size of an object. We also need a manifest file:

Premain-Class: com.baeldung.objectsize.ObjectSizeCalculator

Then using this MANIFEST.MF file, we can create a JAR file and use it as a Java agent:

$ jar cmf MANIFEST.MF agent.jar *.class

Finally, if we run any code with the -javaagent:/path/to/agent.jar argument, then we can use the sizeOf() method:

String ds = "Data Structures"; Course course = new Course(ds); System.out.println(ObjectSizeCalculator.sizeOf(course));

This will print 16 as the shallow size of the Course instance.

7. Class Stats

To see the shallow size of objects in an already running application, we can take a look at the class stats using the jcmd:

$ jcmd  GC.class_stats [output_columns]

For instance, we can see each instance size and number of all the Course instances:

$ jcmd 63984 GC.class_stats InstSize,InstCount,InstBytes | grep Course 63984: InstSize InstCount InstBytes ClassName 16 1 16 com.baeldung.objectsize.Course

Again, this is reporting the shallow size of each Course instance as 16 bytes.

To see the class stats, we should launch the application with the -XX:+UnlockDiagnosticVMOptions tuning flag.

8. Heap Dump

Using heap dumps is another option to inspect the instance sizes in running applications. This way, we can see the retained size for each instance. To take a heap dump, we can use the jcmd as the following:

$ jcmd  GC.heap_dump [options] /path/to/dump/file

For instance:

$ jcmd 63984 GC.heap_dump -all ~/dump.hpro

This will create a heap dump in the specified location. Also, with the -all option, all reachable and unreachable objects will be present in the heap dump. Without this option, the JVM will perform a full GC before creating the heap dump.

After getting the heap dump, we can import it into tools like Visual VM:

As shown above, the retained size of the only Course instance is 24 bytes. As mentioned earlier, the retained size can be anywhere between shallow (16 bytes) and deep sizes (88 bytes).

It's also worth mentioning that the Visual VM was part of the Oracle and Open JDK distributions before Java 9. However, this is no longer the case as of Java 9, and we should download the Visual VM from its website separately.

9. Conclusion

Dalam tutorial ini, kami membiasakan dengan metrik yang berbeza untuk mengukur ukuran objek dalam jangka masa JVM. Selepas itu, kami benar-benar mengukur ukuran contoh dengan pelbagai alat seperti JOL, Java Agents, dan utiliti baris perintah jcmd .

Seperti biasa, semua contoh boleh didapati di GitHub.