Effective Kotlin Item 47: Avoid unnecessary object creation
Object creation always costs something and can sometimes be expensive. This is why avoiding unnecessary object creation can be an important optimization. It can be done on many levels. For instance, in JVM it is guaranteed that a string object will be reused by other code running in the same virtual machine that happens to contain the same string literal2:
Boxed primitives (
Long) are also reused in JVM when they are small (by default, the Integer Cache holds numbers in the range from -128 to 127).
Reference equality (
===) shows that this is the same object. However, if we use a number that is either smaller than -128 or bigger than 127, different objects will be created:
Notice that a nullable type is used to force
intunder the hood. When we use
Int, it is generally compiled to the primitive
int, but if we make it nullable or when we use it as a type argument,
Integeris used instead. This is because a primitive cannot be
nulland cannot be used as a type argument.
Knowing that such mechanisms are available in Kotlin, you might wonder how significant they are. Is object creation expensive?
Wrapping something into an object has 3 costs:
Objects take additional space. In a modern 64-bit JDK, an object has a 12-byte header that is padded to a multiple of 8 bytes, so the minimum object size is 16 bytes. For 32-bit JVMs, the overhead is 8 bytes. Additionally, object references also take space. Typically, references are 4 bytes on 32-bit or 64-bit platforms up to -Xmx32G, and they are 8 bytes for memory allocation pool set above 32Gb (-Xmx32G). These are relatively small numbers, but they can add up to a significant cost. When we think about small elements like integers, they make a difference.
Intas a primitive fit in 4 bytes, but when it is a wrapped type on the 64-bit JDK we mainly use today, it requires 16 bytes (it fits in the 4 bytes after the header), and its reference requires 4 or 8 bytes. In the end, it takes 5 or 6 times more space1. This is why an array of primitive integers (
IntArray) takes 5 times less space than an array of wrapped integers (
Array<Int>), as explained in the Item 58: Consider Arrays with primitives for performance-critical processing.
Access requires an additional function call when elements are encapsulated. Again, this is a small cost as function use is very fast, but it can add up when we need to operate on a huge pool of objects. We will see how this cost can be eliminated in Item 51: Use the inline modifier for functions with parameters of functional types and Item 49: Consider using inline classes.
Objects need to be created and allocated in memory, references need to be created, etc. These are small numbers, but they can rapidly accumulate when there are many objects. In the snippet below, you can see the cost of object creation.
By eliminating objects, we can avoid all three of these costs. By reusing objects, we can eliminate the first and the third ones. If we know the costs of objects, we can start considering how we can minimize these costs in our applications by limiting the number of unnecessary objects. In the next few items, we will see different ways to eliminate or reduce the number of objects. In this item, I will only present one technique, that is designing classes to use primitives instead of wrapped types.
In JVM, we have a special built-in type to represent basic elements like numbers or characters. These are called primitives and are used by the Kotlin/JVM compiler under the hood wherever possible. However, there are some cases where a wrapped class (an object instance containing a primitive) needs to be used instead. The two main cases are:
- When we operate on a nullable type (primitives cannot be
- When we use a type as a generic type argument.
So, in short:
Now you know that you can optimize your code to have primitives under the hood instead of wrapped types. Such optimization makes sense mainly on Kotlin/JVM and on some flavors of Kotlin/Native, but it doesn't make any sense on Kotlin/JS. Access to both primitive and wrapped types is relatively fast compared to other operations. The difference manifests itself when we deal with bigger collections (we will discuss this in Item 58: Consider Arrays with primitives for performance-critical processing) or when we operate on an object intensively. Also, remember that forced changes might lead to less-readable code. This is why I suggest this optimization only for performance-critical parts of code and in libraries. You can identify the performance-critical parts of your code using a profiler.
To consider a concrete example, let’s imagine that you implement a financial application in which you need to represent a stock snapshot. A snapshot is a set of values that are updated twice a second. It contains the following information:
Since you are tracking tens of thousands of stocks, and the snapshot for each of them is updated twice a second, your application will create instances of
SessionDetails many times per second, which will require a lot of effort from the garbage collector. To avoid this, you can change the
SessionDetails class to use primitives instead of wrapped types by eliminating nullability.
Note that this change harms readability and makes this class harder to use because
null is a better way to represent the lack of a value than a special value like
-1. However, in this case we decided to make this change because we are dealing with a performance-critical part of the application. By eliminating nullability, we’ve made our object allocate far fewer objects and much less memory. On a typical machine, the first version of
SessionDetails allocates 192 bytes and needs to create 10 objects; in contrast, the second version allocates only 80 bytes and needs to create only one object. This is a significant difference that might be worth the trouble when we are dealing with tens of thousands of objects.
If such interventions are not enough in your application, you can consider using a very powerful but also very dangerous pattern object pool. Its core idea is to make objects mutable and to store and reuse unused objects. This pattern is hard to implement correctly, and it is easy to introduce synchronization issues, which is why I don’t recommend using it unless you’re sure that you need it.
In this chapter, we learned about the costs of object creation and allocation. We also learned that we can reduce these costs by eliminating objects or reusing them, or by designing our objects to use primitives. The next items present other ways to reduce the number of unnecessary objects in our applications.
To measure the size of concrete fields in JVM objects, use Java Object Layout.
Java Language Specification, Java SE 8 edition, 3.10.5