Polymorphism of a method defined in a class is achieved by choosing a virtual method table according to approximately the same way as in C ++. That is, the overhead of a call is to take the classword object (which is recorded in its header), add the offset known in advance to it, and call the method at the specified address.
Polymorphism of the method defined in the interface is achieved in a more complicated way: in the structure of the class description, a record is found that relates to this interface, and the necessary method is already searched in it. That is, let's say
public static <T> T getFirst(List<T> list) { return list.get(0); } public static <T> T getFirst(AbstractList<T> list) { return list.get(0); }
The first case may be slower because we call the interface method. Although the method is the same, but there is a difference. Even in bytecode two different instructions - invokeinterface and invokevirtual.
The HotSpot JIT compiler aggressively uses the devirtualization technique. Naturally a call to a final-method or a method of a final-class will turn into a normal static call. Also, if the type runtime table says that this method is not redefined anywhere, the call will be static:
public static <T> T getFirst(ArrayList<T> list) { return <static call> list.get(0); }
Although ArrayList not a final-class and the get method is also not final, if we know that it is not redefined at the moment in any loaded class, we can make the call static. If a new class is loaded that violates this condition, the JIT compiler recompiles this method.
If there are options, the type profile is used. For example, if this code has already been executed 5000 times and of them in 4990 cases, the specific ArrayList.get method was called, then the JIT-compiled code will become something like this:
public static <T> T getFirst(List<T> list) { if(list.getClass() == ArrayList.class) { return <static call> ((ArrayList<T>)list).get(0); } // обновить профиль типов return list.get(0); }
Checking list.getClass() == ArrayList.class very fast - it is to get the classword (for speed how to read the field of the object) and compare it with a constant (at the time of JIT compilation, the classword for the ArrayList class is known for sure). Branch-prediction will also work well if the condition is met in most cases.
If out of 5,000 calls there were 3,000 ArrayList and 1990 LinkedList , the code would be something like this:
public static <T> T getFirst(List<T> list) { if(list.getClass() == ArrayList.class) { return <static call> ((ArrayList<T>)list).get(0); } else if(list.getClass() == LinkedList.class) { return <static call> ((LinkedList<T>)list).get(0); } // обновить профиль типов return list.get(0); }
This is a bimorph call. If there were more than two popular options, then the challenge will remain an honest virtual one.
Of course, if you call several methods of an unknown object passed by a parameter in one method (or call a method many times in a loop), the type will be checked only once.
If the call was managed to be virtualized (at least in the bimorph variant), then inline ing was aggressively applied (I saw with my own eyes how in one method 70 pieces were inline to the depth of calls up to 8–9: the first inline the second, the third, etc. ). Inline opens the way to a ton of other optimizations.
As you already understood, initially the code is executed first, slower, and secondly in the profiling mode. That is, each time the method is called, the call is not just made, but the statistics table is updated, which indicates exactly which class was here. When statistics are collected, the method is recompiled with it in mind. Moreover, if there is a fast and slow branch, then the slow one will update the statistics further. For example, if the program usage scenario has changed, then the method can be recompiled again.