A few more memory saving techniques in Java

by Mikhail Vorontsov

This article will list a few more ideas on cheap memory saving techniques in Java. This article follows An overview of memory saving techniques in Java, Memory consumption of popular Java data types – part 1 and Memory consumption of popular Java data types – part 2 articles earlier published in this blog. I strongly recommend you to read An overview of memory saving techniques in Java before reading this article. This article assumes that size of Object references is equal to 4 bytes.

Static inner classes

Java lacks a normal library tuple support like the one Scala has. This forces Java developers to create a lot of usually inner classes, whose only purpose is to compose a few elements as a single entity for a collection. One really popular example of such inner class is Pair, which is often implemented as a generic class.

Inner non-static classes have an important property – they store a reference to their parent class. Such reference allows them to seamlessly call parent methods and access parent fields. Such a reference is often not needed for the simple tuple-like classes. You can get rid of it by marking your class as static. This will save you 4 bytes and increase its chances to occupy 8 bytes less (see An overview of memory saving techniques in Java for more details).

You should generally mark all your inner classes as static until there is a need to remove such qualifier.

Tiny collections

JDK collections incur a noticeable memory overhead in case when collection is small. You can avoid it by using specialized flavors of collections provided by java.util.Collections class. There are two sets of specialized methods:

  • emptyList/emptyMap/emptySet for empty collections
  • singletonList/singletonMap/singleton for collections containing a single element/pair

These methods return immutable collections, so you will need some extra logic if you want to use them for mutable collections, but they make a perfect sense if you have a collection of immutable collections: empty collections are singletons, so they occupy no extra storage (just 4 bytes for a reference pointing to them), singleton collections occupy just either 16 bytes (list/set) or 24 bytes (map).

These ideas were further extended in Scala immutable collections package, which defines 5 special immutable map/set implementations with sizes from 0 to 4.

Boolean flags

If you have a list/array of objects having some boolean flags, check if you can extract these flags into one or more independent BitSet objects. Use original list/array index for accessing these flags in the BitSet. This approach requires that no insertions/removals are being made in the middle of array/list, because BitSet does not efficiently support shifting.

Such improvement may save you rather small amount of memory (1 byte is converted into 1 bit), but it will make a CPU difference if you have some loops on this array checking one of these boolean flags. For example, you have an Order with a boolean requiresProcessing flag. Your code will call some process method on orders having this flag set.

A normal object-oriented loop will check a boolean field on every order. Accessing an order will most likely incur a CPU cache miss (we have a lot of orders and we must follow a pointer in order to get to the order fields). In most situations it means a CPU cache miss on every order. Now suppose you are iterating a separate BitSet with requiresProcessing flags and then jump to the required order using a flag index.

In this situation all your flags are located sequentially, so sequential memory access pattern will be used by CPU cache fetching logic. Besides that, you will have 512 flags in one cache line (64 bytes * 8 bit). It means the cheapest possible iteration of flags plus most likely no overhead on fetching the next 512 flags. Your orders are located in the array, so you have 16 order references in one cache line. Besides that, there is same sequential access pattern (though, with some jumps). Chances are good that obtaining an order reference will also be done from CPU cache. And only after that, when you have to process an order, its data should be obtained from RAM into cache, causing a cache miss.

A simple optimization made it possible to pay for data access only on orders which actually have to be processed instead of every order. You should remember that difference in CPU cache access time and RAM access time is about an order of magnitude.

Remember that you can also use a BitSet for storing a dense set of integer numbers regardless of the starting sequence number in your set (see Bit sets for details).

String pooling

Read a separate article dedicated to String.intern() method implementation in Java 6, 7 and 8, which will describe you the right methods of implementing a string pool in all these versions of Java.

Summary

  • Make all your inner classes static by default. Remove static qualifier only when you have to.
  • If you have a collection of generally small collections, try to use java.util.Collections.empty*/singleton* methods for memory-efficient storage of tiny collections.
  • Prefer a BitSet to arrays/lists of boolean or dense sets of any integer types: bit sets are both memory and CPU cache friendly.

See also