In Java, objects are the basic building blocks of any applications. When an object is created, memory is allocated from the heap to store its instance variables. Understanding how much memory an object consumes is important for optimizing memory usage and preventing OutOfMemoryErrors. It is important to optimize the memory consumption especially in cloud solutions.
Object size is calculated as Object Header size + variable size(for primitives) or refence size(for objects)
. But it is not that simple. Actual memory size consumed depends on data type, architecture, variable object refereed by other object or not(P1 & P2 refers to same country object), String/primitive pools etc...
In Java, primitive member variables are recommended and it consumes less space compare to wrappers. We will do the comparison and see how much memory can be saved with primitive data types.
Object memory size is always multiple of 8. JVM adds padding bytes to make it multiple of 8 if required. If object memory size comes as 12 byte length, then JVM adds 4 padding byte to make it multiple of 8. 12 byte + 4 byte = 16 byte
. In this I will be using HotSpot JVM specification for measuring the sizes.
Size Metrics
To analyze size we use three metrics. Shallow size, Deep size, Retained size.
Shallow size
In this metrics, we consider object size itself. If it is having reference to other objects, we consider reference size while calculating size metric.
Deep Size
In this metrics, we consider object size and size of reference objects.
Retained Size
In this metrics, we consider object's shallow size and the objects are only referenced from this object. In other words, the retained size is the amount of memory that can be reclaimed by garbage-collecting this object. We don't consider objects if it is referred by more than one object.
In real, Retained Size metric is very important and it is purely depends on data and object reference structure. It is tricky to calculate and complicate tasks if object(s) are referred by different instances.
Elements influences object size
Object Header
Every object will have object header and it is based on oopDesc
data structure. Object header will contain mark word
and klass pointer
.
Mark Word
will contain information related to hash code, locking and GC information. The size of this is 4 byte and 8 byte
in 32 and 62 bit architecture
respectively.
Klass pointer
will contain information related to class, modifiers and super class. The size of this is 4 byte
in both architecture.
Object Header size would be 8 byte and 16 byte
in 32-bit and 62-bit
architecture respectively.
Member Variables
- Primitive data types
Primitive data types consumes fixed amount of memory size.
- Primitive Wrappers
Primitive Wrappers will consumes additional memory size. For primitive wrappers memory size calculation is Primitive data type size + Object Header size + Padding bytes if required
.
- Arrays
Arrays can be primitive or non primitive arrays. Memory size for arrays is sum of Object Header + (number of elements * reference for objects /primitive size for primitives) + length field (4 byte always)
. Reference is 4 byte
size always. Length field
will store the length of the array.
If we have Person[]
with 36 length, then size is 12 + (36 * 4 (reference size) ) + 4 = 160 byte
.
If we have byte[]
with 36 length, then size is 12 + (36 * 1 (byte size)) + 4 = 52 + 4 padding = 56 byte
.
- String
The size (shallow) of this String instance is 16 byte and 24 byte
in 32 and 62 bit architecture
respectively. It includes the 4 byte of cached hash code, 4 byte of char[] reference, and object header.
The actual string is stored as byte array and size of it is (1 (byte size) * number of characters) + 12 ( Object header) + 4 (length)
. For 36 length string will have (1*36) + 8 + 4 + 4 = 52 + 4 padding = 56 byte
.
Total Retained/Deep size of String (of 36 length) is 24 byte + 56 byte = 80 byte
.
Lets look into an example
We will first look into class without variables and later get into with variable. We will also look into member variables with wrappers and primitives. We will estimate the size and see how much memory we can save with primitive data types. I will be using VisualVM for profiling the data and 64-bit architecture system.
1. With out any member variables
Consider simple Person
class without any variable.
public class Person{
}
Now we will create 1000 objects of this and put it array.
public class MemoryMgmtController {
int count = 1000;
Person[] persons = new Person[count];
public MemoryMgmtController() {
for (int i = 0; i < count; i++) {
persons[i] = new Person();
}
}
}
As we know array memory size is sum of Object Header + (array size * 4 byte)
, So size of Person[]
would be 16 byte (with 4 padding byte) + (1000 * 4 byte) = 4016 byte
.
In addition to this, we have created 1000 Person objects which will have Object Header information (16 byte with 4 padding byte) by default. So it would be 16 * 1000 = 16000 byte
.
So without any data, for simply creating 1000 objects itself has occupied 4016 + 16000 = 20016 byte (~20KB)
memory.
2. With wrappers as member variables
Consider simple Person
class with wrappers as member variable.
public class Person{
public Boolean active;
public Short country;
public Character gender;
public Integer id;
public Float salary;
public Long pk;
public Double bonus;
public String name;
}
Now we will create 1000 objects with data and assign it to array.
public class MemoryMgmtController {
int count = 1000;
Person[] persons = new Person[count];
public MemoryMgmtController() {
for (int i = 0; i < count; i++) {
Person person = new Person();
person.setActive(Boolean.TRUE);
person.setCountry((short) 12);
person.setGender(GENDER);
person.setId((int) i);
person.setSalary((float) i);
person.setPk((long) i);
person.setBonus((double) i);
person.setName(UUID.randomUUID().toString());
persons[i] = person;
}
}
}
Size of Person[]
would be same as earlier i.e 16 byte (with 4 padding byte) + (1000 * 4 byte) = 4016 byte
.
But if we look at size of 1000 objects, it would increase. The reason we have added member variables which will occupy the size. We have added wrappers as member variable so Person
object will hold the reference to wrapper objects.
Even though we say object header is 16 byte in 64-bit architecture, that is with 4 padding byte. While estimating, we consider 12 byte for object header, if required padding byte are added.
Total Memory estimation
3. With primitives as member variables
When we use primitives they will consumer fixed size and directly part of Person
. Person will not have any reference except String in our scenario.
Using primitives instead of wrappers has saved nearly 45% memory in this scenario. This will vary based on variables and object structure.
Actual memory consumed will depend on so many other factors like retained size, duplicate strings, JVM implementation and many others. Having idea on high level estimation will help us to develop memory optimized applications.
Tips
- Use primitives instead of wrappers where ever possible.
- Use Trove library to have collections with primitives.
- Consider UseCompressedOops java flag to improve the performance in 64-bit architecture.
- Use Java Object Layout (JOL) tool to inspect memory layouts of object.
Happy Coding and Learning !!!
Please drop a comment if you have any question.
Top comments (0)