Java’s Enum.values() Hidden Allocations
I recently profiled an app of mine. During profiling, I’ve noticed tons of array allocations. Something like:
Well easy, a quick grep for TypeInfo[] array. Nothing. Ah, quick grep for TypeInfo collections. Nothing! Huh? Where the hell do I allocate the array then? Ok, more profiling! So, I enabled flight recorder with ‘-XX:+UnlockCommercialFeatures -XX:+FlightRecorder’. Then did a recording with Java Mission Control, capturing the allocation stack traces:
Ok, the TypeInfo.values() method allocates the memory. TypeInfo is a enum, and I’m using the built in values() method. The code looked something like this:
package info.gamlor.blog;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
enum TypeInfo {
UNKNOWN(0),
INT(1),
STRING(2);
private final int id;
TypeInfo(int id) {
this.id = id;
}
public static TypeInfo findType(int type){
for (TypeInfo t : values()){
if(t.id == type){
return t;
}
}
return UNKNOWN;
}
}
public class Main {
private static Random rnd = new Random();
public static void main(String[] args) throws Exception {
List<TypeInfo> parsedData = new ArrayList<>();
for (int i = 0; i < 1000000000; i++) {
int idParsedFromWire = readTypeIdFromData(rnd);
TypeInfo type = TypeInfo.findType(idParsedFromWire);
// Pretend we use the type for the exampe
parsedData.add(type);
consumeData(parsedData);
}
System.out.println(parsedData.size());
}
private static void consumeData(List<TypeInfo> parsedData) {
// Parsed data is consumed
if(rnd.nextBoolean()){
parsedData.clear();
}
}
private static int readTypeIdFromData(Random rnd) throws InterruptedException {
// Data comes from the wire....let's run this example a bit slower
int idParsedFromWire = rnd.nextInt(16);
Thread.yield();
return idParsedFromWire;
}
}
Do you see the allocation? Why should it allocate that many arrays? The Enum values are constant, so it should be able to initialize an array once? Ok, let’s take a closer look and decompile the class file:
javap -c TypeInfo.class | grep -A 10 values
And we get:
public static info.gamlor.blog.TypeInfo[] values(); Code: 0: getstatic #1 // Field $VALUES:[Linfo/gamlor/blog/TypeInfo; 3: invokevirtual #2 // Method "[Linfo/gamlor/blog/TypeInfo;".clone:()Ljava/lang/Object; 6: checkcast #3 // class "[Linfo/gamlor/blog/TypeInfo;" 9: areturn
As we can see, it dereferences a static array of the Enum values. Then it clones that array before returning it, causing an array allocation. Why does it do that? It is a defensive copy. Java’s arrays are a mutable. If values() just returns the reference to the original array, then some code might change the array’s content. That creates havoc, since it would change what Enum.values() returns. Therefore, the defensive copy.
So, after seeing this, it is easy to fix our code. We call Enum.values() once, cache the array and use it for the parsing:
// Enum.values() does a defensive copy and allocates an array on each call
// To avoid that allocation, we call values() only once
private static final TypeInfo[] ALL_VALUES = values();
public static TypeInfo findType(int type){
for (TypeInfo t : ALL_VALUES){
if(t.id == type){
return t;
}
}
return UNKNOWN;
}
Et voila, the allocations are gone =)