Search code examples
javamemoryjvmprimitive-types

Why does primitive data type consume much memory than reference data type for this scenario in Java?


I tried to check the memory consumption of a program. During the check, I have noticed some interesting things.

I created a Load class, which contains some fields.

class Load {
    String name;
    String title;
    long id;
}

I created 500000 Load objects and add them to an ArrayList. I have found, it took around 18 MB of memory.

Then, I modified the Load class and use reference type Long.

class Load {
    String name;
    String title;
    Long id;
}

Again created 500000 Load objects and add them to ArrayList. Interestingly this time it took less memory than the previous one. It way 14 MB.

Run test changing os and JVM version. Found the following results.

OS: Windows 10 Pro 64 bit
JDK: 11 64bit
 
Object Created  | Load Object         | Memory | Load Object         | Memory  
------------------------------------------------------------------------------
1. 500000       | With primitive long | 18 MB  | With reference Long | 14 MB
2. 900000       |                     | 32 MB  |                     | 26 MB
3. 1500000      |                     | 53 MB  |                     | 41 MB

OS: macOS Big Sur 64 bit
JDK: 8 64bit
 
Object Created  | Load Object         | Memory | Load Object         | Memory  
------------------------------------------------------------------------------
1. 500000       | With primitive long | 18 MB  | With reference Long | 14 MB
2. 900000       |                     | 32 MB  |                     | 26 MB
3. 1500000      |                     | 53 MB  |                     | 41 MB

Surprisingly, in all of these test runs, Object contains primitive types long consume more memory than Object contains reference Long.

My question is, why primitive type takes more memory in this scenario?

Memory Tester Code:

public class MemoryChecker {

    private static final long MEGABYTE = 1024L * 1024L;

    public static long bytesToMegabytes(long bytes) {
        return bytes / MEGABYTE;
    }

    public static void main(String[] args) {
        List<Load> list = new ArrayList<Load>();
        for (int i = 0; i <= 500000
                ; i++) {
            list.add(new Load("Jim", "Knopf", 11L));
        }
        // Get the Java runtime
        Runtime runtime = Runtime.getRuntime();
        // Run the garbage collector
        runtime.gc();
        // Calculate the used memory
        long memory = runtime.totalMemory() - runtime.freeMemory();
        System.out.println("Used memory is megabytes: " + bytesToMegabytes(memory));
    }
}

Complete code git repo.


Solution

  • For 32 Bit JVMs, but also for 64 Bit JVMs with the CompressedOOPs feature (which is supported by the HotSpot JVM and on by default), a reference consumes only 4 bytes, compared to the 8 bytes of a long.

    Even when you initialize the reference with an actual object, it may consume less memory when the object is shared. This applies to autoboxing of constants:

    If the value p being boxed is the result of evaluating a constant expression (§15.29) of type boolean, byte, char, short, int, or long, and the result is true, false, a character in the range '\u0000' to '\u007f' inclusive, or an integer in the range -128 to 127 inclusive, then let a and b be the results of any two boxing conversions of p. It is always the case that a == b.

    but also to all operations ending up at Long.valueOf(long) in general.

    This method will always cache values in the range -128 to 127, inclusive, and may cache other values outside of this range.

    Of course, if you create a lot of unshared Long objects, they will consume far more memory than the primitive long. If you use a lot of distinct values, even a potential sharing of them wouldn’t help.