Mengukur Ukuran Objek di JVM

1. Ikhtisar

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

Pertama, kita akan terbiasa dengan berbagai metrik untuk menghitung ukuran objek. Kemudian, kita akan melihat beberapa cara untuk mengukur ukuran instans.

Biasanya, tata letak memori area data runtime bukan bagian dari spesifikasi JVM dan diserahkan pada kebijaksanaan implementor. Oleh karena itu, setiap implementasi JVM mungkin memiliki strategi yang berbeda untuk mengatur tata letak objek dan array dalam memori. Ini akan, pada gilirannya, memengaruhi ukuran instans pada waktu proses.

Dalam tutorial ini, kami berfokus pada satu implementasi JVM tertentu: HotSpot JVM.

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

2. Ukuran Objek Dangkal, Dipertahankan, dan Dalam

Untuk menganalisis ukuran objek, kita dapat menggunakan tiga metrik berbeda: Ukuran dangkal, dipertahankan, dan dalam.

Saat menghitung ukuran dangkal suatu objek, kami hanya mempertimbangkan objek itu sendiri. Artinya, jika objek tersebut memiliki referensi ke objek lain, kita hanya mempertimbangkan ukuran referensi ke objek target, bukan ukuran objek sebenarnya. Misalnya:

Seperti yang ditunjukkan di atas, ukuran dangkal dari Triple hanya merupakan jumlah dari tiga referensi. Kami mengecualikan ukuran sebenarnya dari objek yang dirujuk, yaitu A1, B1, dan C1, dari ukuran ini.

Sebaliknya, ukuran dalam suatu objek mencakup ukuran semua objek yang dirujuk, selain ukuran dangkal:

Di sini, ukuran dalam dari instance Triple berisi tiga referensi ditambah ukuran sebenarnya dari A1, B1, dan C1. Oleh karena itu, ukuran dalam bersifat rekursif.

Saat GC mengambil kembali memori yang ditempati oleh suatu objek, GC membebaskan sejumlah memori tertentu. Jumlah itu adalah ukuran yang dipertahankan dari objek itu:

Ukuran yang dipertahankan dari instans Triple hanya mencakup A1 dan C1 selain instans Triple itu sendiri. Di sisi lain, ukuran yang dipertahankan ini tidak menyertakan B1, karena instance Pair juga memiliki referensi ke B1.

Terkadang referensi tambahan ini dibuat secara tidak langsung oleh JVM itu sendiri. Oleh karena itu, menghitung ukuran yang dipertahankan bisa menjadi tugas yang rumit.

Untuk lebih memahami ukuran yang dipertahankan, kita harus berpikir dalam hal pengumpulan sampah. Mengumpulkan instance Triple membuat A1 dan C1 tidak dapat dijangkau, tetapi B1 masih dapat dijangkau melalui objek lain. Bergantung pada situasinya, ukuran yang dipertahankan bisa berada di antara ukuran dangkal dan dalam.

3. Ketergantungan

Untuk memeriksa tata letak memori objek atau array di JVM, kita akan menggunakan alat Java Object Layout (JOL). Oleh karena itu, kita perlu menambahkan dependensi jol-core :

 org.openjdk.jol jol-core 0.10 

4. Jenis Data Sederhana

Untuk mendapatkan pemahaman yang lebih baik tentang ukuran objek yang lebih kompleks, pertama-tama kita harus mengetahui berapa banyak ruang yang digunakan setiap tipe data sederhana. Untuk melakukan itu, kita dapat meminta Java Memory Layout atau JOL untuk mencetak informasi VM:

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

Kode di atas akan mencetak ukuran tipe data sederhana sebagai 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, berikut adalah persyaratan ruang untuk setiap tipe data sederhana di JVM:

  • Referensi objek menggunakan 4 byte
  • Nilai boolean dan byte mengkonsumsi 1 byte
  • pendek dan nilai char mengkonsumsi 2 byte
  • int dan nilai float mengkonsumsi 4 byte
  • nilai panjang dan ganda mengkonsumsi 8 byte

Hal ini berlaku dalam arsitektur 32-bit dan juga arsitektur 64-bit yang menerapkan referensi terkompresi.

Perlu juga disebutkan bahwa semua tipe data menggunakan jumlah memori yang sama saat digunakan sebagai tipe komponen array.

4.1. Referensi Tidak Terkompresi

Jika kita menonaktifkan referensi terkompresi melalui -XX: -UseCompressedOops tuning flag, maka persyaratan 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 referensi objek akan menggunakan 8 byte, bukan 4 byte. Tipe data lainnya masih menggunakan jumlah memori yang sama.

Selain itu, HotSpot JVM juga tidak dapat menggunakan referensi terkompresi saat ukuran heap lebih dari 32 GB (kecuali kita mengubah perataan objek).

Intinya adalah jika kita menonaktifkan referensi terkompresi secara eksplisit atau ukuran heap lebih dari 32 GB, referensi objek akan menggunakan 8 byte.

Sekarang setelah kita mengetahui konsumsi memori untuk tipe data dasar, mari menghitungnya untuk objek yang lebih kompleks.

5. Objek Kompleks

Untuk menghitung ukuran objek kompleks, mari pertimbangkan seorang profesor tipikal untuk hubungan kursus:

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

Setiap Profesor, selain perincian pribadi, dapat memiliki daftar 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. Ukuran Dangkal: Kelas Kursus

Ukuran dangkal dari instance kelas Kursus harus menyertakan referensi objek 4-byte (untuk bidang nama ) ditambah beberapa overhead objek. Kami dapat memeriksa asumsi ini menggunakan JOL:

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

Ini akan mencetak yang 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 byte, termasuk referensi objek 4 byte ke bidang nama ditambah tajuk objek.

5.2. Ukuran Dangkal: Kelas Profesor

Jika kita menjalankan kode yang sama untuk kelas Profesor :

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

Kemudian JOL akan mencetak konsumsi 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 mungkin kami perkirakan, kolom yang dienkapsulasi menggunakan 25 byte:

  • Tiga referensi objek, yang masing-masing mengkonsumsi 4 byte. Jadi total 12 byte untuk merujuk ke objek lain
  • Satu int yang mengkonsumsi 4 byte
  • Satu boolean yang mengkonsumsi 1 byte
  • Satu ganda yang memakan 8 byte

Menambahkan overhead 12 byte dari header objek ditambah 3 byte padding penyelarasan, ukuran dangkal adalah 40 byte.

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 mengenal berbagai metrik untuk mengukur ukuran objek di runtime JVM. Setelah itu, kami benar-benar mengukur ukuran instans dengan berbagai alat seperti JOL, Agen Java, dan utilitas baris perintah jcmd .

Seperti biasa, semua contoh tersedia di GitHub.