// HACKER NEWS — CYBERSECURITY
Value Classes Still Need Compiler Sympathy
JEP 401, a major Valhalla milestone, has been integrated as a preview feature in JDK 28. This is very exciting, as value classes increase both our ability to communicate the semantics of our programs to others and the optimization opportunities available to the JVM.
However, I have seen people online essentially taking a “Value all the classes!” approach to this. I worry that there is a belief that ordinary classes give you a floor on performance, and that value classes will do their best to raise you above that floor, but won’t ever take you below it. Unfortunately, that is not true. A well-intentioned program may put the JVM into a situation where a flattened representation is faster for some methods, and a reference representation is faster for others. When these methods interact, the JVM is forced to convert between the two representations. I want value classes to be more than just magic, so today I am going to show you what the JVM is capable of right now, and where its limitations are. I hope that with this you’ll have some context for reasoning about the code that you (or your AI agent) write.
The main optimization advantage of value classes is that we give up identity. This gives the JVM freedom to choose a suitable representation for a particular situation. Without the requirement of identity, the runtime can more readily flatten values (avoiding pointer chasing), scalarize them by representing their components independently in registers or on the stack. For the value object itself, escape analysis becomes trivial: there is no identity whose escape must be proven unobservable.
We are going to examine three examples: a large final value stored flat, a direct value transformation compiled without allocation, and a generic virtual call that requires materialization.
JEP 539, Strict Field Initialization in the JVM, lets the JVM rely on a final field having been initialized before its enclosing object becomes observable. Because such a field cannot later be updated, the JVM may use a non-atomic flattened layout without risking a torn assignment. Mutable fields, however, must preserve tear-free assignment. If a mutable field contains a value that is too large for an atomic flattened update, the JVM must instead use a reference layout. The strict-initialization guarantee opens up many optimization possibilities.
FourLongs has 32 bytes of payload, making it too large for an atomic flattened update in the current JVM. But Envelope.payload is a record component and therefore a strictly initialized final field: once initialized, it is never updated. The JVM is consequently free to store payload using a non-atomic flattened layout. In the current Valhalla master build, the field-layout diagnostic reports the following when using PrintFieldLayout:
Here we can see that a FourLongs consists of its four components and a 1-byte null marker, and that it supports a nullable, non-atomic flattened layout. The runtime uses this fact in the Envelope record, and allows FourLongs to be flattened. The key point is that Envelope is also immutable; the layout would have to change if we replaced it with a mutable class:
Why is that? Let’s consider a data race between two threads:
Writing a flattened field requires writing its individual components. If thread1 and thread2 wrote those components independently, another thread could observe a torn value such as (1, 1, 0, 0), assembled from parts of two different assignments. The Java Memory Model forbids such tearing: after both threads have joined, this program may print only (1, 0, 0, 0) or (0, 1, 0, 0). Guaranteeing tear-free assignment for a flattened value this large would be expensive, so the current JVM uses a reference layout. Each thread constructs a complete FourLongs and then performs an atomic reference store.
If we have a small function that changes a component in a loop, like this: