<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Johan Sjölén&#39;s blog</title>
        <link>https://johan-sjolen.github.io/</link>
        <description>Recent content on Johan Sjölén&#39;s blog</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Fri, 21 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://johan-sjolen.github.io/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>Value Classes Still Need Compiler Sympathy</title>
            <link>https://johan-sjolen.github.io/post/compiler-sympathy/compiler-sympathy/</link>
            <pubDate>Fri, 21 Aug 2026 00:00:00 +0000</pubDate>
            <guid>https://johan-sjolen.github.io/post/compiler-sympathy/compiler-sympathy/</guid>
            <description>&lt;p&gt;&lt;em&gt;This post discusses preview features in JDK 28&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://openjdk.org/jeps/401&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;JEP 401&lt;/a&gt;, 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.&lt;/p&gt;&#xA;&lt;p&gt;However, I have seen people online essentially taking a &amp;ldquo;Value all the classes!&amp;rdquo; 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&amp;rsquo;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 &lt;strong&gt;right now&lt;/strong&gt;, and where its limitations are. I hope that with this you&amp;rsquo;ll have some context for reasoning about the code that you (or your AI agent) write.&lt;/p&gt;&#xA;&lt;p&gt;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.&lt;/p&gt;&#xA;&lt;p&gt;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.&lt;/p&gt;&#xA;&lt;h2 id=&#34;immutability-enables-flattening&#34;&gt;Immutability enables flattening&#xA;&lt;/h2&gt;&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://openjdk.org/jeps/539&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;JEP 539&lt;/a&gt;, 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.&lt;/p&gt;&#xA;&lt;p&gt;Consider this small example:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;value record &lt;span style=&#34;color:#a6e22e&#34;&gt;FourLongs&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; a, &lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; b, &lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; c, &lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; d) {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;record&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Envelope&lt;/span&gt;(FourLongs payload) {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;FourLongs&lt;/code&gt; has 32 bytes of payload, making it too large for an atomic flattened update in the current JVM. But &lt;code&gt;Envelope.payload&lt;/code&gt; is a record component and therefore a strictly initialized final field: once initialized, it is never updated. The JVM is consequently free to store &lt;code&gt;payload&lt;/code&gt; using a non-atomic flattened layout. In the current Valhalla master build, the field-layout diagnostic reports the following when using &lt;code&gt;PrintFieldLayout&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Layout of class FourLongs&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  @8  REGULAR 8/8 &amp;#34;a&amp;#34; J&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  @16 REGULAR 8/8 &amp;#34;b&amp;#34; J&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  @24 REGULAR 8/8 &amp;#34;c&amp;#34; J&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  @32 REGULAR 8/8 &amp;#34;d&amp;#34; J&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  @40 NULL_MARKER 1/1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  NULLABLE_NON_ATOMIC_FLAT layout: 33/8&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Layout of class Envelope&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  @8 FLAT 33/8 &amp;#34;payload&amp;#34; LFourLongs;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      FourLongs NULLABLE_NON_ATOMIC_FLAT&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here we can see that a &lt;code&gt;FourLongs&lt;/code&gt; 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 &lt;code&gt;Envelope&lt;/code&gt; record, and allows &lt;code&gt;FourLongs&lt;/code&gt; to be flattened. The key point is that &lt;code&gt;Envelope&lt;/code&gt; is also immutable; the layout would have to change if we replaced it with a mutable class:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MutableEnvelope&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; FourLongs payload;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MutableEnvelope&lt;/span&gt;(FourLongs payload) { &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;payload&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; payload; }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Layout of class MutableEnvelope&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  @8 REGULAR 4/4 &amp;#34;payload&amp;#34; LFourLongs;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Why is that? Let&amp;rsquo;s consider a data race between two threads:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;thread1&lt;/span&gt;(MutableEnvelope a) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    a.&lt;span style=&#34;color:#a6e22e&#34;&gt;payload&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; FourLongs(1, 0, 0, 0);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;thread2&lt;/span&gt;(MutableEnvelope a) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    a.&lt;span style=&#34;color:#a6e22e&#34;&gt;payload&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; FourLongs(0, 1, 0, 0);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;() &lt;span style=&#34;color:#66d9ef&#34;&gt;throws&lt;/span&gt; InterruptedException {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    MutableEnvelope a &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; MutableEnvelope(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; FourLongs(0, 0, 0, 0));&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; t1 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Thread(() &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; thread1(a));&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; t2 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Thread(() &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; thread2(a));&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    t1.&lt;span style=&#34;color:#a6e22e&#34;&gt;start&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    t2.&lt;span style=&#34;color:#a6e22e&#34;&gt;start&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    t1.&lt;span style=&#34;color:#a6e22e&#34;&gt;join&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    t2.&lt;span style=&#34;color:#a6e22e&#34;&gt;join&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    IO.&lt;span style=&#34;color:#a6e22e&#34;&gt;println&lt;/span&gt;(a.&lt;span style=&#34;color:#a6e22e&#34;&gt;payload&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;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 &lt;code&gt;(1, 1, 0, 0)&lt;/code&gt;, assembled from parts of two different assignments. The Java Memory Model forbids such tearing: after both threads have joined, this program may print only &lt;code&gt;(1, 0, 0, 0)&lt;/code&gt; or &lt;code&gt;(0, 1, 0, 0)&lt;/code&gt;. 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.&lt;/p&gt;&#xA;&lt;h2 id=&#34;removing-identity-removes-the-allocation&#34;&gt;Removing identity removes the allocation&#xA;&lt;/h2&gt;&lt;p&gt;If we have a small function that changes a component in a loop, like this:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;static&lt;/span&gt; FourLongs &lt;span style=&#34;color:#a6e22e&#34;&gt;bumpA&lt;/span&gt;(FourLongs value) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; FourLongs(value.&lt;span style=&#34;color:#a6e22e&#34;&gt;a&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; 1, value.&lt;span style=&#34;color:#a6e22e&#34;&gt;b&lt;/span&gt;(), value.&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;(), value.&lt;span style=&#34;color:#a6e22e&#34;&gt;d&lt;/span&gt;());&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; iterations) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    FourLongs value &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; FourLongs(0, 2, 3, 4);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; 0; i &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; iterations; i&lt;span style=&#34;color:#f92672&#34;&gt;++&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        value &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; bumpA(value);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; value.&lt;span style=&#34;color:#a6e22e&#34;&gt;a&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; value.&lt;span style=&#34;color:#a6e22e&#34;&gt;b&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; value.&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; value.&lt;span style=&#34;color:#a6e22e&#34;&gt;d&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At the source level, we can see that every call to &lt;code&gt;bumpA&lt;/code&gt; constructs a new &lt;code&gt;FourLongs&lt;/code&gt;. At the call site of &lt;code&gt;run&lt;/code&gt;, however, we can see that the returned value is effectively only there to change the &lt;code&gt;a&lt;/code&gt; component of &lt;code&gt;value&lt;/code&gt;. A good optimizing compiler ought to be able to recognize that as well. It turns out that C2 is a pretty good compiler! C2 keeps the representation scalarized, and it even recognizes that the final result must be &lt;code&gt;iterations + (2 + 3 + 4) = iterations + 9&lt;/code&gt; when &lt;code&gt;iterations &amp;gt;= 0&lt;/code&gt;. The following is an abridged excerpt of the generated code:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-asm&#34; data-lang=&#34;asm&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;mov&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x0&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;#9               ; Put 9 into x0, which holds the return value&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cmp&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x1&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;#0               ; Compare x1 (contains iterations) with 0&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;b.le&lt;/span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;done&lt;/span&gt;                 &lt;span style=&#34;color:#75715e&#34;&gt;; If less or equal to 0, jump to done&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x0&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;x0&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;w1&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;sxtw&lt;/span&gt;     &lt;span style=&#34;color:#75715e&#34;&gt;; Set x0 = x0 + w1, sign-extending w1 to 64 bits&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;done:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ret&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If we keep &lt;code&gt;bumpA&lt;/code&gt; unchanged but make &lt;code&gt;FourLongs&lt;/code&gt; an identity record, C2 must prove that its identity has no impact on the computation. Compilers can often do this, but now we depend on the compiler proving it in each case. When I tried to do this (by removing &lt;code&gt;value&lt;/code&gt; from the &lt;code&gt;FourLongs&lt;/code&gt; declaration), C2 was not capable of performing this optimization.&lt;/p&gt;&#xA;&lt;p&gt;The following is an abridged excerpt of C2&amp;rsquo;s compilation of &lt;code&gt;IdentityRecordExperiment::run&lt;/code&gt;. The allocation remains in &lt;code&gt;run&lt;/code&gt;&amp;rsquo;s loop even though &lt;code&gt;bumpA&lt;/code&gt; has been inlined:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-asm&#34; data-lang=&#34;asm&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# {method} static &amp;#39;run&amp;#39; &amp;#39;(J)J&amp;#39; in &amp;#39;IdentityRecordExperiment&amp;#39;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;; initial FourLongs allocation&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ldr&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x0&lt;/span&gt;,  [&lt;span style=&#34;color:#66d9ef&#34;&gt;x28&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;#TLAB_TOP]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ldr&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x10&lt;/span&gt;, [&lt;span style=&#34;color:#66d9ef&#34;&gt;x28&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;#TLAB_END]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x11&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;x0&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;#0x28          ; reserve 40 bytes&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cmp&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x11&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;x10&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;b.hs&lt;/span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;slow_allocation&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;; object-header setup omitted&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;str&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x11&lt;/span&gt;, [&lt;span style=&#34;color:#66d9ef&#34;&gt;x28&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;#TLAB_TOP]   ; commit allocation&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;; object initialization omitted&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;; loop body: allocation from inlined bumpA&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ldr&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x0&lt;/span&gt;,  [&lt;span style=&#34;color:#66d9ef&#34;&gt;x28&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;#TLAB_TOP]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ldr&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x10&lt;/span&gt;, [&lt;span style=&#34;color:#66d9ef&#34;&gt;x28&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;#TLAB_END]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x11&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;x0&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;#0x28          ; reserve another 40 bytes&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cmp&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x11&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;x10&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;b.hs&lt;/span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;slow_allocation&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;; object-header setup omitted&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;str&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;x11&lt;/span&gt;, [&lt;span style=&#34;color:#66d9ef&#34;&gt;x28&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;#TLAB_TOP]   ; commit allocation&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;; object initialization omitted&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Clearly, providing the compiler with stronger semantic guarantees can sometimes produce a very big win.&lt;/p&gt;&#xA;&lt;h2 id=&#34;type-erasure-brings-the-allocation-back&#34;&gt;Type erasure brings the allocation back&#xA;&lt;/h2&gt;&lt;p&gt;Now we are going to look at something a bit more complex. This example is derived from an &lt;a class=&#34;link&#34; href=&#34;https://mail.openjdk.org/archives/list/valhalla-dev@openjdk.org/thread/3P5463B2OWURWM4LIMYIQMQOMZCOR6LN/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;email&lt;/a&gt; we received from a user on the valhalla-dev mailing list. He had ported a parsing library from Elm to Java and noticed a slowdown after converting all of his records to value records.&#xA;A performance regression is obviously not the result we want, but an unexpected result like this is fascinating: value classes give the JVM more semantic information and greater freedom, so how could using them make the program slower? I did a deep dive to find the cause and a source-level fix. This investigation has opened up an interesting compiler problem that my colleagues on the C2 team are now actively investigating. I&amp;rsquo;ll explain my findings here, but please keep in mind that I&amp;rsquo;ve had to simplify this greatly. The JVM and &lt;code&gt;javac&lt;/code&gt; are both fairly complex, so I have to leave out details.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;value record &lt;span style=&#34;color:#a6e22e&#34;&gt;LargeValue&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; a, &lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; b, &lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; c, &lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; d) {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;value record &lt;span style=&#34;color:#a6e22e&#34;&gt;Carrier&lt;/span&gt;(LargeValue v, &lt;span style=&#34;color:#66d9ef&#34;&gt;boolean&lt;/span&gt; b) {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Fun&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;R, F&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    R &lt;span style=&#34;color:#a6e22e&#34;&gt;apply&lt;/span&gt;(F value);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Frobber&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;extends&lt;/span&gt; Fun&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Carrier, LargeValue&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;FrobIt&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;implements&lt;/span&gt; Frobber {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; Carrier &lt;span style=&#34;color:#a6e22e&#34;&gt;apply&lt;/span&gt;(LargeValue value) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Carrier(value, &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;GrobIt&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;implements&lt;/span&gt; Frobber { &lt;span style=&#34;color:#75715e&#34;&gt;/* impl omitted on purpose */&lt;/span&gt; }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;DrobIt&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;implements&lt;/span&gt; Frobber { &lt;span style=&#34;color:#75715e&#34;&gt;/* impl omitted on purpose */&lt;/span&gt; }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is pretty simple code. We have multiple &lt;code&gt;Frobber&lt;/code&gt;s that take a &lt;code&gt;LargeValue&lt;/code&gt; and produce a &lt;code&gt;Carrier&lt;/code&gt;, which contains another &lt;code&gt;LargeValue&lt;/code&gt;. The &lt;code&gt;Frobber&lt;/code&gt; interface extends the &lt;code&gt;Fun&amp;lt;R, F&amp;gt;&lt;/code&gt; interface.&lt;/p&gt;&#xA;&lt;p&gt;Let&amp;rsquo;s look at this through the lens of the JVM so that we can understand what is happening. Java implements generics through type erasure, replacing these type parameters with &lt;code&gt;Object&lt;/code&gt;. That means that &lt;code&gt;Frobber&lt;/code&gt; effectively inherits this method as far as the JVM is concerned:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Frobber&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;extends&lt;/span&gt; Fun {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Object &lt;span style=&#34;color:#a6e22e&#34;&gt;apply&lt;/span&gt;(Object value);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The typed signature &lt;code&gt;Carrier apply(LargeValue)&lt;/code&gt; does not appear in the inherited JVM method descriptor and therefore has to be recovered through dynamic analysis. To accommodate this type discrepancy, &lt;code&gt;javac&lt;/code&gt; generates bridge methods in the class file. Each implementation gains a method approximately equivalent to this:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Generated by javac&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; Object &lt;span style=&#34;color:#a6e22e&#34;&gt;apply&lt;/span&gt;(Object value) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; apply((LargeValue) value);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The bridge accepts the erased argument, casts it to the expected type, and invokes the method we actually wrote. Now consider the method from the original reproducer:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;static&lt;/span&gt; Carrier &lt;span style=&#34;color:#a6e22e&#34;&gt;reproduce&lt;/span&gt;(LargeValue value, Frobber a, Frobber b) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Carrier c &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; a.&lt;span style=&#34;color:#a6e22e&#34;&gt;apply&lt;/span&gt;(value);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; b.&lt;span style=&#34;color:#a6e22e&#34;&gt;apply&lt;/span&gt;(c.&lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;());&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There are two separate interface call sites here. If each call site only observes one implementation, C2 can devirtualize it. For example, the first call site might always receive a &lt;code&gt;FrobIt&lt;/code&gt;, while the second always receives a &lt;code&gt;GrobIt&lt;/code&gt;. C2 can independently guard and inline both targets. In this experiment, C2 managed to remove all heap allocations. In pseudo-Java, the result looks like this. We represent scalarized values (values that are not heap references) by appending &lt;code&gt;Fields&lt;/code&gt; to the type name:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;static&lt;/span&gt; CarrierFields &lt;span style=&#34;color:#a6e22e&#34;&gt;reproduce&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        LargeValueFields value,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Frobber a,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Frobber b) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    guard(classOf(a) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; FrobIt.&lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    CarrierFields c &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; inline(FrobIt_apply(value));&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    guard(classOf(b) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; GrobIt.&lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; inline(GrobIt_apply(c.&lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;));&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At this level, there is no longer a call to the generated bridge. Devirtualizing and inlining the target also inlines its bridge. Once the bridge has disappeared into the surrounding compilation, there is no longer a real &lt;code&gt;Object apply(Object)&lt;/code&gt; call boundary.&lt;/p&gt;&#xA;&lt;p&gt;In the report we received, however, the call sites were megamorphic. In our example, that means &lt;code&gt;FrobIt&lt;/code&gt;, &lt;code&gt;GrobIt&lt;/code&gt;, and &lt;code&gt;DrobIt&lt;/code&gt; were all called interchangeably. With three hot implementations at each call site, C2 leaves the calls dynamically dispatched:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;invokeinterface Frobber.apply:(Object)Object&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The inherited method descriptor defines an ABI that requires callers to pass object references and implementations to return object references.&lt;/p&gt;&#xA;&lt;p&gt;The caller currently has a scalarized &lt;code&gt;LargeValue&lt;/code&gt;, but &lt;code&gt;Object apply(Object)&lt;/code&gt; cannot accept four independent scalar components. It requires a genuine reference. The caller must therefore materialize the value before making the call.&lt;/p&gt;&#xA;&lt;p&gt;The dynamically selected bridge then has to translate in the other direction:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Object &lt;span style=&#34;color:#a6e22e&#34;&gt;FrobIt_apply_bridge&lt;/span&gt;(Object argument) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    LargeValueFields value &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            scalarize((LargeValue) argument);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    CarrierFields result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            FrobIt_apply_typed(value);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; materializeCarrier(result);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The bridge casts the incoming reference to &lt;code&gt;LargeValue&lt;/code&gt;, extracts its components, and invokes the typed implementation using the scalarized value-object calling convention. The typed implementation returns a scalarized &lt;code&gt;Carrier&lt;/code&gt;, but the bridge itself promises to return &lt;code&gt;Object&lt;/code&gt;, so it must materialize the result before returning.&lt;/p&gt;&#xA;&lt;p&gt;The megamorphic version of &lt;code&gt;reproduce&lt;/code&gt; therefore looks approximately like this:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;static&lt;/span&gt; CarrierFields &lt;span style=&#34;color:#a6e22e&#34;&gt;reproduce&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        LargeValueFields value,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Frobber a,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Frobber b) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    LargeValue argument1 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; materializeLargeValue(value);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Object returned1 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            invokeinterface_apply_Object(a, argument1);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Carrier carrier1 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (Carrier) returned1;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    CarrierFields c &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; scalarize(carrier1);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    LargeValue argument2 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            materializeLargeValue(c.&lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Object returned2 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            invokeinterface_apply_Object(b, argument2);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Carrier carrier2 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (Carrier) returned2;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; scalarize(carrier2);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The bridge is acting as an ABI adapter. On one side is the erased &lt;code&gt;Object apply(Object)&lt;/code&gt; calling convention. On the other is the typed &lt;code&gt;Carrier apply(LargeValue)&lt;/code&gt; calling convention, where value components can be passed and returned in scalarized form.&lt;/p&gt;&#xA;&lt;p&gt;As you can imagine, this is very expensive. At each call, the caller has to turn a scalarized value into an object reference, which means materializing the value on the heap. It cannot simply point the callee at temporary stack storage: the call is opaque, so the callee may retain the reference and access it after the caller returns. The argument must therefore be a GC-managed heap object.&lt;/p&gt;&#xA;&lt;p&gt;Luckily, the fix is very simple! We avoid this by explicitly redeclaring the typed method in &lt;code&gt;Frobber&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Frobber&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;extends&lt;/span&gt; Fun&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Carrier, LargeValue&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Override&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Carrier &lt;span style=&#34;color:#a6e22e&#34;&gt;apply&lt;/span&gt;(LargeValue value);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;@Override&lt;/code&gt; annotation documents what we are doing, but the important part is the explicit method declaration. Calls whose static receiver type is &lt;code&gt;Frobber&lt;/code&gt; now use the typed descriptor directly:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;invokeinterface Frobber.apply:(LargeValue)Carrier&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The call is still megamorphic. C2 still does not know whether it will dispatch to &lt;code&gt;FrobIt&lt;/code&gt;, &lt;code&gt;GrobIt&lt;/code&gt;, or &lt;code&gt;DrobIt&lt;/code&gt;. But it no longer needs that knowledge to choose the correct calling convention. Every possible target accepts a &lt;code&gt;LargeValue&lt;/code&gt; and returns a &lt;code&gt;Carrier&lt;/code&gt;, so the values can cross the dynamic call boundary in scalarized form:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;static&lt;/span&gt; CarrierFields &lt;span style=&#34;color:#a6e22e&#34;&gt;reproduce&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        LargeValueFields value,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Frobber a,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Frobber b) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    CarrierFields c &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            invokeinterface_typed_apply(a, value);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; invokeinterface_typed_apply(b, c.&lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the reproducer, the erased megamorphic version allocated 192 bytes per invocation of &lt;code&gt;reproduce&lt;/code&gt;. Explicitly redeclaring the typed method reduced that to zero.&lt;/p&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&#xA;&lt;/h2&gt;&lt;p&gt;Declaring a value class is first and foremost a semantic decision. It tells our fellow programmers that its instances are defined entirely by their state and do not need identity. That clearer model is valuable in itself! The JVM’s additional freedom to optimize how those values are represented is a welcome bonus.&lt;/p&gt;&#xA;&lt;p&gt;C2 can do amazing things with that freedom, but it cannot always recover information hidden behind abstraction boundaries. Profiling and inspecting the generated code remain the best ways to understand what is happening.&lt;/p&gt;&#xA;&lt;p&gt;To get the best results, we may still need to have a little sympathy for the compiler.&lt;/p&gt;&#xA;&lt;h2 id=&#34;appendix-printing-c2-assembly&#34;&gt;Appendix: printing C2 assembly&#xA;&lt;/h2&gt;&lt;p&gt;If you want to double-check my work, you can take these code snippets and inspect the assembly yourself. Compile with preview enabled, make sure the target method is invoked often enough to become hot, and then ask the VM to compile and print it:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;javac --enable-preview --release 28 Example.java&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;java --enable-preview -Xbatch -XX:-TieredCompilation \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -XX:+UnlockDiagnosticVMOptions \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -XX:CompileCommand=compileonly,Example::method \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -XX:CompileCommand=print,Example::method \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -XX:+PrintAssembly Example&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;-Xbatch&lt;/code&gt; makes compilation synchronous, and &lt;code&gt;compileonly&lt;/code&gt; keeps the output focused. Note that &lt;code&gt;compileonly&lt;/code&gt; restricts which methods may be compiled; it does not trigger compilation. &lt;code&gt;Example::method&lt;/code&gt; must still be invoked enough times to reach the compilation threshold. Printing assembly requires a JVM build with a disassembler available. Omit &lt;code&gt;--enable-preview&lt;/code&gt; when compiling and running the ordinary-record comparison.&lt;/p&gt;&#xA;</description>
        </item><item>
            <title>You should be using Async UL when collecting GC logs!</title>
            <link>https://johan-sjolen.github.io/post/async-ul/async-ul-expanded/</link>
            <pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate>
            <guid>https://johan-sjolen.github.io/post/async-ul/async-ul-expanded/</guid>
            <description>&lt;h2 id=&#34;tldr&#34;&gt;TL;DR:&#xA;&lt;/h2&gt;&lt;p&gt;If you are on a modern JDK (25 or newer) and perform GC log analysis, you probably want to add this startup argument:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;-Xlog:async:stall&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It will make all GC logging asynchronous, while guaranteeing that no messages will be dropped. The older mode, which was accessed with &lt;code&gt;-Xlog:async&lt;/code&gt;, could potentially drop messages. This mode will instead stall producer threads to allow the output thread to catch up.&lt;/p&gt;&#xA;&lt;p&gt;Use &lt;code&gt;-XX:AsyncLogBufferSize=N&lt;/code&gt; to change the buffer size, an increased buffer size helps with temporary hiccups in output speed. A complete invocation could look something like this:&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;java -XX:AsyncLogBufferSize=8M -Xlog:async:stall -Xlog:gc*:file=gc-%p-%t.log:uptime,level,tags:filecount=5,filesize=20M&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Run &lt;code&gt;java -Xlog:help&lt;/code&gt; for more information.&lt;/p&gt;&#xA;&lt;h2 id=&#34;background&#34;&gt;Background&#xA;&lt;/h2&gt;&lt;p&gt;GC logs are one of those things that have become standard to enable for your Java application. You turn them on, forget about them, and then one day they are the only thing standing between you and a very annoying afternoon.&lt;/p&gt;&#xA;&lt;p&gt;Unfortunately, collecting logs is not free. The VM has to write the messages somewhere, and that work has to happen at some point. Under the default Unified Logging system (the machinery powering all VM logs, including GC), printing log messages is a fully synchronous process: The producer thread, meaning the thread that produces a log message, is also the one that outputs it. This can be extremely detrimental to pause times, as your GC must now await the log write to finish before continuing.&lt;/p&gt;&#xA;&lt;p&gt;In JDK 17, the UL system gained an asynchronous mode. Instead of making the producer thread also do the output work, the VM can enqueue the message and let a separate output thread flush it out.&lt;/p&gt;&#xA;&lt;p&gt;This is a very nice idea. The GC can go back to doing GC things, and the boring output work can happen somewhere else. In the happy case, this gives us what we want: lower disruption from the logging itself, while still getting the log file we asked for.&lt;/p&gt;&#xA;&lt;p&gt;Unfortunately, nothing is allowed to be quite that simple.&lt;/p&gt;&#xA;&lt;p&gt;The buffer used by asynchronous logging is bounded. It has to be, otherwise a sufficiently enthusiastic logging configuration could just turn memory into a sad little text warehouse. So, there is an obvious question: what happens when producer threads create messages faster than the output thread can write them?&lt;/p&gt;&#xA;&lt;p&gt;Before JDK 25, the answer was that producer threads would drop any messages that there isn&amp;rsquo;t room for.&lt;/p&gt;&#xA;&lt;p&gt;This is not a bad answer. If you ask for non-blocking logging, then non-blocking logging is what you get. The VM keeps moving, the application keeps moving, and the output thread can catch up. For some kinds of logging this is a perfectly reasonable trade-off. For GC logs, though, this can be the wrong trade-off.&lt;/p&gt;&#xA;&lt;p&gt;If I am collecting GC logs, it is usually because I want to analyze them later. Maybe I want to understand latency. Maybe I want to compare collectors. Maybe I want to explain why some service decided to have a dramatic little moment in production. In all of these cases, having a chunk of log messages dropped because your output thread stumbled a bit can have a real impact on your analysis.&lt;/p&gt;&#xA;&lt;p&gt;I thought that we can do better, so I decided to have a look at how we can alleviate any user&amp;rsquo;s worries regarding dropping asynchronous messages. I developed a solution which ships in JDK 25. In this release, Unified Logging gained another asynchronous mode. Instead of dropping messages when the buffer is full, it will stall all producer threads until the output thread has caught up.&lt;/p&gt;&#xA;&lt;p&gt;Now, instead of specifying &lt;code&gt;-Xlog:async&lt;/code&gt; you may append a mode to the startup argument, that mode either being &lt;code&gt;:drop&lt;/code&gt; or &lt;code&gt;:stall&lt;/code&gt;. The new mode is &lt;code&gt;:stall&lt;/code&gt;, that&amp;rsquo;s what you want to add if you don&amp;rsquo;t want any messages to be dropped. Plain &lt;code&gt;-Xlog:async&lt;/code&gt; is still the best-effort version.&lt;/p&gt;&#xA;&lt;p&gt;This may sound like we have just reinvented synchronous logging with extra steps, but we have not. Most of the time, logging still works asynchronously. The producer thread enqueues the log message and continues. The output thread handles the slow part. The stalling only matters when the buffer fills up, which is exactly the moment where the old behavior could silently lose data.&lt;/p&gt;&#xA;&lt;p&gt;So the trade-off becomes much nicer:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Compared with synchronous logging, most log messages do not have to pay the direct output cost.&lt;/li&gt;&#xA;&lt;li&gt;Compared with dropping asynchronous logging, the log file remains complete.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;This is what we in Sweden call a &amp;ldquo;lagom&amp;rdquo; solution. You get lower collection costs in the common case, and when the logging system is under pressure, you preserve the thing you actually wanted: a trustworthy log.&lt;/p&gt;&#xA;&lt;p&gt;If you already have a GC logging line, keep it. Add the async mode next to it. For example, the important part of the configuration may look like this:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;java -Xlog:async:stall -Xlog:gc*:file=gc-%p-%t.log:uptime,level,tags:filecount=5,filesize=20M&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is not only useful on multi-core machines. On a single-core system, asynchronous logging can still be a sensible arrangement. Since the output thread can sleep while waiting for a piece of output to be written to file, it will yield and the application threads can continue doing useful work.&lt;/p&gt;&#xA;&lt;h2 id=&#34;implementation-overview&#34;&gt;Implementation overview&#xA;&lt;/h2&gt;&lt;p&gt;The asynchronous logging implementation uses a ping-pong buffer design. There are two fixed-size buffers: one active buffer used by producer threads, and one flushing buffer used by the output thread from which messages are taken and written to output devices.&lt;/p&gt;&#xA;&lt;p&gt;At a high level, the normal flow looks like this:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;producer threads&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   | push log message&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   | signal: data available&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   v&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-------------+       output thread wakes&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;| active      |---------------------------+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;| buffer      |                           |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-------------+                           |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      ^                                   |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      | output thread swaps buffers &amp;lt;-----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      v                                   |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-------------+                           v&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;| flushing    | ----&amp;gt; output thread writes to output devices ---&amp;gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;| buffer      |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-------------+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When a producer thread emits a log message, it takes a lock, appends the message to the active buffer, signals that data is available, and then continues. The output thread waits for that signal. Once there is work to do, it takes the lock, swaps the active and staging buffers, and releases the lock again. The output thread competes with the producer threads for the lock. We could have been more clever here, but we&amp;rsquo;ve found that in practice there is no starvation, and this let&amp;rsquo;s the buffer be filled with more than just one message before the output thread claims the locks.&lt;/p&gt;&#xA;&lt;p&gt;After the swap, the output thread writes the contents of the staging buffer. This is the central point of the design: the lock is held while buffers are exchanged, but not while log output is written. Producer threads therefore avoid waiting for the ordinary I/O path in the common case.&lt;/p&gt;&#xA;&lt;p&gt;In &lt;code&gt;stall&lt;/code&gt; mode, a full active buffer is handled differently from the original dropping mode. If a message does not fit, the producer thread creates a temporary message area sized for that message, posts it for the output thread, and waits until it has been written. This prevents all other producer threads from progressing as well.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;active buffer full&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;producer thread -&amp;gt; [ one-off waiting message ] ---&amp;gt; output thread&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       ^                                            |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       |                                            v&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       +-------------- wake after write &amp;lt;-----------+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The output thread writes the ordinary buffer first, then writes the waiting message, and finally wakes the stalled producer thread. The result is that the common case remains asynchronous, while the full-buffer case becomes explicit waiting instead of silent message loss.&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
