Do sealed interfaces in Java enable any specific JVM optimisations, or are they purely a compile‑time exhaustiveness feature

1 day ago 1
ARTICLE AD BOX

TL;DR: No, there are no optimisations enabled by it and there never will be 1. Because the JVM already does this optimisation and has done so for a very long time already.

Impossible optimisations

Various aspects of java-the-language do not survive the compilation process. 'generics erasure' is a particularly well known one, for example. Checked exceptions don't really exist at the JVM level either (javac knows what a throws definition is and means; java the runtime engine has no idea; it can read the throws line solely for the purpose of ignoring them completely.)

Local variables don't survive either (they get 'compressed' into slots). Any optimisation that relies on the runtime knowing stuff that doesn't survive compilation is trivially impossible.

Sealed classes aren't like that. They survive: The class file format explicitly lists out the allowed subclasses.

But that's cold comfort; the JVM can't make meaningful use of it.

The general design of java makes it impossible for the runtime to preload the sealed classes: Given a fully qualified type name, the runtime can't load such a class. It can only load classes with a [fully qualified type name, classloader] tuple which isn't how the sealed system works (any type whose FQN fits the list of allowed types is fine, it doesn't matter which classloader ends up loading it. Even some dynamically generated classloader that doesn't even exist yet can load it). In that sense, 'sealed' isn't really sealed. At any point in time, any code can create a new classloader and have that classloader load a new class whose fully qualified name is on the list. An infinite number of classes can be created and loaded that are all allowed. 2

That means the JVM:

Can never conclude every possible subclass has actually been loaded. Even if there's a class loaded for every allowed subclass listed in the sealed definition. Pre-emptively load already known subclasses by looking at the 'allowed subclasses' list from the sealed type's definition.

Which essentially means we're done here: The JVM cannot apply any meaningful optimisation.

But.. that's okay - JVM already does it

Because the JVM does what I'd term 'optimistic sealing'. Analogous to the concept of optimistic locking, the JVM hotspot compilers usually just assume all types are sealed:

If there is a meaningful performance benefit to be had by presuming that all subtypes for a given type are known and enumerated, then it will simply hotspot as if no further subtypes will ever appear.

Then, the JVM class loader system gets a hook: If ever you define a subtype of this thing, invalidate the generated hotspot code.

This is a have your cake and eat it too moment: The JVM gets all the benefits of being able to dynamically load whatever you want, while getting to avoid the potentially significant cost of endless dynamic dispatch whenever it is worthwhile to avoid it.

That system's been in hotspot compilers for ages, and it's heavy-traffic optimisation: Tons of very commonly used classes aren't final and yet rarely have subtypes. Making them final now would be backwards incompatible. As a trivial example, one of the most commonly used classes in the JVM: ArrayList - is not final. And yet, rarely extended.

Conclusion

Hence there's no need for hotspot to care about sealed. The obvious optimisations one can apply with it are optimisations the JVM can apply essentially just as well without knowing about sealed.


[1] Well, never say never. The java lang spec is extremely specific, but the JVM is far less so: The JVM spec talks about 'guarantees'; a JVM implementation must ensure all guarantees are fully delivered, but beyond that it can do whatever it wants. This is important, in order to allow JVM implementations on various different platforms to ensure it can pick performant strategies to deliver them. As an example, the JMM section ("Java Memory Model") doesn't mention in any way which CPU primitives should be used to make sure synchronized and volatile do their job. All they do is speak in terms of 'given this code, then line Y must not be able to observe the state of a field as it was before line X modified it, outside of timing issues'. Once a JVM implementation provides that guarantee, it is comforming to spec; it does not matter how it fulfills it. In that sense an absolute promise is not possible.

[2] You might be thinking: The OpenJDK team keeps harping on about how modern JVM versions take 'security' very seriously and this does not sound very secure! Wrong 'security'. In general, if you allow untrusted code to run on your JVM, you've lost the game utterly and totally and must assume that, if that code is malicious, every aspect of your JVM is now under full control of the malicious code) - but the module system is the basis of it: You can't extend a sealed class from another module by using the same name as one of the allowed classes. Within the same module, a new classloader can be made at runtime and then load a class with a fully qualified name that is on the 'allowed!' list, which means it can therefore be loaded.

Read Entire Article