Pages

sizeof in java or how much memory is this object going to consume

Introduction

Ever wonder what is the size of an object? How much space does it take up? And where can I find out this info?
 
 
In this post we will try to answer these question. And see what difference does it make if you use primitives vs Objects (for example float vs Float).
 
The code used in this example is available here for you to try out. I ran my code with JDK 21. Feel free to try out other versions and see if makes a difference.  

Setup

The code uses the most excellent JOL (Java Object Layout) library. And that is what I used.

 
JVM parameters to run the examples:
When running the sample code in your IDE (or by hand), you will need to supply these JVM arguments to the java process:
 
 -Djdk.attach.allowAttachSelf -Djol.magicFieldOffset=true 


Hello World

Run the App class which prints out the object-size based on the class definition of the GamePlayer class:
 
 
What does the SZ column mean? And what about OFF? SZ defines the size in bytes of the different fields of this class. These sizes are JVM dependent and we can use the JVMInfo class to find out what the sizes of different fields in my jvm are:

From this we can see that:
A reference in this class to another object takes 4 bytes
boolean: 1 byte 
byte: 1 byte
char: 2 bytes
short: 2 bytes
int: 4 bytes
float: 4 bytes
long: 8 bytes
double: 8 bytes

We also have array element sizes too.
 
OFF defines the offset bytes from the beginning of the object.
 
I recommend reading the post linked above and others by Aleksey Shipilëv.

Using JOL we can get the following information:
  • how much memory this object would take in memory (based on class definition)
 System.out.println(ClassLayout.parseClass(GamePlayer.class).toPrintable());
  • how much memory an instance of this object takes
 System.out.println(ClassLayout.parseInstance(gamePlayer).toPrintable());
  • how much memory an instance object with references to other objects in an object-graph takes
    • the instance memory usage is the object plus all it's references memory usage, and all the references' references.... until we get to an object that has no outgoing references
System.out.println(GraphLayout.parseInstance(gamePlayer).toPrintable());
System.out.println(GraphLayout.parseInstance(gamePlayer).toFootprint());
 
But before we get onto these methods, let us talk about....

Object Alignment & padding

Working with high-level languages we sometimes forgot what is happening below the surface. Your computers memory is laid out as one really really long array. The OS then gives you chunks of free and contiguous-memory when you ask for it. But in general, remember that memory is laid out as an array of bits. Therefore, when the jvm wants to store a java object in the heap, this object must be laid out in memory. And how fields are stored in memory is called alignment. Read more about it here.
 
A key piece of information is:
 

A Java Object's and it's fields are (by default) aligned to 8 bytes.

 
This means everything must fit within a multiple of 8 bytes. If the fields end up taking less then some multiple of 8, then the JVM adds padding to ensure that it fits within some multiple of 8 bytes.
 
Now back the output of App, we can see that the GamePlayer class takes up 32 bytes. Notice also 3 bytes are lost due to 'alignment/padding gap'. Without this padding, this object would take up 29 bytes. But we can't have this because this is not a multiple of 8. The next multiple of 8 is 32. So 3 bytes are 'lost' due to this padding.

Class's Memory vs Instance's Memory & Object Graph's memory

Now an object can be not created, so it is not taking up any memory, or we create an instance of it and then it is taking up memory. In the App class, we use the code:
 
ClassLayout.parseClass(GamePlayer.class).toPrintable()

com.srasul.GamePlayer object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     N/A
  8   4                    (object header: class)    N/A
 12   4                int GamePlayer.score          N/A
 16   4                int GamePlayer.rank           N/A
 20   4              float GamePlayer.average        N/A
 24   1            boolean GamePlayer.alive          N/A
 25   3                    (alignment/padding gap)   
 28   4   java.lang.String GamePlayer.name           N/A
Instance size: 32 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

Let us count the memory usage
  1. object's mark & header take up 12 bytes
  2. score is an int field, takes up 4 bytes
  3. rank is an int field, takes up 4 bytes
  4. average is a float, takes up 4 bytes
  5. alive is a boolean: 1 byte
  6. some padding of 3 bytes
  7. name is a String reference, 4 bytes 

Total: 32 bytes.

Let us now create an instance of this object:

        GamePlayer gamePlayer = new GamePlayer("srasul", true, 100, 1, 100.0f);
 

 Now we can use the ClassLayout.parseInstance(gamePlayer).toPrintable() to print out the memory usage of an just this instance:

com.srasul.GamePlayer object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4                    (object header: class)    0x010031f8
 12   4                int GamePlayer.score          100
 16   4                int GamePlayer.rank           1
 20   4              float GamePlayer.average        100.0
 24   1            boolean GamePlayer.alive          true
 25   3                    (alignment/padding gap)   
 28   4   java.lang.String GamePlayer.name           (object)
Instance size: 32 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

Since all the memory has to fit inside blocks of 8 bytes (aka alignment size), without the padding, the object's size would be 29 bytes. And in order to fit inside the block, java creates a 'padding gap' of 3 bytes so that the final size is 32.

Now since a GamePlayer instance has a reference to a String object and this string object can be arbitrarily long, this object along with it's references can take up a lot of memory. Let's count this using:  GraphLayout.parseInstance(gamePlayer).toPrintable()
 
com.srasul.GamePlayer@30dae81d object externals:
          ADDRESS       SIZE TYPE                  PATH              VALUE
        62b31b538         32 com.srasul.GamePlayer                   (object)
        62b31b558         24 java.lang.String      .name             (object)
        62b31b570         24 [B                    .name.value       [115, 114, 97, 115, 117, 108]

 
Ok let us count:
The GamePlayer object takes up 32 bytes. It has a reference to a String, and a String object itself takes up 24 bytes. Now the String object has a reference to a byte[] called value. And this byte[] object along with it's data (of the string 'srasul') takes up: 24 bytes. 

We can get a total memory footprint using this: GraphLayout.parseInstance(gamePlayer).toFootprint()
 

com.srasul.GamePlayer@30dae81d footprint:
     COUNT       AVG       SUM   DESCRIPTION
         1        24        24   [B
         1        32        32   com.srasul.GamePlayer
         1        24        24   java.lang.String
         3                  80   (total)

Let us read this: we have 1 instance of Game player that takes 32 bytes. We have 1 instance of a String object that takes 24 bytes, And we have one byte[] that takes 24 bytes. Using the the methods I have shown you can use them get a 'sizeof' the String class. I will leave that as an exercise for the reader. But in total this object has a footprint of 80 bytes.

Let us try to workout why a byte[] with 6 characters takes up 24 bytes.
 
An empty char[] object takes up 16 bytes:

[B object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     N/A
  8   4        (object header: class)    N/A
 12   4        (array length)            N/A
 16   0   byte [B.<elements>             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

Now we specified our string to be 'srasul'. This means, 16 bytes + 5 (for srasul) for the char[] using ClassLayout.parseInstance("srasul".getBytes()).toPrintable() we can see:
 
[B object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x00002268
 12   4        (array length)            6
 16   6   byte [B.<elements>             N/A
 22   2        (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

 
Now 16 + 6 = 22. But remember alignment? everything has to fit in blocks of 8. So the next block of 8 is 24. And this is why the string of 'srasul' takes up: 24 + 24 = 48 bytes in memory. 24 for the object of String and 24 for the char[] that contains all the chars of 'srasul', including the padding up to be a multiple of 8. Since we have a padding 2, this means that if we can increase our string by 1 or 2 chars, the memory usage would remain the same:

ClassLayout.parseInstance("srasul1".getBytes()).toPrintable()
 
[B object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x00002268
 12   4        (array length)            7
 16   7   byte [B.<elements>             N/A
 23   1        (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 1 bytes external = 1 bytes total

 
ClassLayout.parseInstance("srasul12".getBytes()).toPrintable()
 
[B object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x00002268
 12   4        (array length)            8
 16   8   byte [B.<elements>             N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total