How Java allocates memory to objects?


Reading time: 30 minutes | Coding time: 5 minutes

In Java, all objects have memory located in the heap space where Java's garbage collection constantly remove unused objects to make application memory efficient. The heap space is further divided into sections and it depends on the object's memory where the object will be placed.

Background

Memory is divided into 2 parts:

  • stack memory (faster access and contiguous allocation)
  • heap memory (slow access and dynamic allocation)

Programming languages like C and C++ has two types of memory allocation strategy:

  • Dynamic memory allocation (on heap)
  • Static memory allocation (on stack)

Unlike C and C++, Java allocates memory for objects only on heap like dynamic memory allocation. Java uses stack for other purposes like function calls.

In Java, heap is created when Java Virtual Machine (JVM) starts up and this size of heap is dynamic that is it can increase or decrease as per program requirement.

We shall note that Java allocates memory outside the heap as well and this is used for:

  • Internal data structures
  • methods/ function calls
  • threads

and many others.

In this article, we will focus on heap memory as Java objects are stored in heap.

Heap memory

In Java, heap memory divided into two sections:

  • nursery/ young generation/ young space
  • old generation/ old space

In Young space, objects that have been recently created are stored and in old space, objects that are being used for a long time are stored. This categorization of objects is for faster and more efficient garbage collection.

The young space is further divided into keep area which keeps the recently created objects and prevents the transfer of recently created objects to old space if a lot of objects are created inbetween garbage collection.

When a object is allocated, JVM identifies the object as:

  • a small object
  • a large object

depending upon the size of memory required by the object. This is done in runtime.

We can get the size of any object in Java using the instrumentation package. For example:

import java.lang.instrument.Instrumentation;
public class ObjectSizeFetcher 
{
    private static Instrumentation instrumentation;
    public static void premain(String args, Instrumentation inst) 
    {
        instrumentation = inst;
    }
    public static long getObjectSize(Object o) 
    {
        return instrumentation.getObjectSize(o);
    }
}

We can use getObjectSize() to fetch the size of any object in Java.

The size limit for large and small objects depends on multiple factors such as:

  • JVM version
  • heap size
  • JVM garbage collection strategy (JVM settings)
  • Platform

The size of small object is usually between 2 KB to 128 KB beyond which objects are large objects. Small objects are placed in a section known as Thread local areas (TLA). TLA are chunks of free memory reserved from heap. It is given to a Java thread for use. TLA are reserved from the young space if space is available or it will use old space.

The size can be set using JVM settings using options like:

  • -XXtlaSize
  • -XXlargeObjectLimit

Large objects will not fit in TLA and hence, it is directly allocated in heap. Memory allocation of large objects require synchronization of threads. JVM uses a set of caches of free memory chunks to:

  • minimize need for synchronization
  • speedup memory allocation

In Java, memory is allocated to an object when the contructor is called using new().

user-defined-type variable; // memory not allocated
variable = new user-defined-type(); // memory is allocated

Example:

class OpenGenus 
{
    int data;
    
	public static void main (String[] args) 
	{
		OpenGenus var1;
		var1.data = 1; // error
	}
}

Compile type Error:

prog.java:8: error: variable var1 might not have been initialized
		var1.data = 1;
		^
1 error

Correct code:

class OpenGenus 
{
    int data;
    
	public static void main (String[] args) 
	{
		OpenGenus var1;
		var1 = new OpenGenus();
		var1.data = 1; 
	}
}

The above code compiles correctly.

Consider this case to understand when a new object is created and a cached value is used.

Integer a = new Integer(1); // creates a new object
Integer b = 1; // uses a cached value
Integer c; // allocate memory in stack to hold the reference value (null)

For auto-boxing and primitive data types, cached value are used so:

int a = 1; // uses a cached value

Key takeaways

Key points to understand and make the best use of Java:

  • Small objects are faster than larger objects as both are allocated in different sections
  • Small objects are allocated at TLA (Thread local areas)
  • Large objects are allocated at heap and requires synchronization across threads which is slow
  • Some times, large objects use cache memory locations
  • Primitive data types are faster than user defined objects and autoboxing as cached values are used