<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[graalvm - Medium]]></title>
        <description><![CDATA[GraalVM team blog - https://www.graalvm.org - Medium]]></description>
        <link>https://medium.com/graalvm?source=rss----7122626bf34b---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>graalvm - Medium</title>
            <link>https://medium.com/graalvm?source=rss----7122626bf34b---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 22:25:32 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/graalvm" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[GraalVM 25.5 is here! ]]></title>
            <link>https://medium.com/graalvm/graalvm-25-5-is-here-19535117b14b?source=rss----7122626bf34b---4</link>
            <guid isPermaLink="false">https://medium.com/p/19535117b14b</guid>
            <category><![CDATA[graalvm]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Alina Yurenko]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 15:17:23 GMT</pubDate>
            <atom:updated>2026-11-03T15:33:43.545Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ei_iMmyd4sIdaTJm2rifmg.jpeg" /></figure><p>We’re happy to announce GraalVM 25.5!</p><p>This release brings new compiler optimizations, developer experience and observability improvements for Native Image, and updates across the GraalVM language ecosystem. Let’s take a closer look!</p><h3>Introducing Pyronaut: high-performance applications powered by GraalPy 🔥</h3><p>GraalPy is enabling new ways to build Python applications. <a href="https://pyronaut.io/">Pyronaut</a>, a new framework from the Micronaut project, is one of them. It combines GraalPy with Micronaut’s application model to bring <strong>integrated data access, validation, and observability to Python services</strong>. Python type hints and decorators define the application, while build-time processing checks its wiring, configuration, and database queries before startup. This catches many issues before the first request and reduces the amount of infrastructure teams need to assemble.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ImDQWg5as7BwrJeg3savig.png" /><figcaption><em>Pyronaut </em><a href="https://pyronaut.io/performance/"><em>performance</em></a><em> highlights</em></figcaption></figure><p>The GraalVM stack powers both execution and integration. GraalPy runs the Python code, the Graal JIT optimizes frequently executed code, and Micronaut provides a <strong>non-blocking Netty-based HTTP runtime and access to its broader ecosystem</strong>. Python applications can use Micronaut features and JVM libraries directly, while retaining familiar Python syntax, async/await, and tools such as pytest.</p><p>Pyronaut applications can <strong>run as JVM applications for peak performance, use a reusable </strong><a href="https://github.com/oracle/graal/issues/11327"><strong>Crema</strong></a><strong> native base for faster startup and lower memory</strong> use without rebuilding a native executable after every change, or be compiled as native executables. See the <a href="https://pyronaut.io/2026/10/02/introducing-pyronaut/">Pyronaut website</a> for examples and steps to get started.</p><h3>Even higher performance 🚀</h3><p>GraalVM 25.5 adds <strong>several new compiler optimizations</strong>, many of them focused on loops. Aggressive partial unrolling and simulation-based loop peeling give the compiler <strong>more opportunities to optimize repeated work</strong>. New loop transformations can also break up long-running loops to reduce safepoint overhead and reshape loops for further optimization. Improvements to range checks, memory reads and writes, and exact arithmetic can further reduce the work performed inside eligible loops while preserving application behavior.</p><p>Beyond loops, the compiler can now eliminate more locking, including nested locking of the same object and locks across simple control flow. It can also share equivalent code from converging branches, which may <strong>reduce the size of generated code</strong>. These optimizations are enabled by default; see <a href="https://www.graalvm.org/release-notes/25.5/">the release notes</a> for the full list and corresponding options.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tw-FPwYFtZV0eJ6spdcNmw.png" /><figcaption><em>GraalVM 25.5 performance using the </em><a href="https://github.com/barista-benchmarks/barista/tree/master/benchmarks/micronaut-similarity"><em>Micronaut Similarity</em></a><em> benchmark from the </em><a href="https://github.com/barista-benchmarks/barista"><em>Barista</em></a><em> suite.</em></figcaption></figure><p>We expect these optimizations to particularly <strong>benefit applications that spend significant time in loops, such as those processing arrays, performing calculations, transforming data, or doing LLM inference on the CPU</strong>. Workloads with frequent synchronization or many similar control-flow branches may also see lower overhead or smaller generated code.</p><h3>Even better runtime visibility 🔎</h3><p>JDK diagnostic tools such as jstack, jinfo, jps, and jstat can now run on Crema to inspect other JVM processes. On Linux and macOS, native executables built with --enable-monitoring=jvmstat can also read file-backed JVMStat performance data exposed by other HotSpot and Native Image processes.</p><h3>Native Image build and runtime updates 🤖</h3><p>There are a few other useful practical changes:</p><ul><li><strong>Clearer option errors.</strong> An unrecognized Native Image option beginning with - or -- now fails instead of being treated as a main class or output filename. If you intentionally want an output name beginning with a dash, pass it with -o, for example native-image -jar hello.jar -o --my-image, or prefix the positional name with ./.</li><li><strong>Bundle format 2.0.</strong> Bundles now record explicit path root kinds, build argument groups with their source platform, and markers for unavailable inputs. Older bundle formats remain readable.</li></ul><h3>WebAssembly in Oracle AI Database, powered by GraalVM 🚀</h3><p>Oracle AI Database 23.26.4 <a href="https://blogs.oracle.com/developers/whats-new-in-mle-23-26-4-webassembly-lands-in-oracle-ai-database">adds support</a> for <strong>WebAssembly modules in Multilingual Engine (MLE)</strong>, the GraalVM-powered runtime for executing JavaScript inside the database. Developers can now store suitable Wasm binaries as MLE modules, load their JavaScript bindings, and call them as part of database logic.</p><p>This can be useful when an application needs to reuse an existing Wasm library directly where its data lives — for example, for data transformation, validation, or specialized processing — without moving the work to a separate service.</p><h3>Community Updates 🤝</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zmP7i93SgeMY32m5twveGQ.jpeg" /><figcaption>GraalVM Community Summit participants</figcaption></figure><p>Every autumn brings one of our favorite annual events — <a href="https://www.graalvm.org/community/summit/"><strong>GraalVM Community Summit</strong></a>. This year, we welcomed almost 70 committers, partners, and vendors representing companies such as Amazon, Apple, BellSoft, Broadcom, Elide, Google, HEBI Robotics, IBM, JetBrains, Netflix, Red Hat, Saxonica, and others. We spent two days discussing new features and future plans, and now are more energized than ever to make GraalVM even more useful for developers around the world 🙂</p><h3>Conclusion</h3><p>We’d like to take this opportunity to thank our amazing contributors and community for all the feedback, suggestions, and contributions that went into this release.</p><p>If you have feedback for this release or suggestions for features that you would like to see in future releases, please share them with us on <a href="https://graalvm.org/slack-invitation">Slack</a>, <a href="https://github.com/oracle/graal">GitHub</a>, or <a href="https://www.linkedin.com/company/graalvm/">LinkedIn</a>.</p><p>Now go ahead and try the <a href="https://www.graalvm.org/downloads/">new GraalVM</a>! 🚀</p><p>— the GraalVM team</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=19535117b14b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/graalvm/graalvm-25-5-is-here-19535117b14b">GraalVM 25.5 is here! 🎉</a> was originally published in <a href="https://medium.com/graalvm">graalvm</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Chasing the Perfect Assembly]]></title>
            <link>https://medium.com/graalvm/chasing-the-perfect-assembly-d0edb5387a5e?source=rss----7122626bf34b---4</link>
            <guid isPermaLink="false">https://medium.com/p/d0edb5387a5e</guid>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[compilers]]></category>
            <category><![CDATA[graalvm]]></category>
            <category><![CDATA[interpreters]]></category>
            <dc:creator><![CDATA[Yudi Zheng]]></dc:creator>
            <pubDate>Mon, 02 Nov 2026 13:57:05 GMT</pubDate>
            <atom:updated>2026-11-02T13:57:04.713Z</atom:updated>
            <content:encoded><![CDATA[<h4><em>Matching the Speed of Hand-Written Assembly with an Interpreter Written in Java</em></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WOLaScBzr_UqNQfhdIXHjg.jpeg" /></figure><p>Previously, in the “<a href="https://medium.com/graalvm/escaping-the-giant-switch-dec20b572139">Escaping the Giant Switch</a>” episode, we learned how to break one enormous interpreter compilation unit into many independently compiled bytecode handlers.</p><p>This left us with an appealing hypothesis: if we can find the locally optimal assembly for each handler, putting those handlers together should give us a globally optimal interpreter.</p><p>We put that hypothesis to the test with the <a href="https://github.com/oracle/graal/issues/11327">Crema</a> interpreter, with one constraint: its Java source code had to stay maintainable.</p><p>As compiler engineers, we occasionally entertain a slightly arrogant belief: programmers should write straightforward Java, and our extremely smart compiler should handle the rest.</p><p>But when the emitted assembly falls short, who should change?</p><p>Should the compiler learn a more general optimization, or should the interpreter expose a fact that only it knows?</p><h3>Act I: The Expandables</h3><p>Let us start with iconst_0, one of the simplest Java bytecodes. It pushes the integer zero onto the operand stack.</p><p>For this example, we need only a compact model of the interpreter frame and stack state:</p><pre>class Frame {<br>    /* Raw primitive values in local-variable and operand-stack slots. */<br>    final long[]   prims;<br>    /* Object references in local-variable and operand-stack slots. */<br>    final Object[] refs;<br>    /* Bytecode of the method being interpreted. */<br>    final byte[]   code;<br>}<br><br>class Stack {<br>    /* Next free operand-stack slot in Frame.prims/refs. */<br>    int top;<br>}<br><br>static int iconst0Handler(int bci, Frame frame, Stack stack) {<br>    int slot = stack.top;<br>    frame.prims[slot] = 0;<br>    stack.top = slot + 1;<br>    return bci + 1;<br>}</pre><p>Here the bci parameter is the bytecode index, the byte offset of the current instruction within the method’s bytecode array.</p><p>When frame and stack cross the handler boundary as ordinary arguments, the relevant fast path assembly is:</p><pre>    ; regBCI:   incoming bci<br>    ; regFrame: incoming frame<br>    ; regStack: incoming stack<br>    mov     regPrims, QWORD PTR [regFrame + OFFSET_prims]              ; load frame.prims<br>▶   mov     regTop32, DWORD PTR [regStack + OFFSET_top]                ; load stack.top<br>    test    regPrims, regPrims                                         ; is prims null?<br>    je      null_prims<br>    cmp     regTop32, DWORD PTR [regPrims + OFFSET_length]             ; is top out of bounds?<br>    jae     out_of_bounds<br>    mov     QWORD PTR [regPrims + regTop * 8 + OFFSET_arrayStart], 0   ; prims[slot] = 0<br>    inc     regTop32                                                   ; advance the stack top<br>▶   mov     DWORD PTR [regStack + OFFSET_top], regTop32                ; write back stack.top<br>    inc     regBCI32                                                   ; advance to the next bytecode<br>    ret<br>    ; Here and below, reg*32 denotes the lower 32 bits of the corresponding register.</pre><p>For a bytecode that merely pushes zero, this is quite an entourage. Updating stack.top, for example, requires loading the field into a register, incrementing it, and storing it back.</p><p>In the previous article, we taught objects to cross compilation boundaries in pieces, while keeping the handler’s Java signature intact. We call this <em>argument expansion</em>, and distinguish two forms according to whether the original object also crosses the boundary.</p><p>With <em>virtual expansion</em>, the fields travel independently, without the original object reference. The generated stub reconstructs the object around those values and calls the original handler; after inlining, escape analysis can eliminate the reconstructed object.</p><p>With <em>materialized expansion</em>, the original object reference travels alongside selected fields. The handler receives those fields directly while retaining access to the backing object and its other fields.</p><p>For Frame, we choose materialized expansion and recruit prims , refs, and code as <strong>the expandables</strong>. For Stack, we choose virtual expansion. It sends top to join the expandables but stays behind itself.<br>Together, these choices change the handler’s boundary tuple from &lt;bci, frame, stack&gt; to &lt;bci, frame, frame.prims, frame.refs, frame.code, stack.top&gt;.</p><p>The tuple grew. The assembly shrank.</p><p>With the specialized calling convention, these values arrive in the registers where the handler needs them:</p><pre>    ; regBCI:   incoming bci<br>    ; regFrame: incoming frame<br>    ; regPrims: incoming frame.prims<br>    ; regRefs:  incoming frame.refs<br>    ; regCode:  incoming frame.code<br>    ; regTop:   incoming stack.top<br>▶   test    regPrims, regPrims                                         ; is prims null?<br>▶   je      null_prims<br>    cmp     regTop32, DWORD PTR [regPrims + OFFSET_length]             ; is top out of bounds?<br>    jae     out_of_bounds<br>    mov     regSlot32, regTop32                                        ; zero-extend top into regSlot<br>    mov     QWORD PTR [regPrims + regSlot * 8 + OFFSET_arrayStart], 0  ; prims[slot] = 0<br>    inc     regTop32                                                   ; advance the stack top<br>    inc     regBCI32                                                   ; advance to the next bytecode<br>    ; Threaded dispatch replaces the return with a jump to the next handler<br>    jmp     tail_call_target(regCode, regBCI)</pre><p>stack.top now arrives in the lower 32 bits of regTop, and its updated value stays there for the next handler.</p><p>The field traffic is now gone, but the null check and bounds check are still there.</p><p>Native Image’s global data-flow analysis can prove some references non-null. However, when that proof is unavailable, the compiler must retain the null check.</p><p>Fortunately, argument expansion lets us specify non-nullness as part of the handler interface. This contract applies both to direct reference arguments such as frame and to expanded reference fields such as frame.prims.<br>In Graal terminology, the parameter receives a non-null <em>stamp</em>, which describes the values that parameter can hold. The handler compilation can use that stamp to eliminate the internal null check.</p><p>The check still has to happen. Just not in every handler. The compiler inserts null checks before handler calls from the dispatch loop and before threaded tail calls. For a field such as Frame.prims, which is final, the outgoing expanded argument is still the same non-null reference. The compiler can therefore fold away the null check before the outgoing threaded tail call too.</p><p>With argument expansion and the non-null contract, the relevant threaded-handler assembly is:</p><pre>    ; regBCI:   incoming bci<br>    ; regFrame: incoming frame       (non-null)<br>    ; regPrims: incoming frame.prims (non-null)<br>    ; regRefs:  incoming frame.refs  (non-null)<br>    ; regCode:  incoming frame.code  (non-null)<br>    ; regTop:   incoming stack.top<br>▶   cmp     regTop32, DWORD PTR [regPrims + OFFSET_length]             ; is top out of bounds?<br>▶   jae     out_of_bounds<br>    mov     regSlot32, regTop32                                        ; zero-extend top into regSlot<br>    mov     QWORD PTR [regPrims + regSlot * 8 + OFFSET_arrayStart], 0  ; prims[slot] = 0<br>    inc     regTop32                                                   ; advance the stack top<br>    inc     regBCI32                                                   ; advance to the next bytecode<br>    jmp     tail_call_target(regCode, regBCI)</pre><p>For these unchanged references, argument expansion and the non-null contract effectively hoist the checks out of the threaded handler chain.</p><p>Less assembly. Same Java.</p><p>So far, the division of labor works.</p><p>But what about the bounds check?</p><h3>Act II: Handler, Compiler, Verifier, Witness</h3><p>The comparison between slot and prims.length is there for a reason. Java requires lastore to <a href="https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-6.html#jvms-6.5.lastore">throw</a> an ArrayIndexOutOfBoundsException when the index is outside the array bounds.</p><p>That requirement comes from the host Java code implementing the interpreter; the guest instruction iconst_0 merely pushes zero and prescribes no such exception. An out-of-bounds access here would mean an interpreter invariant had been broken, but unless the compiler can prove that slot is valid, the check stays.</p><p>Keeping that check costs more than the comparison alone. For instance, the cold exception path may keep values alive on the fast path, increasing register pressure and potentially causing spills.</p><p>So how can the compiler prove that slot is a valid index and avoid this cost?</p><p>If slot is constant and the array length is known, the proof can be immediate; if, in a counted loop, range analysis proves that every value of slot stays within the array, the check can be eliminated; if an earlier <em>guard</em> established 0 &lt;= slot &amp;&amp; slot &lt; prims.length, the compiler can reuse it for every access it dominates.</p><p>All of these optimizations require the compiler to derive the proof from information in its graph.</p><p>Our tiny handler, however, is compiled as a standalone unit. Its graph receives stack.top and frame.prims as independent inputs, with no proof that the resulting slot is a valid index into the array.</p><p>Fortunately, the Java bytecode verifier can serve as a <em>witness</em> outside that graph.</p><p>Before the JVM executes a method’s bytecode, the bytecode verifier establishes a valid frame shape at every reachable bytecode index. Among other properties, it proves that every operand-stack operation stays within the max_stack recorded in the method’s class-file Code attribute. As long as an interpreter allocates operand-stack storage for that same max_stack and implements the verified operations faithfully, every operand-stack access is within the allocated storage.</p><p>In that sense, verification plays the role of a <em>guard</em> that dominates the entire interpretation. But it lives outside the handler’s compilation unit, so the compiler cannot reuse it as a dominating guard in the handler graph.</p><p>Could we use the same stamp-based contract that previously removed the null checks?</p><p>Just as a stamp can express non-nullness, it can also express a constant integer range such as [0, 63]. However, the required range is [0, prims.length), whose upper bound is a runtime value rather than a compiler constant. The stamp system cannot express this relationship between two runtime values.</p><p>For an ordinary Java array access, without a proof in the graph, the compiler must retain precisely the comparison and branch we want to remove.</p><p>Alternatively, we can make the verifier-established invariant an implicit assumption in the interpreter source, replacing the checked array write with an Unsafe-backed access:</p><pre>static int iconst0Handler(int bci, Frame frame, Stack stack) {<br>    int slot = stack.top;<br>    UNSAFE.putLong(frame.prims, slot * 8L + OFFSET_arrayStart, 0);<br>    stack.top = slot + 1;<br>    return bci + 1;<br>}</pre><p>The bounds check no longer enters the handler’s graph, and the relevant assembly becomes:</p><pre>    ; regBCI:   incoming bci<br>    ; regFrame: incoming frame       (non-null)<br>    ; regPrims: incoming frame.prims (non-null)<br>    ; regRefs:  incoming frame.refs  (non-null)<br>    ; regCode:  incoming frame.code  (non-null)<br>    ; regTop:   incoming stack.top<br>▶   movsxd  regSlot, regTop32                                          ; sign-extend top into regSlot<br>    mov     QWORD PTR [regPrims + regSlot * 8 + OFFSET_arrayStart], 0  ; prims[slot] = 0<br>    inc     regTop32                                                   ; advance the stack top<br>    inc     regBCI32                                                   ; advance to the next bytecode<br>    jmp     tail_call_target(regCode, regBCI)</pre><p>The array-length load, conditional branch, and cold exception edge have all disappeared. Correctness now rests on verification, frame allocation, and the interpreter preserving the verified stack discipline.</p><h4>One Move Too Many</h4><p>The bounds check is gone. But the zero-extension has quietly become a sign-extension.</p><p>Why?</p><p>Consider an input stack.top = -1, giving us slot = -1. Sign-extension preserves that value as 0xFFFFFFFF_FFFFFFFF, while zero-extension produces 0x00000000_FFFFFFFF. Using the latter in the Unsafe access would compute the wrong address, potentially causing a segmentation fault or memory corruption.</p><p>Of course, this input cannot occur in a correctly implemented interpreter executing verified bytecode. But that guarantee remains implicit in the source, so the compiler cannot use it to rule out negative inputs and must preserve Java’s signed conversion.</p><p>Before the switch to Unsafe, the compiler had to insert a bounds check.<br>Once that check passed, it knew that slot was non-negative at the array access. For a non-negative int, sign-extension and zero-extension produce the same 64-bit value, so it could replace the former with the latter. On processors that support move elimination, the resulting register move can be handled during register renaming, avoiding an execution step that sign-extension would require.</p><p>Zero-extension also gives the compiler an opportunity to eliminate the move entirely — a small prize, but one that can be significant in benchmarks such as <a href="https://www.spec.org/jvm2008/">SPECjvm</a>. On x64, writing a 32-bit register also clears its upper 32 bits. If the compiler knows that the instruction that produced slot already did this, it may use that register directly as the address index.</p><p>Then why couldn’t the compiler eliminate the zero-extending move in our expanded handler before the switch to Unsafe?</p><p>Our stack.top arrives as an expanded parameter, so its producing instruction is outside this compilation unit. The calling convention promises an int in the lower 32 bits of regTop; it does not promise that the upper 32 bits are zero. Consider another input, stack.top = 1. The incoming register could contain 0x00000001_00000001, with the correct int value in its lower half. Using the entire register as an address index would include those non-zero upper bits, potentially causing a segmentation fault or memory corruption.</p><p>Proving that stack.top already arrives in a register containing its correctly extended, full-width value would require reasoning across compilation boundaries. We can avoid that proof by making the full-width value part of the handler interface. Changing the index’s representation to long preserves the invariants established by verification. Plus, with Unsafe, we are no longer constrained by Java’s int array indices, since the access takes a long byte offset.</p><p>We therefore changed both stack.top and bci to long, so each arrives as a 64-bit index ready for addressing. Here too, the verifier testifies that branch targets stay within the bytecode array. Neither index needs widening in each handler.</p><p>Our running example now becomes:</p><pre>class Stack {<br>    long top;<br>}<br><br>static long iconst0Handler(long bci, Frame frame, Stack stack) {<br>    long slot = stack.top;<br>    UNSAFE.putLong(frame.prims, slot * 8L + OFFSET_arrayStart, 0);<br>    stack.top = slot + 1;<br>    return bci + 1;<br>}</pre><p>The incoming stack top can serve directly as the address index:</p><pre>    ; regBCI:   incoming bci         (long)<br>    ; regFrame: incoming frame       (non-null)<br>    ; regPrims: incoming frame.prims (non-null)<br>    ; regRefs:  incoming frame.refs  (non-null)<br>    ; regCode:  incoming frame.code  (non-null)<br>    ; regTop:   incoming stack.top   (long)<br>    mov     QWORD PTR [regPrims + regTop * 8 + OFFSET_arrayStart], 0<br>    inc     regTop<br>    inc     regBCI<br>    jmp     tail_call_target(regCode, regBCI)  ; long bci also avoids widening in the unshown dispatch code</pre><p>Store zero. Advance the indices. Dispatch.</p><p>That is more like it.</p><h4>Supplementary Testimony</h4><p>The verifier establishes more than stack capacity.<br>For an interpreter, those guarantees fall into two useful categories:</p><ul><li><em>Access bounds</em>: operand-stack and local-variable accesses stay within the storage allocated from max_stack and max_locals; bytecode operands form complete instructions; branch and exception-handler targets name valid instruction boundaries; and constant-pool indices refer to valid entries.</li><li><em>Type safety</em>: every instruction consumes and produces compatible verification types; stack-manipulation forms are legal; field accesses, invocations, and returns agree with their descriptors; and uninitialized references obey the constructor rules.</li></ul><p>The type-safety proofs are normally implicit in the handler implementation. For example, an iadd handler expects two integers on the operand stack without checking their runtime types.</p><p>Other type guarantees belong to a particular bytecode site rather than to every execution of an instruction. How can we make those facts useful to a shared handler?</p><p>Consider baload, which operates on either a byte[] or a boolean[].<br>Its shared handler contains a type check to distinguish the two array kinds at run time. At a given BCI, however, verification fixes the non-null array operand’s type as either byte[] or boolean[]; it cannot admit both at the same site. Different baload sites may have different array types, so this knowledge cannot be baked into a single shared handler. The guest-array null and bounds checks are still required.</p><p>We could use the verifier’s result to rewrite baload at that BCI to a byte[]- or boolean[]-specific opcode before execution begins.</p><p>Alternatively, we can record this site-specific information through runtime <strong>quickening</strong>.</p><p>On the first execution with a non-null array, the generic handler observes its type and rewrites that BCI to select one of two shared specialized handlers. A single runtime observation would not normally justify permanent specialization; the verifier’s BCI-specific guarantee makes it sufficient here. Subsequent executions use the specialized handler without repeating the type check. The specialized opcode carries the site’s type information into handler selection.</p><p>The same pattern applies when the initial execution establishes information that does not come from verification. For a field access, the original handler resolves the field, performs the opcode-specific access checks, and then quickens the opcode. The quickened handler retrieves the cached resolved entry without repeating resolution or those access checks.</p><p>To summarize, the compiler must generate correct code for every input its interface permits, while the interpreter’s external guarantees rule out some of those inputs. When the compiler cannot see those guarantees, we can still use them to choose a different implementation.</p><p>We have done so through unchecked accesses, wider parameters, and specialized opcodes, putting external knowledge to work without teaching the compiler its original proofs.</p><p>But how far should we push this?</p><h3>Act III: How to Train Your Compiler</h3><p>Let us return to the iconst_0 example. Can we make its assembly even simpler?</p><p>Suppose we carried the address of the next primitive element instead of its logical slot. In C-like code, initializing the pointer and pushing zero would look something like this:</p><pre>int64_t *stack = &amp;frame.prims[top];  // Initialize from the logical top outside of the handlers<br>...<br>*stack = 0;                          // Push zero<br>stack++;                             // Advance by eight bytes</pre><p>With the pointer carried in regStack across threaded tail calls, pushing zero and advancing it could compile to:</p><pre>    mov     QWORD PTR [regStack], 0<br>    add     regStack, 8</pre><p>The simpler addressing mode does not necessarily improve performance.<br>On Intel Skylake, both the original indexed store and this direct-address store execute as two micro-operations. The direct-address form can use an additional store-address port, which may help under contention, but does not guarantee a cycle saving.</p><p>The interpreter’s Java implementation, meanwhile, becomes more cumbersome. The stack top no longer names a logical slot. To address both primitive and reference arrays directly, we now have to maintain two pointers, advancing each according to its array’s element size. We must also keep these pointers into the managed heap valid across garbage collection and safepoints.</p><p>Making the source so much harder to read is a poor trade when we cannot even establish a performance gain. The choice becomes less clear when keeping the source tidy leaves avoidable work in the generated code.</p><h4>The Pursuit of Constants</h4><p>Consider putfield, whose handler branches according to the kind of field it stores into. An int occupies one stack slot, while a long occupies two, so their values start at stack.top — 1 and stack.top-2, respectively.<br>In either case, the receiver sits in the preceding slot.</p><p>Using our Frame and Stack abstraction, a natural implementation is to share the slot calculations and receiver lookup before branching on the field kind:</p><pre>static long putfieldHandler(long bci, Frame frame, Stack stack) {<br>    JavaField field           = resolveField(frame, bci);<br>    JavaKind  kind            = field.kind();<br>    long      valueSlot       = stack.top - kind.slotCount();<br>    long      receiverSlot    = valueSlot - 1;<br>    Object    receiver        = UNSAFE.getReference(frame.refs, receiverSlot * OFFSET_referenceScale + OFFSET_arrayStart);<br>    Object    nonNullReceiver = nullCheck(receiver);<br>    long      offset          = field.offset();<br>    boolean   isVolatile      = field.isVolatile();<br><br>    switch (kind) {<br>        case Int -&gt; {<br>            // OFFSET_endian is 4 on big-endian targets, otherwise 0.<br>            int value = UNSAFE.getInt(frame.prims, valueSlot * 8L + OFFSET_arrayStart + OFFSET_endian);<br>            if (isVolatile) {<br>                UNSAFE.putIntVolatile(nonNullReceiver, offset, value);<br>            } else {<br>                UNSAFE.putInt(nonNullReceiver, offset, value);<br>            }<br>        }<br>        case Long -&gt; {<br>            long value = UNSAFE.getLong(frame.prims, valueSlot * 8L + OFFSET_arrayStart);<br>            if (isVolatile) {<br>                UNSAFE.putLongVolatile(nonNullReceiver, offset, value);<br>            } else {<br>                UNSAFE.putLong(nonNullReceiver, offset, value);<br>            }<br>        }<br>        // Other valid field kinds omitted.<br>        default -&gt; throw shouldNotReachHere();<br>    }<br>    UNSAFE.putReference(frame.refs, receiverSlot * OFFSET_referenceScale + OFFSET_arrayStart, null);<br>    stack.top = receiverSlot;<br>    return bci + 3;<br>}</pre><p>The relevant x64 assembly snippet leading into the switch is the following.</p><pre>    ; regBCI:   incoming bci         (long)<br>    ; regFrame: incoming frame       (non-null)<br>    ; regPrims: incoming frame.prims (non-null)<br>    ; regRefs:  incoming frame.refs  (non-null)<br>    ; regCode:  incoming frame.code  (non-null)<br>    ; regTop:   incoming stack.top   (long)<br>    ; regKindObject holds the decompressed JavaKind reference; setup and null checks omitted.<br>    ; ... receiver decompression omitted ...<br>▶   mov     QWORD PTR [rsp + 0x28], regFieldObject  ; spill decompressed field reference<br>    movsxd  regSlots,      DWORD PTR [regKindObject + OFFSET_slotCount]<br>    mov     regValueSlot, regTop<br>    sub     regValueSlot, regSlots          ; valueSlot = top - kind.slotCount()<br>    mov     regReceiver32, DWORD PTR [regRefs + regValueSlot * 4 + OFFSET_arrayStart - 4]<br>    mov     regReceiverSlot, regValueSlot<br>    dec     regReceiverSlot                 ; receiverSlot = valueSlot - 1<br>    test    regReceiver32, regReceiver32    ; is receiver null?<br>    je      null_receiver<br><br>▶   mov     QWORD PTR [rsp + 0x60], regTop          ; spill stack top<br>    ; ... field offset/modifier loads omitted ...<br>    mov     regKind32,     DWORD PTR [regKindObject + OFFSET_ordinal]<br>    ; ... ordinal bounds check omitted ...<br>▶   mov     QWORD PTR [rsp + 0x10], regKindObject   ; spill decompressed JavaKind reference<br>    ; ... ordinal jump table, typed stores, and threaded dispatch omitted ...</pre><p>At first glance, that is exactly what we asked for. It loads the slot count from JavaKind, then computes valueSlot and receiverSlot before the switch. The receiver load folds the subtraction into its address, but receiverSlot is still computed separately for the later update to stack.top.</p><p>Yet once we enter a switch arm, the slot count is fixed, giving us an opportunity for constant folding. In the int case, for example, replacing the slot count with one would simplify valueSlot to stack.top-1 and receiverSlot to stack.top-2. The memory accesses could then absorb those constant offsets into their address displacements. For the receiver, that gives DWORD PTR [regRefs + regTop * 4 + OFFSET_arrayStart-8]. The stack update could subtract two directly from top at the end.</p><p>That suggests a manual rewrite. Move the shared setup into the branches and specialize it for each kind:</p><pre>static long putfieldHandler(long bci, Frame frame, Stack stack) {<br>    JavaField field = resolveField(frame, bci);<br>    JavaKind kind = field.kind();<br>    // Verification and linking guarantee a valid field kind.<br>    switch (kind) {<br>        case Int -&gt; {<br>            long receiverSlot = stack.top - 2;<br>            long valueSlot = stack.top - 1;<br>            Object receiver = UNSAFE.getReference(frame.refs, receiverSlot * OFFSET_referenceScale + OFFSET_arrayStart);<br>            Object nonNullReceiver = nullCheck(receiver);<br>            int value = UNSAFE.getInt(frame.prims, valueSlot * 8L + OFFSET_arrayStart + OFFSET_endian);<br>            if (field.isVolatile()) {<br>                UNSAFE.putIntVolatile(nonNullReceiver, field.offset(), value);<br>            } else {<br>                UNSAFE.putInt(nonNullReceiver, field.offset(), value);<br>            }<br>            UNSAFE.putReference(frame.refs, receiverSlot * OFFSET_referenceScale + OFFSET_arrayStart, null);<br>            stack.top = receiverSlot;<br>        }<br>        case Long -&gt; {<br>            long receiverSlot = stack.top - 3;<br>            long valueSlot = stack.top - 2;<br>            Object receiver = UNSAFE.getReference(frame.refs, receiverSlot * OFFSET_referenceScale + OFFSET_arrayStart);<br>            Object nonNullReceiver = nullCheck(receiver);<br>            long value = UNSAFE.getLong(frame.prims, valueSlot * 8L + OFFSET_arrayStart);<br>            if (field.isVolatile()) {<br>                UNSAFE.putLongVolatile(nonNullReceiver, field.offset(), value);<br>            } else {<br>                UNSAFE.putLong(nonNullReceiver, field.offset(), value);<br>            }<br>            UNSAFE.putReference(frame.refs, receiverSlot * OFFSET_referenceScale + OFFSET_arrayStart, null);<br>            stack.top = receiverSlot;<br>        }<br>        // Other valid field kinds omitted.<br>        default -&gt; throw shouldNotReachHere();<br>    }<br>    return bci + 3;<br>}</pre><p>For the int case, the resulting addresses are:</p><pre><br>    ; Int arm after duplicating the setup; other instructions omitted.<br>    ; Same incoming registers; regTop still holds the incoming stack.top.<br>    ; OFFSET_arrayStart = 8 and reference scale = 4 in this build.<br>    mov     regReceiver32, DWORD PTR [regRefs + regTop * 4]  ; disp = 8 - 4 * 2<br>    ; ... receiver decompression omitted ...<br>    test    regReceiver32, regReceiver32                   ; is receiver null?<br>    je      null_receiver_int<br>    mov     regValue32,    DWORD PTR [regPrims + regTop * 8] ; disp = 8 - 8 * 1<br>    ; ... stack update, volatility test, and field store omitted ...</pre><p>The other arms fold their constant slot offsets into the addresses in the same way.</p><p>More Java. Less assembly.</p><p>But why can’t the compiler do this for us?</p><p>Graal can fold a final field read when the object containing the field is a known constant. But javac lowers an enum switch to an integer switch derived from ordinal(), typically through a synthetic mapping array. Entering the int case constrains this integer; it does not turn kind into the constant JavaKind.Int in the compiler graph. The slotCount read used to compute valueSlot therefore still has a dynamic receiver.</p><p>Teaching the compiler to map an ordinal back to its JavaKind instance could enable branch-local constant folding. That would require trusting the enum’s canonical-instance invariant, much as final-field folding trusts that a field’s value will not change.</p><blockquote>HotSpot’s C2 compiler does not trust arbitrary instance final fields for constant folding by default, though it makes exceptions for certain classes. Its experimental -XX:+TrustFinalNonStaticFields option broadens that trust. Graal uses a different policy.</blockquote><p>Even if the compiler could establish the enum identity in each branch, that alone would solve only half of our problem. It could specialize valueSlot within each branch, but the shared receiver access would still use the dynamic slot count before the switch. Eliminating that load would require duplicating the receiver access and null check into the branches as well.<br>The default branch would need to preserve the original calculation and null check before throwing its own exception. This is possible, but it requires teaching the compiler when that duplication is worth the extra code.</p><p>Let us leave that as a feature request for the compiler.</p><h4>A Little Too Helpful</h4><p>While we admire the result of constant folding, the compiler may be undoing some of our careful duplication. For instance, each branch calls field.offset(), which reads the same field offset from the resolved metadata object. Writing that call inside each arm does not necessarily keep the load there.</p><p>Graal can turn an eligible field load into a <em>floating read</em>: a load whose position is determined by its dependencies rather than fixed to a point in the control flow. Equivalent reads with matching dependencies can then become one shared node through <em>global value numbering</em>.</p><p>As long as the value remains shared, the scheduler must place it where it is available to all its uses. When those uses lie in different switch arms, this means placing it in a common dominator before the switch, provided its dependencies allow it. Sharing the load saves instruction bytes, but its result now has to stay live across the dispatch. In the duplicated handler, the field offset and modifier reads have moved before the switch:</p><pre>    ; Before kind dispatch; regField holds the compressed field reference.<br>    ; save values before reusing their registers.<br>▶   mov     QWORD PTR [rsp + 0x20], regKindObject   ; spill decompressed JavaKind reference<br>▶   mov     DWORD PTR [rsp + 0x1c], regKindRef32    ; spill compressed JavaKind reference<br>▶   mov     QWORD PTR [rsp + 0x28], regFieldObject  ; spill decompressed field reference<br>▶   mov     QWORD PTR [rsp + 0x30], regFrame        ; spill frame reference<br>    movsxd  regOffset,  DWORD PTR [regHeapBase + regField * 8 + OFFSET_fieldOffset]<br>    mov     regFlags32, 0x50df<br>    and     regFlags16, WORD PTR [regHeapBase + regField * 8 + OFFSET_modifiers]<br>    movzx   regFlags32, regFlags16<br>    mov     regValueSlot, regTop<br>    dec     regValueSlot                   ; top - 1<br>    lea     regReceiverSlot, [regTop - 2]  ; shared by the one-slot arms<br>    lea     regNextBCI, [regBCI + 3]<br>    ; ... ordinal range check and jump-table dispatch ...</pre><p>The same can happen to arithmetic. Across the one-slot cases — boolean, byte, char, and others — the same stack.top-2 calculation appears for the outgoing stack top. If these calculations become one shared node, it must execute before the switch, even though not every arm needs the result.</p><p>Graal’s partial-redundancy-aware scheduler can undo this sharing by duplicating a computation into the branches that need it. Its general heuristic favors splitting when doing so reduces execution frequency. The stack.top-3 calculation shared by the long and double arms illustrates this: the scheduler splits it back into those arms, avoiding the calculation on the more frequent one-slot paths. But the shared stack.top-2 calculation serves most of the execution, so splitting it offers too little frequency saving — even though it would shorten live ranges.</p><p>Keeping the computation shared is a reasonable choice until registers become scarce. The jump-table dispatch generated from the switch needs its selector and a register for the target address, alongside the interpreter state carried to the next handler. Hoisted loads and slot calculations add to that live set even though only one arm will execute. If their overlapping lifetimes exceed the available registers, the allocator must spill something, which is unfortunately the case in many handlers with complicated branching logic. The compiler’s good intentions in sharing computations then turn into extra stores and reloads.</p><p>How do we train the compiler to reduce spilling?</p><p>We can start with compiler directives. One of my favorites is <a href="https://github.com/oracle/graal/blob/66f2f167d822a53d6180a7e58ceb6f755f24ec1d/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/api/directives/GraalDirectives.java#L548-L557">GraalDirectives.opaque</a>, which returns the same runtime value but hides it behind a distinct compiler node. Wrapping field and stack.top separately in each switch arm prevents the compiler from simply treating the resulting expressions as shared work. With each copy used only in its own arm, the scheduler can keep the dependent reads and arithmetic there.</p><p>Our int case now looks like this:</p><pre>case Int -&gt; {<br>    field = GraalDirectives.opaque(field);<br>    long top = GraalDirectives.opaque(stack.top);<br>    long receiverSlot = top - 2;<br>    long valueSlot = top - 1;<br>    // ... receiver check, field store, and stack update using field and these slots ...<br>}</pre><p>It works. It’s just ugly.</p><p>Every arm now describes both the bytecode’s behavior and how we want it compiled. Worse, the desired placement depends on the compiler’s treatment of these directives, so a compiler change may send us back to rearrange the source.</p><p>We can instead teach the compiler the tradeoff directly: accept a little more generated code in exchange for shorter live ranges.</p><p>We restrict read hoisting and sharing, and teach the scheduler to favor separate copies of arithmetic in the consuming branches. The field metadata loads and slot calculations now stay inside the arms, without the source directives:</p><pre>    ; No spills before the switch; dispatch omitted.<br><br>int_case:<br>▶   mov     QWORD PTR [rsp + 0x18], regKindObject   ; spill decompressed JavaKind reference<br>▶   mov     QWORD PTR [rsp + 0x20], regFieldObject  ; spill decompressed field reference<br>    mov     regReceiver32, DWORD PTR [regRefs + regTop * 4]<br>    lea     regReceiverSlot, [regTop - 2]<br>    test    regReceiver32, regReceiver32<br>    je      null_receiver_int<br>    ; ... value load and modifier handling omitted ...<br>    mov     regOffset32, DWORD PTR [regHeapBase + regField * 8 + OFFSET_fieldOffset]<br>    ; ... volatility check, receiver decompression, offset widening, and field store ...</pre><p>The long arm computes top-3 instead. Neither arm carries the other’s stack update through the switch.</p><p>These choices deliberately work against optimizations we built for general-purpose compilation. Sharing work is usually sensible, but our tiny handlers have tightly constrained register interfaces. We therefore special-case handler stubs, putting this policy in the compiler instead of repeating it through directives in every handler.</p><h4>Das Leben der Anderen</h4><p>Even then, spills can remain.</p><p>The two stores inside the int arm preserve values for the null-check exception path and a later safepoint slow path, omitted from the assembly excerpt.</p><p>Neither slow path uses these values for its actual work. Why keep them alive?</p><p>At a call site — including the runtime call on a safepoint poll’s slow path — the compiler may need to describe the caller’s Java execution state for deoptimization, debugging, or runtime stack inspection. Graal records this execution state in a <em>frame state</em>. That can include local variables the call itself never uses, potentially keeping them alive beyond their last executable use. It can even force a reference to be decompressed solely for bookkeeping, although subsequent field accesses need only its compressed form.</p><p>In our putfield example, both slow paths retain the decompressed field and JavaKind references alongside other parameters and locals.</p><p>Keeping a value alive does not require spilling it immediately. There are enough registers for the live values just before this null check. But these references remain live alongside the temporaries needed for the field store, increasing register pressure inside the arm.</p><p>On handler fast paths, Graal’s register allocator favors spilling values with no remaining register-requiring uses when it needs to free a register. Neither decompressed reference has another executable use, making both attractive spill candidates. Here, the resulting interval assignments place the stores before the null check, so the fast path pays too.</p><p>One source-level response is to stop giving intermediate results their own locals. For putfield, we could remove the kind local and write the switch directly:</p><pre>switch (field.kind()) {<br>    // ... same arms ...<br>}</pre><p>The same approach becomes less pleasant in invokestatic:</p><pre>static long invokeStaticHandler(long bci, Frame frame, Stack stack) {<br>    CallInfo callInfo = resolveCall(bci, frame);<br>    Object retObj = VM.dispatchCall(<br>            popArguments(<br>                    callInfo.argumentCount == 0<br>                            ? EMPTY_ARRAY<br>                            : new Object[callInfo.argumentCount],<br>                    frame,<br>                    stack,<br>                    callInfo),<br>            bci,<br>            frame,<br>            callInfo);<br>    // ... return handling and next BCI omitted ...<br>}</pre><p>To avoid an intermediate local, we have crammed argument allocation, preparation, and invocation into one expression — an anti-pattern adopted to satisfy the compiler’s bookkeeping.</p><p>But our handler stubs cannot deoptimize. Unless debugging or runtime inspection requires their local values, we can discard that bookkeeping.</p><p>We therefore teach the compiler which frame-state values these stubs actually need, and prune the rest before register allocation. We retain the BCI used to report the guest’s execution position during stack walking, along with lock information. Live references needed by the GC remain tracked.</p><p>The two spill stores disappear from the int arm:</p><pre><br>int_case:<br>    mov     regReceiver32, DWORD PTR [regRefs + regTop * 4]<br>    test    regReceiver32, regReceiver32<br>    je      null_receiver_int<br>    ; ... value load, field store, stack update, and continuation ...</pre><p>Now a local variable can go back to being a name that helps us read the code.</p><h4>One Spill Flew Over the Sibling’s Nest</h4><p>Return to the version before frame-state pruning. The remaining mystery is why the int arm spills both references as soon as it begins.</p><p>At the null check, 11 live intervals overlap, against 12 allocatable general-purpose registers in our x64 build. Pressure rises later in the field-store sequence, where 13 intervals overlap.</p><p>Why pay for two spills on entry?</p><p>The answer starts in another arm.</p><p>Graal’s linear-scan allocator lays out basic blocks and instructions along a one-dimensional axis, then represents each value’s lifetime as ranges on that axis. Those intervals can contain gaps and be split into children with different register or stack assignments. Allocation follows this linear order, while liveness analysis and reconciliation between blocks still use the control-flow graph.</p><p>In the putfield compilation, the allocator visits the Object arm before the int arm. There, 14 live intervals compete for 12 registers. The decompressed kind and field references become spill victims, freeing registers for the remaining work. The stores appear in the blocks where that pressure rises.</p><p>But their stack assignments extend into the later int arm too. The switch still supplies both references in registers. The Object arm’s stores never execute on the int path, so data-flow resolution must insert new stores to satisfy the int arm’s inherited stack assignments. It places them at the arm’s entrance, before the null check.</p><p>The int arm has register pressure of its own, but these two stack assignments originate in its sibling. It inherits both spills — and pays for them on entry, even though it may not need to free both registers.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hdksKr2x8IPqr7WWbgVeYw.png" /><figcaption><em>The figure abbreviates </em><em>if (safepoint_poll) { safepoint(); } as </em><em>safepoint(), omitting the conditional branch to keep the control flow focused on the switch arms.</em></figcaption></figure><p>We therefore teach the compiler to give a sibling another chance at a register.</p><p>When a spilled interval extends into another block, the allocator checks the locations supplied by its actual predecessors. If the stack assignment does not arrive on every incoming path, it splits the interval at that block’s entrance and allocates the new child separately.</p><p>The new child prefers the register already supplied by an incoming path. This lets the arms make different choices for the same value.</p><p>That independence costs compilation time. Each split requires predecessor checks and creates another interval to allocate, which may itself be split again. Across a large method, this extra work can erode the speed that makes linear scan attractive.</p><p>Yet again, we give handler stubs special treatment. After so many amendments to the compiler’s usual rules, an uncomfortable question presents itself.</p><p><strong><em>Why not write assembly?</em></strong></p><h3>Act IV: Everything Everywhere All at Once</h3><p>Well, that is not for me to decide.</p><p>When my boss <a href="https://github.com/dougxc">Doug Simon</a> and the team started the Maxine VM project, the idea was Java on Java.</p><p>I never convinced them to add __asm__ to Java, so I need an excuse to convince myself.</p><p>Luckily, there is one.</p><p>Our iconst_0 and putfield examples already use a Stack object, but the handlers still access the backing arrays directly.</p><p>Let us encapsulate those accesses in the stack abstraction. Instead of:</p><pre>class Stack {<br>    long top;<br>}<br><br>static long iconst0Handler(long bci, Frame frame, Stack stack) {<br>    long slot = stack.top;<br>    UNSAFE.putLong(frame.prims, slot * 8L + OFFSET_arrayStart, 0);<br>    stack.top = slot + 1;<br>    return bci + 1;<br>}</pre><p>We can write:</p><pre>class Stack {<br>    long top;<br><br>    void pushInt(Frame frame, int value) {<br>        UNSAFE.putLong(frame.prims, top * 8L + OFFSET_arrayStart, value);<br>        top++;<br>    }<br><br>    int popInt(Frame frame) {<br>        top--;<br>        return (int) UNSAFE.getLong(frame.prims, top * 8L + OFFSET_arrayStart);<br>    }<br>}<br><br>static long iconst0Handler(long bci, Frame frame, Stack stack) {<br>    stack.pushInt(frame, 0);<br>    return bci + 1;<br>}</pre><p>Stack now provides an overlay on the frame, while virtual argument expansion keeps that overlay in the source without carrying an object into the emitted assembly.</p><p>The handler no longer needs to know where zero goes.</p><p>We can now subclass Stack and override pushInt, caching values in instance fields instead of writing them immediately to the backing array.<br>Again, argument expansion carries these fields between handlers in registers.</p><p>That gives us top-of-stack caching.</p><h4>One Field, Many Roles</h4><p>Let us return to the simplest cache from the previous article, with two primitive fields, tos1 and tos2, and a template variable tosDepth counting the cached JVM slots. We can view these fields as an extension of Frame.prims: they hold stack slots just like the array elements, except they live in registers. top marks the end of the materialized stack; the cached slots extend it. Object references remain in the backing frame, so before pushing one, we write back the cached values and reset tosDepth to zero.</p><p>The following table shows a selection of handlers, how they interpret the cached slots, and what they leave for the next handler.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mKNJt6d6l58N8gkfj1aUbA.png" /><figcaption>Cache-field roles across selected handlers. Uncached operands come from the frame; a long occupies a payload slot and a padding slot, while instanceof requires an empty primitive cache.</figcaption></figure><p>The types represented by tos1 and tos2 change across handlers, but we need no runtime type tests. The bytecode verifier establishes the operand types, and the cache protocol preserves their representation. The remaining question is how to use those bits without paying for a conversion.</p><p>We declare tos1 and tos2 as long, wide enough to hold the bits of any primitive value. Reading an int through an ordinary Java cast introduces a narrowing operation into the compiler graph. Depending on how it is lowered and allocated, that operation can introduce a register move and a separate register for the narrowed value.</p><p>For an integer operand, however, our protocol already places the value in the register’s lower 32 bits. We use a compiler directive to read those bits as an int, without introducing a separate narrowing operation. The upper bits need not be zero; a 32-bit use simply does not read them. Writing an integer back into a cache field needs the matching treatment: preserve its low 32 bits without requesting Java’s signed widening to long. unpackInt ignores the upper bits; packInt leaves them unspecified. On x64, the 32-bit arithmetic already supplies the bits we need.</p><p>That leaves two more mysterious entries in the table: <strong>Preserved</strong> and <strong>Unconstrained</strong>. A <strong>preserved</strong> value must reach the next handler unchanged, but passing it through requires no knowledge of its type.<br>For nop(tosDepth=2), both fields have Schrödinger’s type: their bits have a meaning, but we have no reason to look. Matching incoming and outgoing ABI registers lets the allocator carry these values through without moves, provided it keeps those registers intact.</p><p>Passing the bits through is enough until we need to write them back to the frame. Then we must know how much stack space they occupy. Our current state machine avoids that ambiguity by counting slots rather than values: a fully cached long uses both cache fields, including its padding.<br>Caching two longs would require a state that tells us more. We will come back to this later.</p><p>An <strong>unconstrained</strong> field need not preserve even its bits. For nop(tosDepth=1), we can pass whatever bits happen to occupy the register for tos2. We express this freedom through compiler directives, allowing the allocator to reuse that register inside the handler without preserving or restoring its contents. We need not even put zero there before the next handler.</p><p>We have already complained about directives cluttering the source.<br>Fortunately, we can sweep this machinery under the Stack overlay’s carpet. The handler compilation knows the concrete stack implementation, so it can inline the overridden operations.</p><pre>// Directive names in this illustrative example describe their intended contracts.<br>class CachedStack extends Stack {<br>    static final int NO_SLOTS_CACHED = 0;<br>    static final int ONE_SLOT_CACHED = 1;<br>    static final int TWO_SLOTS_CACHED = 2;<br><br>    long tos1;<br>    long tos2;<br>    int tosDepth = NO_SLOTS_CACHED;<br><br>    @Override<br>    void pushInt(Frame frame, int value) {<br>        switch (tosDepth) {<br>            case ONE_SLOT_CACHED -&gt; {<br>                tos2 = GraalDirectives.packInt(value);<br>                tosDepth = TWO_SLOTS_CACHED;<br>            }<br>            case TWO_SLOTS_CACHED -&gt; {<br>                UNSAFE.putLong(frame.prims, top * 8L + OFFSET_arrayStart, tos1);<br>                top++;<br>                tos1 = tos2;<br>                tos2 = GraalDirectives.packInt(value);<br>                // The cache stays full: 2 → 2.<br>            }<br>            default -&gt; {<br>                assert tosDepth == NO_SLOTS_CACHED;<br>                tos1 = GraalDirectives.packInt(value);<br>                tosDepth = ONE_SLOT_CACHED;<br>            }<br>        }<br>    }<br><br>    @Override<br>    int popInt(Frame frame) {<br>        return switch (tosDepth) {<br>            case ONE_SLOT_CACHED -&gt; {<br>                tosDepth = NO_SLOTS_CACHED;<br>                long result = tos1;<br>                tos1 = GraalDirectives.unconstrainedValue();<br>                yield GraalDirectives.unpackInt(result);<br>            }<br>            case TWO_SLOTS_CACHED -&gt; {<br>                tosDepth = ONE_SLOT_CACHED;<br>                long result = tos2;<br>                tos2 = GraalDirectives.unconstrainedValue();<br>                yield GraalDirectives.unpackInt(result);<br>            }<br>            default -&gt; {<br>                assert tosDepth == NO_SLOTS_CACHED;<br>                top--;<br>                yield (int) UNSAFE.getLong(frame.prims, top * 8L + OFFSET_arrayStart);<br>            }<br>        };<br>    }<br>}</pre><p>The handler sees only stack operations:</p><pre>static long iadd(long bci, Frame frame, Stack stack) {<br>    int right = stack.popInt(frame);<br>    int left = stack.popInt(frame);<br>    stack.pushInt(frame, left + right);<br>    return bci + 1;<br>}</pre><h4>One Handler, Many Variants</h4><p>Let us manually inline the two pops and the push in iadd, keeping only the operations that affect tosDepth:</p><pre>static long iadd(long bci, Frame frame, CachedStack stack) {<br>    // First popInt.<br>    switch (stack.tosDepth) {<br>        case ONE_SLOT_CACHED -&gt; stack.tosDepth = NO_SLOTS_CACHED;<br>        case TWO_SLOTS_CACHED -&gt; stack.tosDepth = ONE_SLOT_CACHED;<br>        default -&gt; { }<br>    }<br><br>    // Second popInt.<br>    switch (stack.tosDepth) {<br>        case ONE_SLOT_CACHED -&gt; stack.tosDepth = NO_SLOTS_CACHED;<br>        case TWO_SLOTS_CACHED -&gt; stack.tosDepth = ONE_SLOT_CACHED;<br>        default -&gt; { }<br>    }<br><br>    // pushInt.<br>    switch (stack.tosDepth) {<br>        case ONE_SLOT_CACHED -&gt; stack.tosDepth = TWO_SLOTS_CACHED;<br>        case TWO_SLOTS_CACHED -&gt; { }<br>        default -&gt; stack.tosDepth = ONE_SLOT_CACHED;<br>    }<br>    return bci + 1;<br>}</pre><p>The previous article introduced handler specialization: give the incoming template variable a constant value and let constant folding do its job. For iadd(tosDepth=2), each state update determines which arm of the next switch survives. The markers below trace that sequence:</p><pre>static long iadd_2(long bci, Frame frame, CachedStack stack) {<br>    // Represents the incoming template constant, not a runtime assignment.<br>    stack.tosDepth = TWO_SLOTS_CACHED;<br><br>    // First popInt.<br>    switch (stack.tosDepth) {<br>        case ONE_SLOT_CACHED -&gt; stack.tosDepth = NO_SLOTS_CACHED;<br>▶       case TWO_SLOTS_CACHED -&gt; stack.tosDepth = ONE_SLOT_CACHED;<br>        default -&gt; { }<br>    }<br><br>    // Second popInt.<br>    switch (stack.tosDepth) {<br>▶       case ONE_SLOT_CACHED -&gt; stack.tosDepth = NO_SLOTS_CACHED;<br>        case TWO_SLOTS_CACHED -&gt; stack.tosDepth = ONE_SLOT_CACHED;<br>        default -&gt; { }<br>    }<br><br>    // pushInt.<br>    switch (stack.tosDepth) {<br>        case ONE_SLOT_CACHED -&gt; stack.tosDepth = TWO_SLOTS_CACHED;<br>        case TWO_SLOTS_CACHED -&gt; { }<br>▶       default -&gt; stack.tosDepth = ONE_SLOT_CACHED;<br>    }<br>    return bci + 1;<br>}</pre><p>Restoring the operand operations, constant folding and dead-code elimination leave:</p><pre>static long iadd_2(long bci, Frame frame, CachedStack stack) {<br>    int right = GraalDirectives.unpackInt(stack.tos2);<br>    int left = GraalDirectives.unpackInt(stack.tos1);<br>    stack.tos1 = GraalDirectives.packInt(left + right);<br>    stack.tos2 = GraalDirectives.unconstrainedValue();<br>    stack.tosDepth = ONE_SLOT_CACHED;<br>    return bci + 1;<br>}</pre><p>The corresponding assembly is compact:</p><pre>    ; regBCI:   incoming bci         (long)<br>    ; regFrame: incoming frame       (non-null)<br>    ; regPrims: incoming frame.prims (non-null)<br>    ; regRefs:  incoming frame.refs  (non-null)<br>    ; regCode:  incoming frame.code  (non-null)<br>    ; regTop:   incoming stack.top   (long)<br>    ; regTos1:  incoming stack.tos1  (long)<br>    ; regTos2:  incoming stack.tos2  (long)<br>    add     regTos1_32, regTos2_32              ; Java int addition<br>    inc     regBCI<br>    jmp     tail_call_target(regCode, regBCI, ONE_SLOT_CACHED)</pre><p>The same process derives iadd(tosDepth=0) and iadd(tosDepth=1), which fetch two operands or one operand from the backing frame, respectively.</p><p>The handlers describe bytecode behavior; the stack implementation supplies caching behavior wherever those operations occur.<br>Inlining brings them together, and constant folding derives each specialized variant. This is an aspect-oriented way to implement the optimization.</p><p>We still design the transitions in the stack abstraction, but need not calculate the outgoing tosDepth separately for every handler. The compiler fills in that column for us. That state selects the next already-specialized handler at the threaded tail call.</p><p>That only works while the compiler can determine the outgoing state on each path. Different branches may lead to different constant states, but invocation bytecodes are less cooperative: they may pop arguments in a loop whose iteration count depends on the target method. Our implementations flush the cache before that loop and finish with a known empty-cache state. Code that operates directly on the backing frame likewise needs the cached values written back first.</p><p>Flushing is less alarming than it sounds. These are the stack stores the uncached interpreter would have performed earlier; caching merely postpones them, and may eliminate some before a flush is needed.</p><p>Writing cached values back does not require allocating the overlay object. It can remain virtual, provided we do not let it escape through a call.</p><h4>One Interpreter, Many Stack Implementations</h4><p>Now we can change the stack implementation without rewriting the handlers. A different subclass can encode a different state machine.</p><p>Our first model spends two cache fields on one long, including its padding, because the state counts slots rather than values.<br>But the state could tell us more.</p><p>Let &lt;CAT1&gt; mean an int or float, occupying one JVM slot, and &lt;CAT2&gt; mean a long or double, occupying two. We then have six populated states: &lt;CAT1&gt;, &lt;CAT2&gt;, &lt;CAT1,CAT1&gt;, &lt;CAT1,CAT2&gt;, &lt;CAT2,CAT1&gt;, and &lt;CAT2,CAT2&gt;, with pairs ordered from bottom to top. Seven, including the empty cache.</p><p>Each cache field now holds a complete payload. The state tells materialization how much space to reserve for it in the backing frame.<br>For &lt;CAT1,CAT1&gt;, we write two integers or floats and advance top by two slots. For &lt;CAT2,CAT2&gt;, we write two longs or doubles and advance top by four. The fields still extend the backing array, but the number of slots they represent depends on the state.</p><p>Two registers. Now room for two longs.</p><p>We can go further and distinguish the actual primitive types: &lt;INT&gt;, &lt;FLOAT&gt;, &lt;LONG&gt;, &lt;DOUBLE&gt;, and ordered pairs such as &lt;INT,LONG&gt; or &lt;DOUBLE,FLOAT&gt;. That lets the stack implementation choose a register representation as well as a slot width. We keep integer payloads in long fields and add double fields for floating-point payloads, which can travel in XMM registers on x64. A float retains its raw bits rather than being numerically widened to a double. Consecutive floating-point handlers can then operate on those payloads without shuttling them between general-purpose and floating-point registers.</p><p>The state determines which fields contain valid payloads and must be written back during materialization; unused representations must not be treated as stack values. The expansion configuration exposes that validity to the compiler, so those unused fields need not occupy registers either.</p><p>There is a bill for all this information. Four single-value states and sixteen ordered pairs, plus the empty state, give us twenty-one states. Each incoming state creates another handler variant to compile, and each viable variant contributes generated code.</p><p>Some handler/state combinations are impossible, and we can say so. The typed popInt rejects a cached operand whose kind is not INT using an unreachable-path directive. For iadd entering in &lt;DOUBLE,INT&gt;, the first pop succeeds, but the second encounters the cached double. Once specialization exposes that contradiction, the compiler propagates the unreachable marker back to the entry, discarding the preceding work and reducing the variant to a tiny trap. This reduces both the remaining compilation effort and the emitted code size.</p><p>To reduce the number of variants further, we can also choose not to cache some perfectly legal combinations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4yEK5EIR6mnI94oOzrJFhA.png" /></figure><p>For example, we could keep only the empty state, the four single-type states, and the four same-type pairs: nine states. A push that would create a mixed-type pair would instead write the existing cache and the incoming value to the frame, then return to the empty-cache state. &lt;INT,DOUBLE&gt; is legal for DASTORE, but we can still omit it and flush instead.</p><p>Whether we add states or drop them, the changes stay in the stack subclass and its expansion configuration.</p><p>The handlers still push, pop, and compute.</p><h3>The Fast and the Curious</h3><p>With those pieces in place, let us step back and see how far we have come.</p><p>We <strong>threaded the handlers</strong>, then used <strong>the verifier’s knowledge</strong> to remove checks and widen their indices. We <strong>taught the compiler</strong> to make different tradeoffs for these tiny handlers. Finally, we <strong>cached the top of the stack</strong>, keeping more interpreter state in registers.</p><p>To see how these changes translate into performance, we selected Sieve and Mandelbrot from the <a href="https://github.com/smarr/are-we-fast-yet">Are We Fast Yet </a>suite. Both are arithmetic-heavy workloads that exercise the handler and operand-stack operations discussed above. All measurements are interpreter-only, with JIT compilation disabled.</p><p>Our starting point is Crema before threaded tail calls. For TOS, we compare the basic slot cache, the category cache, and the typed cache.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fHj4KRJrCgkh3GzegyBjmg.png" /></figure><p>Threading gives both workloads a substantial boost, but neither catches HotSpot until we teach the compiler the handler-specific tradeoffs.</p><p>With the basic cache, Sieve runs about <strong>48%</strong> faster than HotSpot; Mandelbrot, about <strong>34%</strong>. Sieve heavily exercises baload and bastore.<br>Two differences likely help here. We quicken these bytecodes for the array type; in particular, bastore no longer needs HotSpot’s byte-versus-boolean check. Our two-entry primitive cache can also keep both the index and value in registers, whereas HotSpot caches only the top operand.</p><p>The category cache brings Mandelbrot to <strong>41%</strong> faster. Mandelbrot is heavy on double arithmetic: the basic slot cache holds only one double, while the category cache can keep both operands in registers.</p><p>The typed cache is the interesting twist. Although llvm-mca estimates <strong>7.5%</strong> <strong>fewer cycles</strong> than the category-cache version for the interpreter instruction sequence corresponding to Mandelbrot’s hot loop, the typed cache runs slower in the benchmark. One possible culprit is code size: more handler variants mean a larger instruction footprint, potentially increasing pressure on the instruction cache.</p><p>Two arithmetic-heavy workloads do not settle the performance of an entire interpreter. They do show that readable Java and competitive interpreter code can coexist — even when getting there takes quite a bit of teaching.</p><h3>The End Credits</h3><p>We started by asking how close ordinary Java could get to the assembly we wanted. The answer depended on how we divided the work between the interpreter’s source code and the compiler. The source describes the execution model and supplies knowledge the compiler cannot discover on its own. The compiler turns that description into machine code, sometimes with policies tailored to the interpreter.</p><p>The harder question is where the design should live. When the design lives only in comments or in the programmer’s head, the implementation can drift away from it. Expressed through the right abstraction, it becomes something we can maintain and extend — and something the compiler can turn into efficient code.</p><p>Escape analysis deserves a credit of its own. Together with scalar replacement, it lets an object remain a useful source abstraction without requiring an object in the generated code. Its fields become independent compiler values that can live in registers. Argument expansion carries that idea across handler boundaries. The design stays in the source. Its objects need not survive into the assembly.</p><p>We still care about every load, move, and spill. We would just rather not write each one ourselves.</p><p>There is still call overhead to address.<br>That is where we go next.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d0edb5387a5e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/graalvm/chasing-the-perfect-assembly-d0edb5387a5e">Chasing the Perfect Assembly</a> was originally published in <a href="https://medium.com/graalvm">graalvm</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[GraalVM 25.3 is here! ]]></title>
            <link>https://medium.com/graalvm/graalvm-25-3-is-here-41641acebfaf?source=rss----7122626bf34b---4</link>
            <guid isPermaLink="false">https://medium.com/p/41641acebfaf</guid>
            <category><![CDATA[graalvm]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Alina Yurenko]]></dc:creator>
            <pubDate>Tue, 25 Aug 2026 16:14:07 GMT</pubDate>
            <atom:updated>2026-08-25T16:14:06.407Z</atom:updated>
            <content:encoded><![CDATA[<p>Today, we’re pleased to announce the release of <strong>GraalVM 25.3!</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1DUWU40GQ3pJF3kIZU10Sw.jpeg" /></figure><p>You can already <a href="https://www.graalvm.org/downloads/">download this release</a> and check <a href="https://www.graalvm.org/release-notes/25.3/">the release notes</a> for more details.</p><p>GraalVM 25.3 brings <strong>several performance and security improvements, as well as updated language runtimes</strong> — keep reading to see what’s new!</p><h3>Run even faster 🚀</h3><p>GraalVM now includes a <strong>new priority inliner</strong>, available in both JIT and Native Image modes. It performs broader call-graph analysis to focus inlining on the most promising call sites, potentially improving the performance of generated-code and enabling further compiler optimizations. This means that many applications will see performance improvements by simply upgrading to the latest version.</p><p>Native Image now provides <strong>faster execution paths and optimized spinning for </strong><strong>synchronized operations</strong>, improving locking performance. Its runtime bytecode interpreter has also been redesigned to enable tail-call threading between outlined bytecode handlers, significantly improving interpreter performance.</p><p>The Graal compiler additionally <strong>optimizes unsigned integer division and remainder operations</strong> by constant values using magic-number transformations.</p><h3>Additional security hardening 🛡️</h3><p>This release introduces <strong>new control-flow integrity options through </strong><strong>-H:CFI</strong>. Control-flow integrity (CFI) is a security mechanism that helps prevent attackers from redirecting program execution to unexpected code locations. On AMD64, software-based checks can ensure that indirect branches land on valid targets. On AArch64, pointer authentication codes can protect return addresses stored on the stack. In addition, executable anonymous memory-mapping locations are now randomized by default.</p><h3>Updated language runtimes</h3><p>GraalJS now includes Node.js 24.18.1. It also implements the Iterator Includes, Iterator Join, and Iterator Chunking proposals, available in the ECMAScript staging mode (--js.ecmascript-version=staging).</p><p>GraalPy incorporates the Python 3.13.14 standard library and implements additional Python 3.13 features.</p><h3>Expanded WebAssembly support</h3><p>GraalWasm now recognizes WebAssembly branch hints, allowing if and br_if likelihood information to guide optimized code generation. This release also adds the table64 portion of the Memory64 proposal and experimental support for Wide Arithmetic operations.</p><h3>Community</h3><ul><li><a href="https://kotlinlang.org/docs/whatsnew-eap.html#kotlin-compiler-native-image">Say hello to the Kotlin compiler on Native Image</a>! The native image provides a drop-in replacement for the standard kotlinc command-line tool, while offering faster startup time and higher performance.</li><li>Explore how <a href="https://farnoy.dev/posts/manual-reification-on-the-jvm">manual reification </a>helps the Graal compiler turn flexible data pipelines into highly optimized code.</li><li>Join us at <a href="https://devoxx.be/">Devoxx Belgium</a> for several GraalVM sessions across Native Image, Python, and AI!</li></ul><h3>Conclusion</h3><p>We’d like to take this opportunity to thank our amazing contributors and community for all the feedback, suggestions, and contributions that went into this release.</p><p>If you have feedback for this release or suggestions for features that you would like to see in future releases, please share them with us on <a href="https://graalvm.org/slack-invitation">Slack</a>, <a href="https://github.com/oracle/graal">GitHub</a>, or <a href="https://www.linkedin.com/company/graalvm/">LinkedIn</a>.</p><p>Now go ahead and try the <a href="https://www.graalvm.org/downloads/">new GraalVM</a>! 🚀</p><p>— the GraalVM team</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=41641acebfaf" width="1" height="1" alt=""><hr><p><a href="https://medium.com/graalvm/graalvm-25-3-is-here-41641acebfaf">GraalVM 25.3 is here! 🎉</a> was originally published in <a href="https://medium.com/graalvm">graalvm</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Escaping the Giant Switch]]></title>
            <link>https://medium.com/graalvm/escaping-the-giant-switch-dec20b572139?source=rss----7122626bf34b---4</link>
            <guid isPermaLink="false">https://medium.com/p/dec20b572139</guid>
            <category><![CDATA[compilers]]></category>
            <category><![CDATA[graalvm]]></category>
            <category><![CDATA[interpreters]]></category>
            <category><![CDATA[truffle]]></category>
            <dc:creator><![CDATA[Yudi Zheng]]></dc:creator>
            <pubDate>Thu, 13 Aug 2026 14:02:23 GMT</pubDate>
            <atom:updated>2026-08-13T14:02:21.747Z</atom:updated>
            <content:encoded><![CDATA[<p><em>a true story about bytecode interpreters</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vBQOE-g2Kp5Djkt9fsJkRw.jpeg" /></figure><p>Last year, <a href="https://thomaswue.dev/">Thomas</a>, bearer of the One Graal, came to us with a prompt:</p><blockquote><em>「 /goal make the GraalPy interpreter faster than CPython 」</em></blockquote><p>To understand what Thomas was asking, it helps to know what the two interpreters are made of. The <a href="https://graalpy.org/">GraalPy</a> interpreter is written in Java. CPython’s is written in C. So the real assignment was to make a Java interpreter outrun a C one, without giving up the high-level Java programming model.</p><p>The good news was that we controlled the compiler.</p><p>The bad news was that we controlled the compiler.</p><p>There are very few hard limits when you own the entire stack. There are also very few excuses.</p><p>If the branch predictor was unhappy, that was our problem.</p><p>If the register allocator was unhappy, that was our problem.</p><p>So we started looking for something dangerous to optimize.</p><h3>The Giant Switch</h3><p>The most obvious suspect was the interpreter dispatch loop. To keep the discussion concrete, let us start with a tiny program:</p><pre>static int plus1(int x) {<br>    return x + 1;<br>}</pre><p>On a stack-based virtual machine, this could be compiled into a bytecode sequence like:</p><pre>iload_0<br>iconst_1<br>iadd<br>ireturn</pre><p>Even if you have never looked at Java bytecode before, the instructions are fairly self-explanatory: load the first local variable (which happens to be the method parameter in this example), push the constant 1, add the two values, and return the result.</p><p>One natural way to implement such a bytecode interpreter is a large dispatch loop:</p><pre>while (true) {<br>    int opcode = bytecodes[pc];<br>    switch (opcode) {<br>        case ILOAD_0:<br>            stack[sp++] = locals[0];<br>            pc++;<br>            break;<br>        case ICONST_1:<br>            stack[sp++] = 1;<br>            pc++;<br>            break;<br>       case IADD:<br>            int b = stack[--sp];<br>            int a = stack[--sp];<br>            stack[sp++] = a + b;<br>            pc++;<br>            break;<br>       case IRETURN:<br>            return stack[--sp];<br>       // imagine a few hundred more friends here<br>    }<br>}</pre><p>Before continuing, it is worth acknowledging that this is not the only way to implement a bytecode interpreter. HotSpot’s template interpreter, for example, is famously efficient. Rather than relying on a giant switch loop, it generates platform-specific native code for executing bytecodes.</p><p>That option is powerful, but it also moves the burden onto handwritten or explicitly generated low-level machinery. Our challenge was different: keep the interpreter expressed through Java objects and methods, while still giving the compiler enough information to recover some of the same efficiencies automatically.</p><p>The real dispatch loop was, of course, much larger than this toy example, with hundreds of bytecode handlers.</p><p>Still, the execution pattern remains the same: every bytecode handler eventually returns to the same dispatch loop, which fetches the next opcode and jumps to the corresponding handler.</p><p>This architecture has many virtues. It is well understood, easy to maintain, and surprisingly effective. It also has a tendency to accumulate state. The program counter lives there. The operand stack lives there. The local variables live there. Profiling information lives there. Before long, every piece of interpreter state is flowing through the same compilation unit.</p><p>That last detail would eventually matter a great deal.</p><p>For now, all we saw was a giant switch sitting in the middle of one of our hottest execution paths.</p><p>Naturally, we blamed it for the performance gap.</p><h3>Take I: Thread the Switch</h3><p>The first theory was branch prediction.</p><p>At the end of each non-terminating bytecode handler, control returns to the switch. The interpreter reads the next opcode and uses the same dispatch branch to select the next handler. So every transition in our plus1 example passes through the same machine-code branch.</p><p>Modern processors do not wait politely for every branch to resolve. They speculate. The processor predicts where a branch will go and starts executing along that path before the branch has actually completed.</p><p>When the prediction is correct, much of that work is effectively free. When the prediction is wrong, the speculative work is discarded and execution restarts from the correct target. Ideally, the processor would learn that after iload_0 comes iconst_1, after iconst_1 comes iadd, and after iadd comes ireturn.</p><p>Unfortunately, the predictor does not see bytecodes. It sees machine instructions.</p><p>From the processor’s perspective, all of these transitions originate from the same dispatch branch.</p><p>To build some intuition, imagine that each branch instruction is a signpost. Whenever the branch resolves, the signpost turns to point toward the destination it just took. The next time execution reaches that signpost, the processor speculates by following its current direction. Under this simple model, the shared dispatch signpost will mispredict every transition in our example.</p><p>Real branch predictors use considerably richer history, but each time the branch executes, the predictor must still choose one specific target and begin speculating along that path.</p><p>This is the classic motivation behind threaded dispatch. The trick is to duplicate the signposts.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*seNXmlfTnaRpERN66und7Q.png" /></figure><p>The bytecode sequence stays the same, but each handler gets its own copy of the dispatch branch at a different machine-code location.</p><p>The obvious question is how to generate such a layout. Fortunately, the replay itself is surprisingly straightforward.</p><p>We expose this through a feature of the <a href="https://github.com/oracle/graal/blob/master/truffle/docs/bytecode_dsl/BytecodeDSL.md">Truffle Bytecode DSL</a>. The dispatch sequence is marked for special treatment:</p><pre>while (true) {<br>    int opcode = bytecodes[pc];<br>    switch (markThreadedSwitch(opcode)) {<br>        // ...<br>    }<br>}</pre><p>Because we control the compiler, that marker can trigger a late code-generation step. During code generation, we record the machine code for the dispatch path, from the loop header through the table jump generated for the switch. At the very end of compilation, the compiler replaces each interpreter backedge with a copy of those exact machine-code bytes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H0vIhJv7B9azBxDmDBCsfw.png" /></figure><p>No re-optimization is required. No new register allocation is required.</p><p>As a result, all registers, memory operands, and addressing modes remain exactly as they were in the original dispatch sequence. The important difference is that the branch instruction is no longer shared by every bytecode transition.</p><p>Some patching is still required. Branches leaving the copied region, metadata references, and other bookkeeping details need to be adjusted. These details matter, but they do not change the basic idea: copy the generated dispatch path to each backedge.</p><p>One unpredictable dispatch branch becomes many more predictable ones.</p><p>That was the theory, at least.</p><h3>The Rabbit Hole</h3><p>As expected, threaded dispatch helped, but the magnitude never felt right.</p><p>Honestly, I never really bought branch prediction as the whole story.</p><p>So we opened the intermediate representation our compiler emits.</p><p>As compiler engineers, this is usually the point where we hope to find a small, local mistake instead of a new research project.</p><p>Fortunately, there was one.</p><p>The shared dispatch loop header was carrying around a surprising amount of register pressure.</p><p>To understand why this matters, imagine an interpreter that keeps enough state live to exceed the number of available registers. On x86–64, that is not as difficult as it sounds. Despite decades of progress elsewhere in computing, most of us are still developing on machines with 16 general-purpose registers. <a href="https://www.intel.com/content/www/us/en/developer/articles/technical/advanced-performance-extensions-apx.html">Intel APX</a> promises relief, but it has yet to appear on the machines that generate our management update charts.</p><p>Eventually, some values must be spilled to memory. In our case, the scheduling heuristics had a tendency to pull interpreter states into the shared dispatch loop header.</p><p>The unfortunate part is that many of those states were not needed by every bytecode handler. An interpreter state that was only relevant to a handful of bytecodes could still compete for registers in the shared dispatch path executed by all of them. The more registers a handler needed locally, the fewer good choices remained for values live around the shared loop header.</p><p>A straightforward mitigation was to tweak the heuristics so fewer interpreter states were scheduled into the loop header.</p><p>This did not make register pressure disappear. It changed where the cost was paid. An interpreter state needed by one handler should ideally compete for registers in that handler, not in the shared dispatch path executed by every bytecode. That is a much better trade-off.</p><p>Ordinarily, increasing code size would be a concern.</p><p>At this point we were already duplicating the dispatch loop onto every interpreter backedge. Worrying about a few more instructions felt somewhat academic.</p><p>The optimization worked, surprisingly well.</p><p>Unfortunately, this was also where we fell into the rabbit hole.</p><p>We had reduced the cost of the shared dispatch path, but the compiler was still fundamentally reasoning about the same enormous program.</p><p>A giant switch had become a giant compilation unit. A giant compilation unit had become a giant live-range problem.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IxaX77lmm5Q28sTdiMoUjA.png" /></figure><p>Values from unrelated bytecode handlers were still competing for the same limited register budget. For an interpreter, not all values are equal. Spilling a rarely used handler-local temporary is annoying. Spilling the program counter, the stack pointer, the bytecode array, or frequently accessed frame state can add memory traffic to almost every bytecode transition.</p><p>While a register allocator can often find excellent solutions, it cannot exhaustively explore the optimization space of a large compilation unit in a reasonable amount of time.</p><p>It must therefore make more heuristic decisions, and heuristic decisions inevitably leave some performance on the table.</p><p>Register allocation was only where the problem first became obvious. The same shape affected every optimization that had to reason about the interpreter as one enormous body.</p><p>We could keep tuning heuristics and teaching the compiler new tricks.</p><p>Or we could ask a more uncomfortable question.</p><p>Why was the entire interpreter being optimized as one giant compilation unit in the first place?</p><h3>Take II: Escape the Giant Switch</h3><p>Once we framed the problem as a giant compilation unit, the obvious solution seemed equally obvious.</p><p>Split it.</p><p>After all, compilers already know how to compile multiple methods independently. Why force the entire interpreter through one enormous optimization problem?</p><p>In fact, there is a remarkably straightforward way to do exactly that. Every bytecode handler can simply become its own method:</p><pre>@DontInline<br>static void iload0(...) { ... }<br><br>@DontInline<br>static void iconst1(...) { ... }<br><br>@DontInline<br>static void iadd(...) { ... }</pre><p>Now every handler is compiled independently. The giant compilation unit disappears. The giant live-range problem disappears. The register allocator can focus on one bytecode handler at a time.</p><p>The problem is gone. Unfortunately, so is most of the performance.</p><p>The issue is that compilation boundaries are not free.</p><p>For an interpreter, there is quite a lot of state to carry around. The bytecode stream. The program counter. The operand stack. The stack pointer. Profiling information. Exception state. Various pieces of bookkeeping accumulated over years of evolution.</p><p>Inside a single compilation unit, many of these values can remain virtual. The compiler is free to keep them in registers and move them around as it sees fit.</p><p>Across a method boundary, that illusion breaks down: values must cross a calling convention, and virtual state may need to be materialized.</p><p>In a language like Java, primitive interpreter state is particularly awkward. One option is to store everything in an object and pass that object from handler to handler. The register allocator will certainly appreciate the smaller compilation units.</p><p>The memory subsystem, less so.</p><p>We would have traded a register allocation problem for a memory traffic problem. What we really wanted was something contradictory.</p><p>We wanted each bytecode handler to be compiled independently, as if it were its own compilation unit.</p><p>At the same time, we wanted interpreter state to flow between handlers as if no compilation boundary existed at all.</p><p>In other words, we wanted optimization across compilation units.</p><p>Normally, those words do not belong in the same sentence.</p><h3>The Valhalla-Shaped Trick</h3><p>The obvious question was how to move interpreter state across compilation boundaries without sacrificing the optimization opportunities we had just fought so hard to recover.</p><p>The idea itself was not entirely new.</p><p>Project Valhalla had already spent years exploring a similar observation: programmers like objects, but compilers often prefer their contents.</p><p>Consider our running example:</p><pre>class State {<br>    int pc;<br>    int sp;<br>}<br><br>static void iadd(State state, int[] stack) {<br>    int b = stack[state.sp--];<br>    int a = stack[state.sp--];<br>    stack[++state.sp] = a + b;<br>    state.pc++;<br>}</pre><p>From the interpreter author’s perspective, this is exactly what it should be: a small mutable object carrying interpreter state from one bytecode handler to the next. From the compiler’s perspective, however, the interesting part is rarely the object itself. The interesting part is the values it contains.</p><p>Conceptually, instead of reasoning about State state, the compiler would much rather reason about &lt;pc, sp&gt;.</p><p>So we took the idea and developed it further for interpreter state. Rather than passing interpreter objects across compilation boundaries, we expand selected fields into a tuple of values. Together with the existing handler arguments, that gives the compiler the boundary tuple it actually wants to optimize. To preserve the original programming model, the compiler leaves the original handler signature unchanged and synthesizes a small stub:</p><pre>static void __stub_iadd(int pc, int sp, int[] stack) {<br>    State state = new State(pc, sp);  // To be virtualized<br>    iadd(state, stack);               // To be inlined<br>}</pre><p>After inlining, escape analysis can often remove the reconstructed state object from the optimized representation and replace its fields with compiler values. Even when it cannot do so globally, Graal’s control-flow-aware Partial Escape Analysis can apply the same optimization on the fast path where object materialization is unnecessary.</p><p>Expanding interpreter state into a tuple solves only half of the problem. The tuple still needs to come back, but Java methods are much happier accepting many inputs than returning many outputs. So the generated stub grows a tuple return value:</p><pre>// Pseudo-Java<br>static &lt;int, int, int[]&gt; __stub_iadd(int pc, int sp, int[] stack) {<br>    State state = new State(pc, sp);  // To be virtualized<br>    iadd(state, stack);               // To be inlined<br>    return &lt;state.pc, state.sp, stack&gt;<br>}</pre><p>At first glance, this may look like constructing and returning a tuple object. The important detail is where the updated values land. The generated calling convention returns the tuple through the same locations that carried it in. If pc enters the stub in one register, the updated pc&#39; comes back in that same register. The caller-side write-back can then be inserted mechanically:</p><pre>// Pseudo-Java<br>&lt;pc&#39;, sp&#39;, stack&gt; =<br>    __stub_iadd(state.pc, state.sp, stack);<br>state.pc = pc&#39;;<br>state.sp = sp&#39;;</pre><p>Interpreter state now follows a round trip:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vieGhJQQIRACsT4nnTVwAQ.png" /></figure><p>The state objects preserve the Java programming model on both sides of the boundary. Interpreter authors keep working with objects, fields, and ordinary method signatures, while the compiler uses the tuple to carry selected state between compilations as register values.</p><p>Escape analysis also runs independently in the caller, which may keep the original state object virtual.</p><p>The write-back itself does not necessarily become memory traffic. If both state objects remain virtual, the updated values may continue flowing as compiler values throughout the entire round trip. In practice, this gives us much of what we wanted from escape analysis across compilation boundaries, without requiring the two compilations to become one.</p><p>The register allocator offers another perspective. Previously, the dispatch loop and all bytecode handlers formed one enormous register-allocation problem. Now the caller and each handler are allocated independently, with fixed argument locations connecting them into one continuous execution.</p><h3>The Hidden Boundary Tax</h3><p>There are still some limits to what this interface can express. In principle, we could carry every interpreter state across the call boundary. In practice, we must reserve part of the register budget for each handler’s local work. Even a simple bytecode such as iadd needs scratch registers for its local calculation, so the boundary tuple cannot occupy every useful register.</p><p>In the old giant compilation unit, an unselected value could still be visible to the inlined handler body and remain register-resident through scalar replacement. After outlining, a value that is not part of the boundary tuple is no longer carried into the handler as a compiler value. If the handler needs it, it must recover the value from materialized interpreter state through an ordinary memory access.</p><p>Usually this is still acceptable. We can select the most important states, and leave statistically negligible state to be recovered from memory when a handler actually needs it.</p><p>The more unavoidable issue is the call itself.</p><p>Besides ordinary call mechanics such as return-address bookkeeping and frame setup, a managed runtime like Native Image has to emit safepoint metadata for the call, describing where live references are. Unlike non-call operations, calls force the compiler to treat the callee as a black box. Live references residing in registers may be clobbered during the call, even if our special calling convention restores them once the call returns. That means the references need a GC-visible home, which often means a stack slot.</p><p>The outlined call boundary therefore turns part of the original register-pressure problem into a backup-and-restore problem around the stub call.</p><p>Yet the call also gives us the clue for escaping that cost. When the handler returns, the updated tuple already occupies the locations where the next handler expects its inputs. At that point, the obvious question is no longer just how to make the call cheaper. Why are we returning to the caller at all?</p><h3>Thread the Calls</h3><p>The answer, of course, is that we do not.</p><p>Instead, each handler stub performs the dispatch itself. This optimization is often called tail call threading. After executing the bytecode implementation, it fetches the next opcode, looks up the corresponding handler stub, and jumps directly to it. The bytecode stream therefore becomes a first-class part of the handler interface, extending the tuple to &lt;pc, sp, stack, bytecodes&gt;.</p><p>The generated code now looks roughly like this:</p><pre>// Pseudo-Java<br>static &lt;int, int, int[], short[]&gt;<br>__stub_iadd(int pc, int sp, int[] stack, short[] bytecodes) {<br>    State state = new State(pc, sp);  // To be virtualized<br>    iadd(state, stack);               // To be inlined<br>    int nextOpcode = bytecodes[state.pc];<br>    jump handlerTable[nextOpcode]<br>        with &lt;state.pc, state.sp, stack, bytecodes&gt;<br>}</pre><p>The specialized calling convention makes this natural: the jump requires no reshuffling and transfers the current interpreter state directly.</p><p>Viewed from a distance, the handlers resemble LEGO bricks: the shared tuple interface forms the studs that let them snap directly together.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RsqMbrs9RZ5Sj6Xv2kcuYQ.png" /></figure><p>The dispatch loop still exists. It simply stops getting invited to every bytecode.</p><h3>The Benchmark Everybody Came For</h3><p>At this point, we should probably return to the benchmark that drove this whole adventure.</p><p>To recap, the first suspect was branch prediction in the dispatch loop, and we <strong>threaded the switch</strong> to attack it. The spill came later; we <strong>tuned the scheduling</strong> heuristic and pushed the spill cost into individual handlers. Eventually, the compilation unit itself became suspicious. We <strong>outlined the handlers</strong>, then <strong>threaded the tail-call</strong> between them.</p><p>The final, threaded-tail-call configuration is <a href="https://medium.com/graalvm/graalvm-25-1-is-here-13829606982e">GraalVM 25.1</a> as released. The other configurations use that same release with the relevant optimizations selectively disabled, allowing us to reconstruct each stage of the journey. All measurements are interpreter-only, with GraalPy’s JIT compilation disabled.</p><p>We selected a few arithmetic-heavy benchmarks that are representative of the interpreter optimizations discussed above. Other workloads are dominated by different costs, such as guest-language calls or attribute access, and therefore depend more heavily on the implementation of the Python runtime itself.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a4Z548VB9QgkbHvA_uv55w.png" /></figure><p>bytecode-benchmark fits the story most clearly. The baseline starts below CPython. Threading the switch brings it to roughly parity, and tuning the scheduling pushes it ahead.</p><p>Then we got greedy. To thread execution directly from one handler to the next, we first outlined the handlers into separate compilation units, and hit the boundary tax. The handlers were smaller, but every call was surrounded by bookkeeping memory accesses.</p><p>Tail-call threading is what makes that structure pay. Once the updated tuple can flow directly from one handler to the next through the specialized calling convention, bytecode-benchmark moves comfortably ahead of CPython.</p><p>arith-binop and sieve start from stronger baselines but follow the same broad progression.</p><p>Overall, the benchmark result is not simply that threading wins. It is more specific: threading makes the handler boundary cheap enough that outlining stops being structurally fatal.</p><p>That leaves a more interesting question. If the compilation boundary between caller and handler no longer has to be an optimization boundary, what about the boundary between one handler and the next?</p><h3>“One More Thing”</h3><p>The most frequently accessed values in a stack interpreter are, unsurprisingly, the values at the top of the stack. A typical arithmetic bytecode may load values from the operand stack, perform an operation, and immediately push the result back. Even after escaping the giant switch, these stack accesses remained firmly on the hot path.</p><p>This is not a new observation. Top-of-stack caching is a well-established interpreter optimization. The question was how to make it expressible in ordinary Java code while still giving the compiler a register-level representation to optimize. The answer, again, was the tuple.</p><p>By this point, interpreter state was already flowing between handlers as a tuple of register-resident values. Nothing limited that tuple to the existing interpreter state, such as the program counter or stack pointer: it could also carry recent operand-stack values, keeping them in registers across handler boundaries.</p><p>Conceptually, the handler tuple grows from &lt;pc, sp, stack, bytecodes&gt; to &lt;pc, sp, stack, bytecodes, tos1, tos2, tosDepth&gt;.</p><p>Here tosDepth says how many cached values are currently valid. When tosDepth is 1, tos1 is the current top of stack. When tosDepth is 2, tos2 is the current top of stack, while tos1 is the value beneath it.</p><p>At this point, some readers may be counting registers and becoming increasingly uncomfortable. Under a conventional calling convention, they would be right. Sooner or later additional arguments would spill onto the stack, undoing much of the benefit we were trying to achieve.</p><p>Fortunately, we control the compiler.</p><p>Nothing requires us to stop at the six integer argument registers provided by the platform ABI. Handler stubs never cross a language boundary, so we are free to employ a considerably more adventurous calling convention and use essentially the entire allocatable register file for interpreter state. The tuple may be growing, but it is still very much intended to stay in registers.</p><p>In our running example, iload_0 pushes the local value into tos1. Then iconst_1 pushes the constant into tos2. Finally, iadd consumes tos2 and tos1, then leaves the result in tos1.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yTVqIcWarEWAVppVZuKaGA.png" /></figure><p>The important part is not merely that tos1 and tos2 avoid memory accesses inside one handler. The value loaded by iload_0 remains in a register across iconst_1 and into iadd, without ever touching the operand stack.</p><p>No stack loads.</p><p>No stack stores.</p><p>At least not immediately.</p><p>Of course, reality is slightly less convenient. Two registers can only hold two stack values. Sooner or later a bytecode sequence will push a third value, then a fourth, and eventually the cache must spill into the backing operand stack. Likewise, some handlers may need to materialize cached values before interacting with code that expects the operand stack to reside in memory. The interpreter therefore carries tosDepth as part of its state and lets individual handlers decide when values should remain in registers and when they must be synchronized with the backing stack array.</p><p>Without further help, every push and pop now has to account for all possible cache states. A push is no longer simply stack[++sp] = value;. It starts to look more like:</p><pre>switch (tosDepth) {<br>    case 0:<br>        tos1 = value;<br>        tosDepth = 1;<br>        break;<br>    case 1:<br>        tos2 = value;<br>        tosDepth = 2;<br>        break;<br>    case 2:<br>        stack[++sp] = tos1;<br>        tos1 = tos2;<br>        tos2 = value;<br>        break;<br>}</pre><p>A pop has the same problem in reverse.</p><p>After spending several sections moving interpreter state into registers, we had accidentally introduced a new runtime state machine.</p><h3>The (Compile-) Time (State) Machine</h3><p>If tosDepth merely describes the shape of the cached stack, perhaps it should not travel in the tuple at all. Instead, we could turn the runtime state machine into a compile-time one, sending that decision backward in time from every bytecode execution to handler generation.</p><p>Consider iadd written in the most boring possible way:</p><pre>static void iadd(...) {<br>    int b = stack.pop();<br>    int a = stack.pop();<br>    stack.push(a + b);<br>    state.pc++;<br>}</pre><p>Without specialization, each pop and push must inspect tosDepth at runtime, as in the push example above.</p><p>Instead of generating a single handler that must reason about every possible cache state, we generate multiple handler variants. Each variant gets assigned a constant tosDepth, which is enough for ordinary compiler optimizations to fold away the irrelevant arms of push and pop. After those branches disappear, the updated tosDepth often becomes deterministic as well. Dispatch can then use that value to choose the next variant.</p><p>Returning to our LEGO example, imagine that each bytecode brick now bundles several handler variants, one for each possible incoming tosDepth. We stack these composite bricks vertically, and the tosDepth produced by one handler selects the path into the next.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*b3Cw5b3PojvAA4g2nZaDIQ.png" /></figure><p>The state machine still exists, but it no longer lives as control flow inside every handler. It lives in the transitions between handler variants.</p><p>Now consider the iadd&lt;2&gt; specialization. The compiler knows that both operands already reside in registers. The first pop becomes int b = tos2;. The second pop becomes int a = tos1;. And the push becomes tos1 = a + b;.</p><p>After constant folding and dead-code elimination, the handler is essentially reduced to tos1 = tos1 + tos2;.</p><p>Consequently, tosDepth no longer needs to occupy a runtime slot in the tuple.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RdMI77Am06jQv31IjmEwww.png" /></figure><p>The columns are not runtime branches; they are separately generated handler variants. The highlighted path follows iload_0&lt;0&gt; to iconst_1&lt;1&gt;, then iadd&lt;2&gt; and ireturn&lt;1&gt;. Each handler’s output cache shape determines the next variant, so neither a runtime tosDepth value nor a test of it appears along the path.</p><p>We traded runtime state transit for instruction bytes: multiple handler variants eliminate a runtime value and its control flow, freeing a register for actual interpreter state. The previous sections taught the interpreter how to carry state efficiently across compilation boundaries. This section taught it that some state never needed to travel in the first place.</p><p>The remaining concern is register pressure. For the mechanism to work well, the handler interface needs enough registers to carry two or more cached top-of-stack values in addition to the existing interpreter state. That is already a tight budget. GraalPy’s tail-call-threaded handlers pass a fairly heavy argument tuple today, and adding more top-of-stack values can exhaust registers and push other state back to memory.</p><p>The situation is even trickier because the Python stack can hold both primitive values and object references. Those may need special treatment, or even separate cached top-of-stack values, which would make the interface wider again. At that point, the optimization starts fighting the same register-pressure problem it was meant to avoid.</p><p>We are still exploring ways to mitigate this register cost, or at least warn interpreter authors when the handler interface becomes too wide.</p><h3>Take III: ?</h3><p>This article started with a giant switch.</p><p>The giant switch led to a spill.</p><p>The spill led to a giant compilation unit.</p><p>And the giant compilation unit turned out to be hiding far more than a few misplaced registers.</p><p>We first threaded the switch, giving each bytecode handler its own dispatch branch. We then tuned scheduling so handler-local state spilled only where necessary, rather than across the entire dispatch loop. Finally, we outlined the handlers into separate compilation units and connected them through a specialized tail-call convention. Interpreter state crosses those compilation boundaries as a tuple of values in fixed register locations, allowing the handlers to remain separate compilation units without turning their boundaries into optimization boundaries.</p><p>The threaded-handler implementation shipped in GraalVM 25.1.3 and is enabled by default in GraalPy. On the interpreter-only, arithmetic-heavy workloads shown above, it takes bytecode-benchmark from below CPython to comfortably above it, while arith-binop reaches roughly three times CPython.</p><p>This does not make every Python workload three times faster. Calls, attribute access, and other runtime operations are dominated by different parts of the GraalPy implementation. Even within the handler interface, registers remain a finite resource: carrying more interpreter state eventually risks pushing existing values back to memory.</p><p>That is also where the next experiment begins. If compilation boundaries no longer prevent values from remaining visible to the optimizer, the same interface can carry cached operand-stack values directly from one handler to the next. The mechanism works; finding the right register budget is still ongoing work.</p><p>For now, the giant switch is gone.</p><p>Until the next rabbit hole.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dec20b572139" width="1" height="1" alt=""><hr><p><a href="https://medium.com/graalvm/escaping-the-giant-switch-dec20b572139">Escaping the Giant Switch</a> was originally published in <a href="https://medium.com/graalvm">graalvm</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[When Prompts Become Plugins]]></title>
            <link>https://medium.com/graalvm/when-prompts-become-plugins-34a737d30f54?source=rss----7122626bf34b---4</link>
            <guid isPermaLink="false">https://medium.com/p/34a737d30f54</guid>
            <category><![CDATA[harness-engineering]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[graalvm]]></category>
            <category><![CDATA[ai-agent]]></category>
            <dc:creator><![CDATA[Christian Humer]]></dc:creator>
            <pubDate>Tue, 28 Jul 2026 14:50:18 GMT</pubDate>
            <atom:updated>2026-07-29T13:13:33.431Z</atom:updated>
            <content:encoded><![CDATA[<h4>Generating User Extensions with Graal Script Agent</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9ChLpkktTK2wH0TYDxba_Q.png" /></figure><p>AI is transforming how software is built, but many of its benefits are still focused on people who can already code. Coding agents make developers more productive because they can write, run, and revise software. Ordinary application users should be able to benefit from a similar kind of transformation without becoming programmers themselves.</p><p>An application user often knows the outcome they need: a particular search, report, validation rule, visualization, routing policy, or workflow. They can very well describe that outcome without knowing how to implement it. However, they cannot be expected to study extension points or review code generated on their behalf.</p><p>But would you let ordinary users install AI-generated code into a production application without anyone reviewing it? At first glance, the only responsible answer is no, no, and again no! But that assumes generated code must be trusted before it can run.</p><p>What if generated code could be useful without being able to perform destructive actions directly?</p><p>That is the motivation behind our <strong>Graal Script Agent </strong>library<strong>.</strong></p><h3>From prompt to result</h3><p>Graal Script Agent is a library that lets applications turn end-user prompts into sandboxed plugins. The application owner defines the allowed extension points. The model generates each script from the user’s prompt and extension contract without having access to production data. The application can then run it locally whenever needed.</p><p>As a first demo, we have added a new magic <em>Script Agent</em> tab to everyone’s favorite example, <a href="https://github.com/graalvm/micronaut-petclinic-graal-script-agent">PetClinic</a>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*v2M8ByMiOxanWW5QU-nOvw.png" /></figure><p>Now put yourself in the position of one of your users. Frank is a veterinarian trying to follow up with a pet owner. He has forgotten the owner’s name, but remembers that the owner lives somewhere in Madison. Let’s assume PetClinic’s built-in search supports owner lookups by last name only, so he is stuck.</p><p>Frank is amazing with animals but completely clueless about software. Today is his lucky day because it can stay that way. Instead of learning a complex query screen or a query language, he simply describes what he remembers and the data he wants:</p><blockquote>“Show owners in Madison with their telephone numbers.”</blockquote><p>And this is the result:</p><figure><img alt="A generated PetClinic query showing owners in Madison and their telephone numbers" src="https://cdn-images-1.medium.com/max/914/1*0DCiIrUBQp9DRkrZY2BAtQ.png" /></figure><p>He gets what he wanted: a list of owners in Madison, with each owner’s telephone number shown alongside their name, perhaps enough to refresh his memory.</p><p>What happened here? The agent selected the query extension point and generated a script against the query-only APIs exposed by the application. Because every capability in this extension point is non-destructive, PetClinic can execute the script directly and display its result.</p><p>Before we move on, notice the <em>Add to Find Owners page</em> button below the result. Clicking it persists the generated script as a plugin and makes this query available directly from PetClinic’s regular <em>Find Owners</em> page.</p><p>Whenever someone runs the saved query, the plugin executes locally against current data without another model call. Production objects stay inside the application, and the functionality remains available until it is removed, without anyone reviewing generated code.</p><p>A query is the straightforward case because it cannot change application data. But what if Frank asks PetClinic to make a change?</p><blockquote>“Add a dog named Fido born 2022–05–01 for George Franklin and add a visit on 2026–03–01 for an annual checkup.”</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/914/1*ESgecBP9kuC9cbkzdpYifQ.png" /></figure><p>This time, the agent selects a different extension point: one designed for data modifications. The generated script can query owner data and call an API for creating and modifying pets and owners. During script execution, however, the host records the requested modifications instead of committing them and displays the result.</p><p>Frank can now review the proposed pet and visit and choose whether to execute the modification. He reviews the domain-specific result, not the generated code. If he presses <em>Execute</em>, PetClinic validates and persists the approved modifications.</p><p>The selected extension point sets the security posture: queries can run directly, while modifications require review and approval.</p><p>That is the core idea. Now let’s find out how you can build such plugin mechanisms yourself with Graal Script Agent.</p><h3>Defining the Extension Points</h3><p>An extension point is a place where an application deliberately allows new behavior to plug in. In Java, it can be expressed as an interface that defines what a plugin must return and which application APIs it may use.</p><p>PetClinic’s extension contract can be simplified to this:</p><pre>sealed interface Extension<br>        permits QueryExtension, ModificationExtension {<br>}<br><br>non-sealed interface QueryExtension extends Extension {<br>    QueryResult execute(OwnerQueryApi ownersApi);<br>}<br><br>non-sealed interface ModificationExtension extends Extension {<br>    void execute(OwnerQueryApi ownersApi,<br>            ModificationApi modificationApi);<br>}<br><br>interface OwnerQueryApi {<br>    List&lt;OwnerView&gt; findByCityStartingWith(String cityPrefix);<br>}<br><br>interface ModificationApi {<br>    void addPet(String ownerFirstName, String ownerLastName,<br>            String petName, String petTypeName, String birthDate);<br><br>    void addVisit(String ownerFirstName, String ownerLastName,<br>            String petName, String visitDate, String description);<br>}</pre><p>Extension is the starting point for schema discovery. Its sealed hierarchy defines the available extension shapes, while each execute method defines<br>the script’s entry point and the application APIs available to it. The child interfaces remain non-sealed so the completed script can be bound to the<br>selected interface. This single-method shape is probably the most common, although extension contracts may contain multiple methods and richer type hierarchies.</p><p>With the extension point defined, generating an extension script only requires a model connector, a guest language, and the user’s prompt:</p><pre>Model model = /* LangChainModel.of(...) or SpringAiModel.of(...)*/;<br>String prompt = &quot;Show owners in Madison with their telephone numbers.&quot;;<br><br>Script&lt;Extension&gt; extensionScript;<br>try (ScriptAgent agent = ScriptAgent.newBuilder(model)<br>                           .language(&quot;js&quot;).build()) {<br>    extensionScript = agent.generate(<br>            Extension.class,<br>            prompt);<br>}</pre><p>First, Graal Script Agent discovers the contract starting from Extension, presents it to the model, and asks the model to produce one of its permitted extension types. For Frank’s first prompt, that is a QueryExtension. For his second, it is a ModificationExtension. If the request cannot be implemented against the schema, generation fails with a useful message instead of returning an invalid plugin.</p><p>In this example, language(“js”) configures JavaScript as the scripting language. JavaScript is compact and runs locally through embedded GraalJS. Graal Script Agent can also use GraalPy to author Python scripts if preferred.</p><p>Against the simplified contract above, the JavaScript generated for Frank’s modification request could look like this:</p><pre>util.implement(types.ModificationExtension, {<br>  execute(ownersApi, modificationApi) {<br>    modificationApi.addPet(&quot;George&quot;, &quot;Franklin&quot;, &quot;Fido&quot;, <br>                           &quot;dog&quot;,    &quot;2022-05-01&quot;);<br>    modificationApi.addVisit(&quot;George&quot;, &quot;Franklin&quot;, &quot;Fido&quot;, <br>                             &quot;2026-03-01&quot;, &quot;annual checkup&quot;);<br>  }<br>})</pre><p>The script agent runtime exposes helpers such as util.implement to make it easier for the model to fulfill the contract. The framework handles these internal details. More difficult prompts or larger schemas naturally produce more elaborate scripts, but this shows the basic shape.</p><p>This example defines the plugin contract using Java interfaces and lets Graal Script Agent discover it automatically. The <a href="https://graalvm.github.io/graal-script-agent/latest/index.html#extension-points">extension-point guide</a> explains how to design such contracts in much more detail.</p><h3>Sandboxing the Plugin</h3><p>The generated Script&lt;Extension&gt; can be serialized and persisted, but it is not an active plugin. Before the application can use it, the script must be bound to a GraalVM polyglot context for execution:</p><pre>try (Context context = Sandbox.UNTRUSTED.newContext(<br>        extensionScript.language())) {<br>    Extension extension = extensionScript.bind(context);<br><br>    if (extension instanceof QueryExtension query) {<br>        QueryResult result = query.execute(ownerQueryApi);<br>        display(result);<br>    } else if (extension instanceof ModificationExtension modification) {<br>        ModificationPlan plan = new ModificatinPlan();<br>        modification.execute(plan, ownerQueryApi);<br>        if (reviewByUser(plan)) {<br>            validateAndPersist(plan);<br>        }<br>    }<br>}</pre><p>Sandbox.UNTRUSTED treats the generated source as potentially adversarial. It blocks host filesystem, network, process, environment, and arbitrary Java access; runs the guest in an isolated runtime; and limits CPU time, memory, threads, output, and program complexity.</p><p>Graal Script Agent secures the boundary between that guest runtime and the application through a schema-driven host binding. The plugin can access only the members and types declared by the extension schema. Undeclared members remain inaccessible even if the underlying Java object exposes more functionality.</p><p>Neither layer can make a dangerous exposed API safe. Exposed host methods must still enforce authorization, validation, tenant boundaries, and side-effect policy. For a deeper discussion of sandbox policies, resource limits, isolates, and production considerations, please see our recently published <a href="https://medium.com/graalvm/sandboxing-script-extensions-with-graalvm-f192a8fb0c5f">blog post on sandboxing</a>.</p><h3>Giving the Model Context</h3><p>Coding agents became dramatically more useful when they got access to a shell. Early assistants mostly suggested code. Modern agents write code, run it, inspect the result, and revise. Code became the universal adapter between the agent and its environment.</p><p>Graal Script Agent applies the same idea inside a boundary controlled by the application. Frank’s prompt describes the result he wants, but not PetClinic’s Java API. The generated schema prompt supplies that grounding: it describes the available methods, types, records, enum values, and expected result shapes in language-specific form for JavaScript or Python.</p><p>For more complex extensions, Graal Script Agent adds an iterative feedback loop using application-controlled authoring tools rather than shell access:</p><ul><li>Draft editing lets the model build and revise one script.</li><li>Schema inspection provides type details on demand when the API is too large to present in full.</li><li>Sample inspection shows representative synthetic or sanitized application objects to the model and it can use a script to inspect it.</li><li>Mock execution runs the draft in a bounded environment so the model can observe failures and revise it.</li><li>Application-defined tests check the behavior before accepting a plugin and give feedback to the model when a test fails.</li></ul><p>Not every extension needs every tool. A clear prompt and a small schema may be enough for the Madison query. Larger or more context-dependent extension points benefit from samples, execution feedback, and tests. When application-defined tests are configured, completion succeeds only after all of them pass.</p><p>By default, the model receives the user’s prompt and the schema describing the extension API, not live application data. Sample inspection is an explicit opt-in that makes selected application objects visible to the model. Applications handling sensitive data should use synthetic or sanitized samples, or a local model.</p><h3>Turn on the music!</h3><p>As a kid, I was fascinated by Winamp’s music visualizations. I remember staring at them for hours. Creating one myself felt out of reach: too much graphics programming and too much math.</p><p>I am much, much older now, but Graal Script Agent lets me create a plugin system where I can prompt my own Winamp visualizations using natural language by exposing an extension point as small as this:</p><pre>void draw(PixelBuffer buffer, int tick, AudioFrame audio);</pre><p>The generated plugin receives a pixel buffer and a small audio summary on every frame. That tiny contract is enough to generate bars, waves, pulses, beat flashes, and spectrum effects without exposing the UI toolkit, audio system, files, or threads.</p><p>Now let’s prompt it with <em>“two rotating dodecahedra”</em>:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FG-V6F3wcorI%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DG-V6F3wcorI&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FG-V6F3wcorI%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/45c8eca35c9febd00967c4e3ecf40f6f/href">https://medium.com/media/45c8eca35c9febd00967c4e3ecf40f6f/href</a></iframe><p>The model is not part of the rendering loop; only the generated scripts are. These scripts run locally, filling a 320 × 180 pixel buffer while the demo targets 30 frames per second. GraalJS compiles the hot drawing function, so the visualization runs smoothly even inside the sandbox.</p><p>The source code is available <a href="https://github.com/graalvm/graal-script-agent/blob/main/examples/src/main/java/org/graalvm/scriptagent/examples/WinampDemo.java">here</a>.</p><h3>Filtering compiler graphs!</h3><p>We also integrated Graal Script Agent into <a href="https://www.graalvm.org/latest/tools/igv/">Ideal Graph Visualizer</a> (IGV), a tool the GraalVM team uses to inspect compiler graphs. These graphs can be very large, so we often write custom filters by hand to find the few nodes or paths relevant to an investigation. With Graal Script Agent, we can instead describe what we want to find or highlight, and it generates the corresponding filter plugin.</p><p>IGV demonstrates the opposite end of the spectrum from the tiny Winamp contract. It exposes a substantial graph API, while the concrete node classes, properties, and relationships vary with the compiler graph being inspected. The static schema describes the stable graph operations, but the exact shape of the data becomes clear only at authoring time. Sample inspection lets the model examine representative graphs and ground its generated filters in observed node names, properties, dependencies, block structure, and execution frequencies instead of guessing from generic conventions.</p><p>Below is a screenshot of IGV, filtering the most common control flow path of the graph. This is a very useful extension for our workflow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2OiqVYGoFUHusD3dg9NkEQ.png" /><figcaption>Below, a generated filter has reduced a large compiler graph to its most frequently executed path.</figcaption></figure><p>The source code for this integration will be available soon.</p><h3>Where It Fits and Where It Doesn’t</h3><p>Some behaviors should not be exposed as prompt-authored extensions at all. Core price calculations, authorization decisions, or irreversible bulk operations may require stronger assurance and a regular development process.</p><p>The best candidates are bounded customizations such as saved searches, ad-hoc reports, routing rules, UI filters, dashboard cards, visualizations, data exploration, and validation logic. They are often specific to one user, team, tenant, or task, valuable enough to matter, but too specialized to justify development.</p><p>Extensions can also propose modifications. The application then needs a stronger workflow: review the proposed operations, present them in domain terms, and require confirmation before enabling writes. To be honest, we haven’t fully explored this space, so we don’t exactly know where the limits are.</p><p>Not every useful extension needs to become a permanent plugin. Often the most value comes from complex one-off tasks: the user knows the outcome they need but not how to achieve it through the application’s existing screens and controls. The generated extension can run once and then be discarded.</p><p>Like a chatbot, the interaction begins with a natural-language request. Unlike a generic chatbot, the result is integrated with the application’s types, data, UI, and safety workflow. It can perform the task through application-defined APIs and present the result where the user already works.</p><h3>Try it yourself</h3><p>Graal Script Agent is in preview, but it is ready for you to try!</p><p>The docs include a <a href="https://graalvm.github.io/graal-script-agent/latest/index.html#getting-started">getting-started guide</a> with a minimal application covering schema discovery, script generation, sandboxed execution, and authoring tools.</p><p>Start small. Define one query-only Java interface. Expose only methods that you’re comfortable allowing untrusted code to call. Then prompt the agent to implement it and run the result inside an <a href="https://medium.com/graalvm/sandboxing-script-extensions-with-graalvm-f192a8fb0c5f">appropriately configured</a> sandbox.</p><p>We have built only a handful of applications with this so far, which is why feedback matters a lot at this stage. If prompt-authored extensions sound useful for something you are building, get in touch and join the #script-agent channel in our <a href="https://www.graalvm.org/slack-invitation/">community Slack</a>. Feel free to open any issues you encounter on <a href="https://github.com/graalvm/graal-script-agent">GitHub</a>.</p><h3>Further reading</h3><ul><li><a href="https://graalvm.github.io/graal-script-agent/latest/index.html">Graal Script Agent manual</a>: Start here for setup, schema discovery, runtime binding, and authoring tools</li><li>Blog Post on <a href="https://medium.com/graalvm/sandboxing-script-extensions-with-graalvm-f192a8fb0c5f">Sandboxing Script Extensions with GraalVM</a>: Find out more about sandbox policies, resource limits, isolates, and production considerations.</li><li><a href="https://github.com/graalvm/micronaut-petclinic-graal-script-agent">Micronaut PetClinic Script Agent demo</a>: The demo shown at the beginning.</li><li>Want to contribute? Check out the <a href="https://github.com/graalvm/graal-script-agent">GitHub repository</a>.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=34a737d30f54" width="1" height="1" alt=""><hr><p><a href="https://medium.com/graalvm/when-prompts-become-plugins-34a737d30f54">When Prompts Become Plugins</a> was originally published in <a href="https://medium.com/graalvm">graalvm</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[GraalVM 25.2 is here! ]]></title>
            <link>https://medium.com/graalvm/graalvm-25-2-is-here-600a9ae53f83?source=rss----7122626bf34b---4</link>
            <guid isPermaLink="false">https://medium.com/p/600a9ae53f83</guid>
            <category><![CDATA[jvm]]></category>
            <category><![CDATA[graalvm]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Alina Yurenko]]></dc:creator>
            <pubDate>Tue, 28 Jul 2026 14:29:17 GMT</pubDate>
            <atom:updated>2026-07-28T15:21:21.353Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*u9fPaxl0hCtEPFCjtgMKeg.jpeg" /></figure><p>Today, we’re pleased to announce the release of <strong>GraalVM 25.2</strong>!</p><p>GraalVM 25.2 is the latest release on our <a href="https://medium.com/graalvm/accelerating-the-graalvm-release-train-26b0d7cff2ab">innovation release train</a>, accelerating the delivery of new features and improvements. Meanwhile, GraalVM 25.0 will continue receiving regular Critical Patch Updates (CPUs) for applications that require production stability.</p><p>You can already <a href="https://www.graalvm.org/downloads/">download GraalVM</a> and check <a href="https://www.graalvm.org/release-notes/25.2/">the release notes</a> for more details.</p><p>GraalVM 25.2 brings <strong>sandboxed application plugins, reduced memory usage, G1 in Native Image across all platforms, and more</strong> — keep reading to see what’s new!</p><h3>Sandboxed application plugins with Graal Script Agent 🤖</h3><p>This releases introduces <a href="https://github.com/graalvm/graal-script-agent">Graal Script Agent</a>. Graal Script Agent turns <strong>natural-language requests into sandboxed application plugins</strong> that run locally, so users can customize applications for their specific needs without writing or reviewing code.</p><p>Many applications could benefit from this functionality, particularly those including dashboards, reporting, and data exploration. Extensions can also propose modifications, but the<strong> application should record and preview those operations for approval</strong> instead of allowing the generated script to commit them directly. The selected extension point determines the policy: a read-only query may execute immediately, while a modification requires validation and confirmation.</p><p>Application developers define extension points that specify what kinds of plugins may be generated and which application APIs they can access. Given a user request, a model uses these definitions to generate a JavaScript or Python plugin. The extension point also determines how the application handles the result: for example, a read-only query may run immediately, while a modification can require confirmation.</p><p><strong>Generated plugins run in a restricted GraalVM polyglot context with resource limits</strong>. Schema-driven bindings restrict their access to the application APIs explicitly exposed to them.</p><p>A plugin can run once and be discarded or be stored for later use. A stored plugin runs locally against current application data without requiring another model call.</p><p>To learn more about Graal Script Agent and see several use cases in action, read the <a href="https://medium.com/p/34a737d30f54">related blog post</a>.</p><h3>Reduced memory usage with compressed references 🚀</h3><p>GraalVM 25.2 introduces<strong> compressed references in Native Image</strong>, enabled by default. On 64-bit systems, references between managed objects are now stored as 32-bit heap-relative values instead of full 64-bit addresses.</p><p>This more compact representation can reduce the memory occupied by reference-heavy object graphs and reference arrays. It can also improve cache efficiency because more references fit into the processor’s fast cache. Build-time objects stored in the executable’s image heap may also occupy less space, although the effect on the total executable size depends on the proportion of code, metadata, resources, and image-heap data it contains.</p><p>In our test, a <a href="https://github.com/alina-yur/swiss-travel-advisor">Micronaut web application using Oracle Database</a> showed a <strong>39% reduction in RSS under load</strong> with GraalVM Community Edition 25.2 compared with 25.0:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G5BdY7cxQoFnBEl9mNXQDA.png" /><figcaption>Impact of compressed references on average RSS under load, MB</figcaption></figure><p>Compressed references do not make the executable 32-bit: native addresses and other native data remain 64-bit. The feature limits the managed heap to 32 GB. Applications requiring a larger heap can disable it at build time with -H:-UseCompressedReferences.</p><h3>G1 across all platforms!🎉</h3><p>Native Image now supports G1 GC on Windows! It can provide <strong>better throughput and lower latency</strong>, particularly for multithreaded applications and larger heaps. To enable it, build your application with --gc=G1.</p><p>This release also brings <strong>image startup and size improvements</strong>. Native images using G1 now start faster because more initialization work is completed at image build time, with applications that have larger image heaps seeing the greatest benefit. Native executables using G1 are also smaller when built with profile-guided optimizations (PGO) or -Os.</p><h3>Vector API Support by Default 🚀</h3><p>In this release we have enabled Java Vector API support by default when the jdk.incubator.vector module is part of the boot module layer.</p><p>The Vector API allows supported processors to perform the same operation on multiple values simultaneously using SIMD instructions. This can accelerate computation-heavy workloads such as machine learning, numerical computing, image processing, and data analysis, with results depending on the workload and target hardware.</p><h3>Community and Ecosystem 🌎</h3><ul><li>Thomas Wuerthinger will speak at IntelliJ IDEA Conf 2026 about <a href="https://lp.jetbrains.com/intellij-idea-conf-2026/#agenda">Sandboxing AI Agents with GraalVM </a>— you can still register to join online.</li><li>Check out the <a href="https://glaforge.dev/posts/2026/07/24/antigravity-brain-visualizer-now-with-a-contextual-smart-chat/">Antigravity session visualizer</a>, built by Guillaume Laforge using Micronaut, LangChain4j, and GraalVM!</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mjOFCt-INO59Wi2E" /><figcaption><a href="https://glaforge.dev/posts/2026/07/24/antigravity-brain-visualizer-now-with-a-contextual-smart-chat/">Antigravity Brain Visualizer</a></figcaption></figure><ul><li>Micronaut 5.1 was released just yesterday, with Oracle chat-memory auto-configuration and other features — <a href="https://micronaut.io/2026/07/27/micronaut-framework-5-1-0-release/">see what’s new</a>.</li><li>The <a href="https://github.com/HebiRobotics/reachability-annotations">Reachability Annotations</a> project, released by HEBI Robotics, generates Native Image metadata at compile time, simplifying configuration for reflection-heavy and FXML-based applications.</li><li>Meet <a href="https://github.com/codriverlabs/KubeMicroVM">KubeMicroVM</a>, a new Kubernetes operator and CLI for AWS Lambda MicroVMs, built with Quarkus and Native Image!</li><li>Inspect Parquet files directly in your browser: Gunnar Morling used GraalVM Web Image to <a href="https://x.com/gunnarmorling/status/2078929654627352720">compile Hardwood’s dive to WebAssembly</a>.</li></ul><h3>Conclusion</h3><p>We’d like to take this opportunity to thank our amazing contributors and community for all the feedback, suggestions, and contributions that went into this release.</p><p>If you have feedback for this release or suggestions for features that you would like to see in future releases, please share them with us on <a href="https://graalvm.org/slack-invitation">Slack</a>, <a href="https://github.com/oracle/graal">GitHub</a>, or <a href="https://www.linkedin.com/company/graalvm/">LinkedIn</a>.</p><p>Now go ahead and try the <a href="https://www.graalvm.org/downloads/">new GraalVM</a>! 🚀</p><p>— the GraalVM team</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=600a9ae53f83" width="1" height="1" alt=""><hr><p><a href="https://medium.com/graalvm/graalvm-25-2-is-here-600a9ae53f83">GraalVM 25.2 is here! 🎉</a> was originally published in <a href="https://medium.com/graalvm">graalvm</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Sandboxing Script Extensions with GraalVM]]></title>
            <link>https://medium.com/graalvm/sandboxing-script-extensions-with-graalvm-f192a8fb0c5f?source=rss----7122626bf34b---4</link>
            <guid isPermaLink="false">https://medium.com/p/f192a8fb0c5f</guid>
            <category><![CDATA[security]]></category>
            <category><![CDATA[sandboxing]]></category>
            <category><![CDATA[graalvm]]></category>
            <dc:creator><![CDATA[Christian Humer]]></dc:creator>
            <pubDate>Wed, 22 Jul 2026 11:03:39 GMT</pubDate>
            <atom:updated>2026-07-22T11:03:38.034Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xpzxWdCAIRcMJDHeFPqZ7Q.png" /></figure><p>Applications increasingly execute scripts that are supplied by customers, loaded dynamically, or generated by AI. This post shows how GraalVM sandboxing can restrict what those scripts access, limit the resources they consume, and isolate failures from the host application.</p><p>This post is for you if:</p><ul><li>You are developing a script extension system.</li><li>You run customer-provided scripts on your server.</li><li>Your application or AI model generate scripts.</li><li>You embed JavaScript or WebAssembly and do not trust the code or its input.</li></ul><p>Using a JavaScript discount rule as a running example, we progress from trusted application code to fully untrusted source. The same approach works whether the host runs on the JVM or as a native executable built with Native Image.</p><h3>Why script extensions need sandboxing</h3><p>Modern SaaS applications often let organizations customize business logic instead of forcing every customer through one fixed workflow. No product can anticipate every business model, integration, or policy, so extensibility is a key driver of adoption. An entire industry has grown around customizing business applications.</p><p>A custom extension fills in behavior that the application deliberately leaves open. The application defines which data the extension receives, which operations it may call, and which result it must return. The extension supplies the missing decision, transformation, or workflow. Unlike configuration, which selects behavior the product already implements, a script extension can express logic the product developers did not anticipate.</p><p>Extensions often take the form of small scripts evaluated at runtime. Scripts are particularly convenient because they can be loaded and changed without rebuilding the application.</p><p>When an application starts executing extension code, it crosses a boundary from trusted, well-audited code to more or less unknown territory: The extension code may contain an accidental infinite loop, allocate too much memory, process malicious input, or deliberately attack the runtime.</p><p>All the features discussed here are available in both Oracle GraalVM and GraalVM Community Edition.</p><h3>A discount rule extension</h3><p>Throughout this post, we use a discount-rule extension that receives an Order and returns a discount rate. It only needs the order total, so Order exposes a single method:</p><pre>public final class Order {<br><br>    private final double total;<br><br>    public Order(double total) {<br>        this.total = total;<br>    }<br><br>    @HostAccess.Export<br>    public double total() {<br>        return total;<br>    }<br>}</pre><p>A rule maintained with the application might look like this:</p><pre>Source rule = Source.create(&quot;js&quot;, &quot;&quot;&quot;<br>                (order) =&gt; {<br>                    if (order.total() &gt;= 100) {<br>                        return 0.10;<br>                    }<br>                    return 0.0;<br>                }<br>                &quot;&quot;&quot;);<br><br>try (Context context = Context.newBuilder(&quot;js&quot;).build()) {<br>    Value discount = context.eval(rule);<br>    double rate = discount.execute(new Order(120)).asDouble();<br>    System.out.println(rate);<br>}</pre><p>The flow has four parts. A Source represents the extension code independently of its execution. A Context contains the guest application state and configuration. context.eval(rule) evaluates the source in that context and returns a Value representing the JavaScript function. Calling execute() invokes that function with the exported Order.</p><p>The same Source can be evaluated in multiple contexts, with each evaluation producing a Value owned by that context.</p><p>Because no sandbox policy is specified, the context uses TRUSTED. Java and JavaScript execute in the same runtime and share the same heap, minimizing interoperability overhead and enabling joint optimization.</p><p>For an application-owned rule whose source and inputs are trusted, this may be the right trade-off. But not every extension should be able to reach every host capability or share resources with the application. GraalVM lets us add those boundaries without changing how the rule is represented, evaluated, and executed. We begin by limiting what the extension can access.</p><h3>Constraining host access</h3><p>The discount rule needs the order total, but nothing else from the host environment. A regular Context already starts with restrictive defaults: only explicitly exported host members are accessible, while native access, process creation, environment access, and host file and socket access are disabled.</p><p>With TRUSTED, these restrictions are defaults that application code can relax while configuring the context. CONSTRAINED enforces a restrictive configuration and rejects incompatible options, including those introduced by shared configuration code or later refactoring.</p><pre>try (Context context = Context.newBuilder(&quot;js&quot;)<br>                .sandbox(SandboxPolicy.CONSTRAINED)<br>                .out(OutputStream.nullOutputStream())<br>                .err(OutputStream.nullOutputStream())<br>                .build()) {<br>    Value discount = context.eval(rule);<br>    double rate = discount.execute(new Order(120)).asDouble();<br>    System.out.println(rate);<br>}</pre><p>Order.total() remains available because it is explicitly marked with @HostAccess.Export. Other host methods and classes are not exposed automatically. Naming &quot;js&quot; also limits the context to JavaScript; constrained contexts require an explicit list of permitted languages.</p><p>The policy prevents the final configuration from enabling broad host access, native access, process creation, system exit, host files or sockets, inherited environment variables, or host class loading. If any part of the application requests an incompatible capability, context creation fails rather than silently weakening the policy.</p><p>CONSTRAINED requires explicit guest output and error streams. This example discards both. Applications that retain guest output should use bounded, application-owned streams. The final System.out.println is host application code and is unaffected by this redirection.</p><p>The exposed surface is deliberately narrow:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cTB-ISZd8-aBmMf7V5wniA.png" /></figure><p>Every exported method remains an application-defined capability. If Order exposed refund() or cancel(), the script could call them. Exported methods must validate guest input and must not expose privileged operations. A sandbox cannot make a dangerous application operation safe.</p><p>CONSTRAINED limits the exposure of application code to the extension, but Java and Javascript still share the underlying runtime resources. Restricting CPU and memory usage requires the next level of sandboxing.</p><h3>Running trusted code on untrusted input</h3><p>At this point, it is tempting to think that a trusted rule with narrow host access needs no further safeguards. But trusted source does not imply trusted input. An attacker who cannot change the rule may still choose data that drives it into its worst possible behavior. Host-access restrictions do not prevent excessive CPU use, memory allocation, or failures in the guest runtime.</p><p>A production rule might parse a coupon, traverse line items, or inspect nested metadata. A crafted coupon can trigger a worst-case parsing path, a huge order can cause large temporary allocations, and an unexpected numeric value can send a loop into a state its author assumed was impossible. Malformed input may even expose a defect in a guest-language library or runtime.</p><p>ISOLATED addresses both concerns. In addition to enforcing the capability restrictions of CONSTRAINED, it requires CPU and isolate-heap limits and executes the guest code in a separate runtime instance with its own heap, garbage collector, and JIT compiler.</p><p>Isolated execution requires the isolate variant of the Maven language dependency. For JavaScript, use js-isolate with Oracle GraalVM or js-isolate-community with GraalVM Community Edition. See <a href="https://www.graalvm.org/latest/reference-manual/embed-languages/#polyglot-isolates">Configuring Polyglot Isolates</a> for dependency details.</p><pre>try (Context context = Context.newBuilder(&quot;js&quot;)<br>                .sandbox(SandboxPolicy.ISOLATED)<br>                .out(OutputStream.nullOutputStream())<br>                .err(OutputStream.nullOutputStream())<br>                .option(&quot;engine.MaxIsolateMemory&quot;, &quot;64MB&quot;)<br>                .option(&quot;sandbox.MaxCPUTime&quot;, &quot;2s&quot;)<br>                .build()) {<br>    Value discount = context.eval(rule);<br>    double rate = discount.execute(new Order(120)).asDouble();<br>    System.out.println(rate);<br>}</pre><p>The source, evaluation, and invocation remain unchanged. The memory and CPU limits shown above are mandatory for ISOLATED; context creation fails if either is omitted. Their values must be chosen by the application. A CPU-bound execution is canceled when it exceeds its configured budget, while guest allocations are accounted against the isolate heap.</p><p>The sandbox setup now looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*g6pQHjuJ8OQk124xarX8aQ.png" /><figcaption><em>Separate VM and heap, but still inside the same OS process.</em></figcaption></figure><p>By default, the guest runtime runs as an isolate inside the application process. It can also run in an external process when a separate address space and process boundary are required. We will return to that deployment choice later.</p><p>The Java API still looks the same, but separate heaps change the lifetime of references that cross between Java and JavaScript. That is the next consequence to consider.</p><h3>Working with separate heaps</h3><p>At first, separate heaps appear to make memory ownership simple: host objects live on one heap and guest objects on another. Cross-heap references complicate that picture.<br><br>A separate heap gives guest execution its own garbage collector and creates a clear accounting boundary for memory. When Java and JavaScript share a heap, their objects occupy the same memory pool. GraalVM can estimate how much memory a context retains, but that accounting is periodic and its limit may be exceeded before enforcement. It cannot provide a hard guest-only heap ceiling.</p><p>An isolate changes that. The engine.MaxIsolateMemory option places a hard cap on the complete guest runtime heap. Guest allocations are accounted against this limit rather than consuming space in the host application heap. This is a limit on the isolate heap, not on every form of native or process memory.</p><p>The separate heap does not prevent values from crossing between Java and JavaScript. Objects remain on their owning heap, while references across the boundary are represented by handles behind the scenes. This introduces a problem familiar from distributed garbage collection.</p><p>Suppose the discount rule also receives a DiscountLog callback through which it records why a discount was applied:</p><pre>public final class DiscountLog {<br><br>    private String reason;<br><br>    @HostAccess.Export<br>    public void report(Value details) {<br>        reason = details.getMember(&quot;reason&quot;).asString();<br>    }<br>}</pre><p>When JavaScript calls log.report({ reason: &quot;large order&quot; }), details is a guest value passed to Java. The implementation copies the required field into a Java String, so the host does not retain a reference into the guest heap.</p><p>Without scoped callback values, retaining details in a Java field could create a cross-heap cycle if the guest object referred back to log. The complete cycle might no longer be reachable by the application, yet each collector would still see an incoming reference from the other heap and keep its side alive:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5fmCweiQ8mq4igSpOc6qzQ.png" /><figcaption><em>Each collector sees an incoming reference from the other heap. Neither can determine that the cycle is dead.</em></figcaption></figure><p>Distributed garbage-collection problems are easy to dismiss because small tests usually finish before retained objects accumulate, and neither heap contains an obvious leak when examined independently. In production, however, cross-heap cycles can accumulate gradually and surface much later as growing isolate heaps, unexpected resource-limit failures, or reduced throughput. The callback that created the cycle may be far removed from the eventual symptom, making the problem particularly difficult to diagnose.</p><p>GraalVM avoids the common accidental form of this cycle by scoping guest values passed to exported Java callbacks. ISOLATED enables this behavior by default, as does UNTRUSTED, the policy for untrusted source discussed in the next section. The host can use such a value while the callback runs, but its reference into the guest heap is released when the callback returns:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UPA0N0XbdpSEaQyb36IJzg.png" /><figcaption><em>While the callback runs, the host may use the guest value and copy what it needs.</em></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*J8b_dTxUb5C_IWO0GRJGJA.png" /><figcaption><em>The host keeps copied data, not a reference into the guest heap.</em></figcaption></figure><p>With scoped callback parameters, details is valid only while report() executes. When the callback returns, the scoped Value is invalidated and its reference into the guest heap is released. Retaining it in a Java field and using it later would result in an IllegalStateException.</p><p>If a callback deliberately needs to retain a guest value, it can call details.pin() before returning. Pinning opts that value out of automatic release. It is an explicit lifetime decision and should be used carefully because it can reintroduce a long-lived reference across the two heaps.</p><p>The existing Order.total() callback accepts no guest object, so it requires no special handling.</p><p>Scoped callback parameters are the main interoperability rule applications need to account for when moving to separate heaps. Context creation, evaluation, and value invocation otherwise work as before.</p><p>With host capabilities, runtime failures, and cross-heap references accounted for, we can take the final step: stop trusting the rule source itself.</p><h3>Running untrusted code</h3><p>The best-understood example of untrusted JavaScript is the web browser. A browser downloads JavaScript from a website and executes it while assuming that the website may be malicious. The script may try to escape the JavaScript runtime, exploit the JIT compiler, exhaust resources, or misuse every capability available to it. It must not compromise the browser or gain arbitrary access to the local machine.</p><p>UNTRUSTED is designed for this adversarial model. In our example, the customer-supplied discount rule plays the role of website JavaScript, while the SaaS application plays the role of the browser.</p><p>So far, the application has owned the discount-rule source. Now assume that the source was submitted by a customer, loaded from an external database, or generated by AI. The Source itself must be treated as untrusted:</p><pre>Source rule = Source.create(&quot;js&quot;, submittedRule);</pre><p>Creating a Source only represents the submitted program. It does not validate the program or make it trusted. The sandbox is established when the source is evaluated in an appropriately configured context. Although the API may allow it, running untrusted source under any policy other than UNTRUSTED is unsupported and not covered by the sandbox.</p><p>With ISOLATED, we trusted the rule and protected the host from bugs or malicious input. With UNTRUSTED, the attacker controls the source processed by the parser, runtime, and JIT compiler. The attacker can deliberately construct programs that consume resources, exercise unusual runtime paths, or shape the machine code emitted by the compiler.</p><p>UNTRUSTED retains the separate runtime, heap, garbage collector, and JIT compiler provided by ISOLATED. It then requires additional resource limits and enables compiler and runtime defenses designed for adversarial guest code:</p><pre>double applyDiscount(Source rule, Order order) {<br>    try (Context context = Context.newBuilder(&quot;js&quot;)<br>                    .sandbox(SandboxPolicy.UNTRUSTED)<br>                    .out(OutputStream.nullOutputStream())<br>                    .err(OutputStream.nullOutputStream())<br>                    .option(&quot;engine.MaxIsolateMemory&quot;, &quot;64MB&quot;)<br>                    .option(&quot;sandbox.MaxHeapMemory&quot;, &quot;32MB&quot;)<br>                    .option(&quot;sandbox.MaxCPUTime&quot;, &quot;2s&quot;)<br>                    .option(&quot;sandbox.MaxASTDepth&quot;, &quot;100&quot;)<br>                    .option(&quot;sandbox.MaxThreads&quot;, &quot;1&quot;)<br>                    .option(&quot;sandbox.MaxOutputStreamSize&quot;, &quot;64KB&quot;)<br>                    .option(&quot;sandbox.MaxErrorStreamSize&quot;, &quot;64KB&quot;)<br>                    .build()) {<br>        Value discount = context.eval(rule);<br>        return discount.execute(order).asDouble();<br>    }<br>}</pre><p>As with ISOLATED, an UNTRUSTED execution is canceled after the runtime detects that its configured CPU budget has been exceeded. GraalVM reports the failure with a PolyglotException for which isResourceExhausted() returns true. Once a resource limit has canceled a context, that context can no longer execute guest code.</p><p>Attacker-controlled source also makes the JIT compiler part of the attack surface. UNTRUSTED <a href="https://dl.acm.org/doi/10.1145/3517208.3523751">blinds constants</a> embedded in generated machine code and randomizes compiled-function entry points, making the compiler output less predictable for JIT-spraying attacks. It also masks memory accesses and inserts <a href="https://dl.acm.org/doi/full/10.1145/3708821.3733887">speculative-execution barriers</a> where masking is not applicable. Because the guest has its own JIT compiler, these defenses can be applied to guest code without imposing their cost on the host application’s compiler.</p><p>One final assumption remains: if an execution was contained successfully, one might consider the result trusted, but it is not. A hostile rule may return -1, NaN, or 1000 without ever escaping the sandbox. The application must verify that the discount is finite and between 0 and 1. The sandbox constrains how the rule executes; it does not decide whether its answer is meaningful.</p><p>Under both ISOLATED and UNTRUSTED, the guest runtime runs as an internal isolate inside the application process by default. Applications that require a separate address space and process boundary can use the external isolate mode with engine.IsolateMode=external. The child process has a separate address space and signal domain, so fatal guest failures no longer terminate the host process.</p><p>The sandbox limit values in this example are illustrative rather than production defaults. Production limits should be derived from representative workloads; see <a href="https://www.graalvm.org/latest/security-guide/sandboxing/#resource-limits">Configuring sandbox resource limits</a>, including the use of sandbox.TraceLimits.</p><p>With the policy and its limits selected, the next decision is which scripts may share an engine.</p><h3>Sharing code within a security domain</h3><p>A SaaS application rarely runs one rule for one customer. It may run the same rules and libraries in many execution contexts across requests and tenants. Creating a separate engine for every context would needlessly parse and warm up that code repeatedly.</p><p>An explicit Engine defines the scope of code sharing. It owns the code cache: cached Source entries, parsed ASTs, profiling data, and optimized machine code. A Context owns an execution scope&#39;s application state, including global variables, guest objects, modules, and context-bound Value instances. Sharing an engine shares code, not application state.</p><p>By default, each context creates an implicit engine. An application opts into sharing by creating an explicit engine for a security domain and passing it to each context with Context.newBuilder(&quot;js&quot;).engine(domainEngine). The engine must be created with the domain&#39;s permitted languages, sandbox policy, default streams, and engine-level options such as engine.MaxIsolateMemory. Contexts attached to it must use compatible sandbox settings. The source can then be parsed once, and compilation and warmup can be amortized across contexts. This can reduce startup latency and memory when many contexts execute the same code.</p><p>Truffle languages, including Graal.js, implement context-independent code. This means the same JavaScript AST and optimized machine code can execute in different contexts without capturing the global variables or objects of the context that first ran it. This comes with a small peak-performance penalty: type checks, property accesses, and function or method calls cannot specialize as aggressively on context-specific object identities. When the same code runs in many contexts, avoiding repeated parsing and warmup typically outweighs that cost.</p><p>A security domain is a group of guest programs for which the application accepts sharing code artifacts and runtime resources. Programs in the same domain may all be untrusted relative to the host without being mutually distrusting. For example, one customer’s rules might share an engine, while rules from customers that must be isolated from one another use separate engines.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BRQcnVfyXi7yikcVh61REw.png" /></figure><p>All contexts sharing an engine use the engine’s sandbox policy. With isolate-backed execution, they also share the guest runtime, isolate heap, garbage collector, and JIT compiler. The hard engine.MaxIsolateMemory limit therefore applies to the domain&#39;s shared isolate. Context limits apply per context and generally accumulate across guest executions in that context. Scripts from different security domains need separate engines.</p><h3>Bringing sandboxed extensions to production</h3><p>Sandboxing has been a long-term focus for GraalVM. The Polyglot API, Truffle runtime, and GraalJS that provide its foundation have been used in production for many years. Within Oracle, <a href="https://www.netsuite.com/portal/resource/articles/erp/graal-runtime-technology-improves-netsuite-platform-developer-productivity.shtml">NetSuite SuiteScript</a> and <a href="https://www.graalvm.org/js/mle-oracle-db/">Oracle Database MLE</a> are longstanding users of these technologies. Picnic also uses GraalVM to run JavaScript and Python business rules from a Java backend, evaluating approximately 38 million rules per day, as described in <a href="https://www.youtube.com/watch?v=-JuchoMU75U">Extending Java with Python and JavaScript at Picnic</a>.</p><p>GraalVM is a good fit when a JVM host application needs to run JavaScript or WebAssembly close to its application data, expose a host API, and execute reusable code in many separate contexts, whether the host runs on the JVM or as a native executable built with Native Image. Native Image builds must include reachability metadata for host members exposed to guest code, as described under <a href="https://www.graalvm.org/latest/reference-manual/embed-languages/#configuring-native-host-reflection">Configuring Native Host Reflection</a>. GraalVM is particularly useful when scripts range from application-owned code to source submitted by untrusted users because the same Polyglot API supports progressively stronger capability, resource, and isolation boundaries. The same API can even be used for multiple languages.</p><p>In GraalVM 25.1, the sandbox policies support <a href="https://www.graalvm.org/javascript/">JavaScript</a> and <a href="https://www.graalvm.org/webassembly/">WebAssembly</a>. Sandbox support for pure <a href="https://graalpy.org/">Python</a> code without native extensions is under development.</p><p>GraalVM Community Edition 25.1 makes these sandboxing capabilities available to the broader GraalVM community. We look forward to seeing how developers use them to build, evaluate, and deploy sandboxed scripts. We welcome use cases, questions, and feedback through <a href="https://github.com/oracle/graal/issues">GitHub</a> and the <a href="https://www.graalvm.org/slack-invitation/">GraalVM Community Slack</a>.</p><p>To get started, see the documentation for <a href="https://www.graalvm.org/latest/reference-manual/embed-languages/">embedding languages</a>, <a href="https://www.graalvm.org/latest/reference-manual/embed-languages/#polyglot-isolates">Polyglot Isolates</a>, and <a href="https://www.graalvm.org/latest/security-guide/sandboxing/">sandboxing</a>.</p><p>Happy sandboxing!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f192a8fb0c5f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/graalvm/sandboxing-script-extensions-with-graalvm-f192a8fb0c5f">Sandboxing Script Extensions with GraalVM</a> was originally published in <a href="https://medium.com/graalvm">graalvm</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[GraalJS 25.1: Modern JavaScript on GraalVM]]></title>
            <link>https://medium.com/graalvm/graaljs-25-1-modern-javascript-on-graalvm-25f5c7cea361?source=rss----7122626bf34b---4</link>
            <guid isPermaLink="false">https://medium.com/p/25f5c7cea361</guid>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[graalvm]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[graaljs]]></category>
            <dc:creator><![CDATA[Andreas Woess]]></dc:creator>
            <pubDate>Tue, 14 Jul 2026 13:33:50 GMT</pubDate>
            <atom:updated>2026-07-14T14:29:36.272Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1pD0dTqxCE-OMIzpMEcsNA.png" /></figure><p><a href="https://www.graalvm.org/javascript/"><strong>GraalJS</strong></a>, the high-performance embeddable JavaScript implementation on <strong>GraalVM</strong>, is here with the latest ECMAScript features!</p><p>JavaScript is a core part of modern web development. At Oracle, we use it in multiple ways: to build products and services, to let developers extend our platforms, and to connect applications with Oracle services.</p><p>We also participate in moving the language forward. Oracle is part of <a href="https://ecma-international.org/members/">Ecma International</a> and <strong>TC39</strong>, the committee that evolves ECMAScript, the standardized language implemented by JavaScript engines.</p><p>GraalJS is a key part of that work. It is modern, fast, easy to embed in Java and Kotlin applications, and interoperable with GraalPy and other Graal Languages. Through GraalJS’s support for Node, it can also run Node.js applications, and it uses GraalWasm to implement the WebAssembly JavaScript API.</p><p>GraalJS is developed in the open, with source code and community distributions available under the Universal Permissive License (UPL). GraalJS is published on <a href="https://github.com/oracle/graaljs">GitHub</a> and Maven Central, and its compatibility work is tested against Test262, the official ECMAScript conformance suite with more than 50,000 tests.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pYXHVQDqb7IE_k0e0Lkt1g.png" /><figcaption><a href="https://compat-table.github.io/compat-table/es2016plus/">ECMAScript Runtime Compatibility Matrix</a></figcaption></figure><p>The unique features of GraalJS, such as embeddability and fast interoperability with Java, enable several important use cases in Oracle’s systems. <strong>NetSuite</strong> is a good example: <a href="https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/article_163726005075.html">SuiteScript</a>, its scripting engine that supports sophisticated procedural logic, <strong>is powered by GraalJS</strong>. For customers and partners developing in SuiteScript, this provides access to the full range of modern JavaScript language features out of the box.</p><p><strong>Oracle Database</strong> takes the idea of embeddability in another direction. The <a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/introduction-to-mle.html"><strong>Multilingual Engine (MLE)</strong></a> in Oracle Database lets developers run JavaScript close to the data, including dynamic snippets and JavaScript modules stored in the database. Under the hood, MLE uses GraalVM’s embedding and Native Image capabilities to <strong>bring JavaScript execution into the database environment.</strong></p><p>The latest release, <strong>GraalJS 25.1</strong>, ships full support for <a href="https://tc39.es/ecma262/2026/"><strong>ECMAScript 2026</strong></a>, the 17th edition of the ECMAScript standard, and makes it the default language version.</p><p>For most applications, no extra configuration is required: the latest standardized JavaScript language and library features are available out of the box. Applications that need to pin a specific ECMAScript version for compatibility can continue to do so with the js.ecmascript-version option.</p><h4><strong>ECMAScript 2026 Highlights</strong></h4><p>ECMAScript 2026 standardizes practical additions across numeric computation, iterators, asynchronous data collection, error detection, keyed collections, binary encoding, and JSON processing. These features are available by default in GraalJS 25.1.</p><ul><li><strong>Array.fromAsync()</strong> constructs arrays from async iterables, sync iterables, and array-like inputs. It also accepts an optional mapping function applied to each element, and the result is awaited before it is added to the array.</li><li><strong>Iterator.concat()</strong> sequences multiple iterator or iterable inputs into one lazy iterator, so iterator-based code can stay composable without allocating intermediate arrays.</li><li><strong>Math.sumPrecise()</strong> sums an iterable of floating-point numbers with improved numerical stability and minimal precision loss, especially when values have different magnitudes. For example, Math.sumPrecise([1e16, 0.1, -1e16]) returns 0.1, while naive summation returns 0.</li><li><strong>Get-or-insert APIs for </strong><strong>Map and </strong><strong>WeakMap</strong> return an existing value when one is already present, or provide a default value during retrieval when the key is not already present: getOrInsert() and getOrInsertComputed() on Map.prototype and WeakMap.prototype. These APIs simplify common cache, memoization, and grouping patterns.</li><li><strong>Hex and Base64 string support for Uint8Array</strong> adds binary conversion APIs for strings of hexadecimal- and Base64-encoded binary data, including fromBase64(), toBase64(), setFromBase64(), fromHex(), toHex(), and setFromHex().</li><li><strong>More expressive JSON APIs</strong> let JSON.parse() revivers access the matched segment of JSON source text for the current value, which is useful to preserve precise numeric values. ES2026 also adds JSON.rawJSON() and JSON.isRawJSON(); JSON.rawJSON() allows validated raw JSON fragments to be embedded during stringification, giving applications fine-grained control over JSON.stringify() output for primitive values. Together, these additions allow serialization of BigInt values and make JSON handling more extensible.</li><li><strong>Error.isError()</strong> provides a standard way to test whether a value is an actual error object, instead of relying on less robust cross-realm-sensitive checks such as instanceof Error.</li></ul><h4><strong>Beyond ECMAScript 2026</strong></h4><p>GraalJS 25.1 also includes support for selected language features outside ECMAScript 2026. Temporal is available in ECMAScript 2027 mode. Some earlier-stage proposals are available in ECMAScript staging mode, while others use explicit experimental options.</p><p><strong>Temporal in ECMAScript 2027 Mode</strong></p><p>GraalJS 25.1 completes support for <strong>Temporal</strong>, with modern date/time APIs such as Temporal.PlainDate for calendar dates and Temporal.ZonedDateTime for time-zone-aware scheduling and arithmetic.</p><p>Temporal is not part of ECMAScript 2026, so it is not enabled by default. Use ECMAScript 2027 mode to enable it:</p><pre>js --js.ecmascript-version=2027 app.mjs</pre><h4><strong>Non-Final ECMAScript Features</strong></h4><p>GraalJS 25.1 includes support for several proposals that have not been finalized yet. These features are experimental and may still change.</p><p>In ECMAScript staging mode, GraalJS 25.1 includes:</p><ul><li><strong>Immutable ArrayBuffers</strong>: APIs for creating ArrayBuffers that cannot be mutated, resized, detached, or transferred: transferToImmutable() and sliceToImmutable().</li><li><strong>Joint Iteration</strong>: Iterator.zip() and Iterator.zipKeyed() for advancing multiple iterators together and working with positionally aligned values.</li></ul><p>To enable ECMAScript staging features, set js.ecmascript-version to “staging”: js --js.ecmascript-version=staging app.mjs.</p><p>GraalJS 25.1 also includes additional proposals behind explicit experimental options:</p><ul><li><strong>Explicit Resource Management</strong> ( --js.explicit-resource-management): using / await using declarations and disposable resource stacks for deterministic cleanup.</li><li><strong>Import Text</strong> (--js.import-text): imports text resources as strings, such as YAML or template files.</li><li><strong>Import Bytes</strong> (--js.import-bytes): imports arbitrary files as raw bytes using import attributes, returning a Uint8Array backed by an immutable ArrayBuffer.</li></ul><p><strong>Node.js 24</strong></p><p>GraalJS 25.1 updates the Node support to <strong>Node.js 24.14.1</strong>, bringing the Node.js 24.x LTS line into the release alongside ECMAScript 2026 support.</p><p><strong>Experimental Web Crypto API Option</strong></p><p>GraalJS 25.1 also adds the experimental--js.crypto option to expose getRandomValues() and randomUUID() from the Web Crypto API. Use it with --experimental-options --js.crypto=true app.mjs.</p><p>For the full list of GraalJS changes, see the <a href="https://github.com/oracle/graaljs/blob/master/CHANGELOG.md">CHANGELOG on GitHub</a>.</p><h3><strong>Conclusion</strong></h3><p>GraalJS 25.1 brings the latest JavaScript features to GraalJS by default, while keeping compatibility options available for applications that need them. It also updates to Node.js 24 and includes experimental features for developers who want to try what is coming next.</p><p>We would like to thank our contributors and community for the feedback, suggestions, and contributions that went into this release. If you have feedback on GraalJS 25.1 or suggestions for future releases, share them with us on <a href="https://graalvm.org/slack-invitation">Slack</a>, <a href="https://github.com/oracle/graal">GitHub</a>, or <a href="https://twitter.com/graalvm">Twitter</a>.</p><p>Now go ahead and try <a href="https://www.graalvm.org/javascript/">GraalJS 25.1</a>!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=25f5c7cea361" width="1" height="1" alt=""><hr><p><a href="https://medium.com/graalvm/graaljs-25-1-modern-javascript-on-graalvm-25f5c7cea361">GraalJS 25.1: Modern JavaScript on GraalVM</a> was originally published in <a href="https://medium.com/graalvm">graalvm</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[GraalVM 25.1 is here! ]]></title>
            <link>https://medium.com/graalvm/graalvm-25-1-is-here-13829606982e?source=rss----7122626bf34b---4</link>
            <guid isPermaLink="false">https://medium.com/p/13829606982e</guid>
            <category><![CDATA[graalvm]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[Alina Yurenko]]></dc:creator>
            <pubDate>Tue, 30 Jun 2026 14:56:14 GMT</pubDate>
            <atom:updated>2026-06-30T17:32:16.582Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5kJfpRSi_vDCi1yWkinRSA.jpeg" /></figure><h3>First innovation release 🚀</h3><p>Today, we’re pleased to announce the release of <strong>GraalVM 25.1</strong>!</p><p>GraalVM 25.1 is the first innovation release to build on GraalVM 25, with 25.2, 25.3, and subsequent releases to follow. <a href="https://medium.com/graalvm/accelerating-the-graalvm-release-train-26b0d7cff2ab">This new release train</a> will deliver features and improvements sooner, through smaller, incremental releases that make it easier to try new capabilities and provide feedback.</p><p>Meanwhile, the long-term support GraalVM 25.0 release train will continue to receive regular Critical Patch Updates (CPUs), providing the stability that production applications require.</p><p>You can already <a href="https://www.graalvm.org/downloads/">download GraalVM</a> and check <a href="https://www.graalvm.org/release-notes/25.1/">the release notes</a> for more details.</p><p>This release is packed with new exciting features: even smaller native images, Native Image skill, AI-powered reachability metadata, polyglot sandboxing, and so much more — keep reading to learn about them!</p><p>You can also watch the release stream with our favorite features and demos:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FvmPscsMBqFc%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DvmPscsMBqFc&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FvmPscsMBqFc%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/4166d2884d0d2de3f3bc1446931ddbdc/href">https://medium.com/media/4166d2884d0d2de3f3bc1446931ddbdc/href</a></iframe><h3>Even Smaller Native Images 📦</h3><p>Native Image is widely used to produce small, self-contained native executables. It does this through sophisticated reachability analysis that includes only the parts of your application you actually need, powerful optimizations from the Graal compiler, and more.</p><p><strong>In this release, we further reduce the size of native images. We see an improvement of around 3% across a broad set of benchmarks, with small applications, such as CLI and automation tools, seeing the most significant gains. You can expect even more updates towards reducing image size in the next releases!</strong></p><p>This improvement is achieved by reducing metadata stored in the image, making image heap storage more compact, and new more efficient String.format optimization.</p><p>As an example, we measured the impact of size optimization on the <a href="https://github.com/google/google-java-format">google-java-format</a> project, which uses GraalVM Native Image to produce compact executables for distribution. <strong>The current release contains a linux-x86–64 build of 33.03 MB, while built with GraalVM 25.1 it goes down to 30.74 MB — 6.94% smaller</strong>!</p><h3>Native Image Performance 🚀</h3><p>Great news: this release introduces a highly-requested by our community feature: <strong>G1 GC is now available in Native Image on Darwin/aarch64</strong>! This means that you can have the same build setup for native images between your local Darwin AArch64 machine and a Linux-based deployment environment, or have better performance for your applications running on Darwin.</p><p>We also introduced a new Serial GC policy, Adaptive2, which uses mark-compact collection in the old generation by default. On average, <strong>this policy reduces memory usage and often improves throughput and latency</strong>. In our benchmarks, we observe improvements around peak RSS, throughput, and latency, although the exact impact varies by workload. For example, a <a href="https://github.com/barista-benchmarks/barista/tree/master/benchmarks/micronaut-shopcart">Micronaut ShopCart application</a> using Adaptive2 shows <strong>~8% lower RSS and 3.8% higher throughput</strong>.</p><h3>1500 libraries in the GraalVM Reachability Metadata Repository! 🤖</h3><p>Native Image uses a closed-world assumption for AOT compilation. Static analysis can infer a lot, but some behavior is only visible at run time. <strong>Reachability metadata </strong>fills that gap for dynamic features such as reflection, resources, serialization, JNI, and proxies.</p><p>To simplify and automate working with reachability metadata for our users, several years ago we introduced <a href="https://github.com/oracle/graalvm-reachability-metadata">GraalVM Reachability Metadata Repository</a>. The goal was simple: let users share and reuse metadata instead of solving the same compatibility problems in every application. Over the years, we built out the repository together with Spring and Micronaut teams, and other members of the GraalVM community.</p><p>This year, with the rise of production-grade AI agents, we saw an opportunity to scale this work. <strong>We use a fleet of AI agents and a carefully designed workflow to generate the necessary metadata for thousands of libraries</strong>. The workflow starts with Native Image reports, uses tests to exercise dynamic behavior through standard library APIs, generates metadata from observed execution, and verifies the result with Native Image, CI, and human review.</p><p>As a result, <a href="https://graalvm.github.io/native-build-tools/latest/index.html">Native Build Tools</a> now can automatically pull reachability metadata for almost 1500 libraries!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Oyo6TSuOE21esTtKX7nYxQ.png" /><figcaption>~1500 libraries covered by the <a href="https://github.com/oracle/graalvm-reachability-metadata">Reachability Metadata Repository</a></figcaption></figure><p>To benefit from the repository, just make sure that your Maven or Gradle project uses <a href="https://graalvm.github.io/native-build-tools/latest/index.html">Native Build Tools</a>.</p><p>Requesting reachability metadata for a new library is as simple as providing its Maven coordinates — follow the <a href="https://github.com/oracle/graalvm-reachability-metadata/tree/master#-manually-request-library-support">instructions on GitHub</a>.</p><h3>Native Image Skill for AI Agents 🤖</h3><p>We added a <a href="https://github.com/oracle/skills/tree/main/graal/native-image">Native Image skill</a> to <a href="https://github.com/oracle/skills">oracle/skills</a>! This repository is a centralized collection of specialized skills for working with common Oracle technologies, such as Oracle Database, Oracle APEX, GraalVM, Fusion, OCI, and others.</p><p>Our skill provides AI assistants with the best practices and practical steps for common Native Image workflows and tasks. By using this skill, an AI assistant can help you:</p><ul><li>Build native executables with the Native Image CLI;</li><li>Configure Native Build Tools for Maven and Gradle projects;</li><li>Set common build options for performance, binary size, monitoring, and security;</li><li>Work with reachability metadata;</li><li>Troubleshoot build failures, runtime differences, class initialization issues, missing metadata, and other issues.</li></ul><p>We look forward to your feedback as you try the skill in your projects. You <a href="https://github.com/oracle/skills/tree/main/graal/native-image">can explore on GitHub</a>, or install from command line:</p><pre>npx skills add oracle/skills/graal</pre><h3>Dynamic Class Loading in Native Image 🌎</h3><p>This release includes an early version of D<strong>ynamic Class Loading </strong>in Native Image. It can be useful for libraries and tools that generate JVM bytecode at run time, and open the door to more plugin use cases, such as loading javac annotation processors dynamically. Good example of such project is for example <strong>ByteBuddy</strong> — a library commonly used to generate and modify classes at runtime.</p><p>To try it, pass -H:+RuntimeClassLoading to your Native Image build.</p><p>While Crema is still a work in progress, it includes another step in<br> that direction: it adds <strong>Ristretto</strong> <strong>— a JIT compiler for runtime-loaded classes</strong>. To try it, additionally pass -H:+GraalJITCompileAtRuntime to your Crema build.</p><h3>Developer Experience Improvements 🛠️</h3><ul><li>JFR recordings and heap dumps on Windows: Native Image now supports these diagnostics on Windows, making debugging more consistent across platforms.</li><li>--print-options for native-image: print available build options directly from the tool in table, Markdown, or JSON format. It can be useful for option discovery, docs, and tooling.</li><li>JFR emergency dumps on POSIX: when an out-of-memory error happens while a JFR recording is active, Native Image can preserve the in-memory recording data in an emergency JFR file.</li><li>Runtime collection of conditional reachability metadata: -XX:TraceMetadataConditionPackages helps collect more precise metadata from a native image at run time.</li><li>Simpler tracing-agent filtering: the Native Image agent now applies built-in filters by default. Custom filters can still be passed directly when needed.</li><li>Reachability metadata schema shipped with GraalVM: the schema is now available locally at &lt;graalvm-home&gt;/lib/svm/schemas/reachability-metadata-schema.json, with more detailed validation constraints.</li><li>Portable Native Image bundles: bundle path maps are now portable across platforms, and the bundle format is versioned as 1.0.</li><li>Bundle size warnings: Native Image bundles can now warn when individual files or the total bundle size exceed configured limits.</li><li>-Werror for Native Image: treat warnings as errors, which is useful for CI and stricter build pipelines.</li></ul><h3>GraalPy 🐍</h3><ul><li>GraalPy now includes new -X jit=0|1|2 presets. These presets make it easier to tune GraalPy for different workloads, from startup-heavy applications to throughput-oriented ones. The GraalPy launcher now defaults to the jit=1 preset.</li><li>The GraalPy Native standalone on Linux now uses a lower-footprint Native Image garbage collection configuration. This reduces resident set size (RSS) for many workloads, but may increase startup time and warmup time — we encourage you to try it on your own workloads.</li><li>Java interoperability got several useful improvements. Foreign buffer objects are now treated as Python buffer-compatible binary objects, so APIs such as memoryview, bytes, bytearray, binascii.hexlify, and io.BytesIO work naturally with them. This makes it easier to pass binary data between Python and Java ByteBuffer or ByteSequence objects, with minimal and sometimes zero copies.</li><li>For embedding use cases, GraalPy adds more precise control over execution. New Truffle source options let embedders set the Python optimization level with python.Optimize and run code with a fresh globals dictionary using python.NewGlobals. There is also a new Java backend for pyexpat, useful when running without native access or in multiple-context scenarios. This backend is the default when embedding; the standalone distribution still defaults to native expat.</li><li>GraalPy 25.1 also improves compatibility. The _ctypes module now uses the native CPython version instead of the previous intrinsified implementation, which improves compatibility and reduces the memory footprint of using ctypes. GraalPy also dispatches sys.audit events to hooks registered with sys.addaudithook, including events raised through the PySys_Audit C API.</li><li>Standalone JVM artifacts are no longer released as separate distributions. For standalone deployments, use the GraalPy native artifacts. If you require Java interoperability, use our jbang launcher (jbang graalpy@oracle/graalpython -c &quot;print(&#39;hello from GraalPy&#39;)&quot;) or a custom embedding.</li></ul><h3>GraalJS and GraalWasm</h3><p>GraalVM 25.1 also brings important updates for GraalJS, including new ECMAScript support, Node.js updates, new WebAssembly proposals, and more.</p><ul><li>ECMAScript 2026 mode and features are now enabled by default.</li><li>Node.js has been updated to version 24.14.1.</li><li>Temporal support is complete and available in ECMAScript 2027 mode: enable with --js.ecmascript-version=2027</li><li>The new experimental js.crypto option provides getRandomValues() and randomUUID() from the Web Crypto API.</li><li>The stable js.performance option provides performance.now(), timeOrigin, and toJSON() from the Web High Resolution Time API.</li><li>The WebAssembly GC proposal is now implemented and enabled by default! It can be disabled with wasm.GC=false.</li><li>GraalWasm now uses a bytecode-handler-based design for the WebAssembly interpreter, improving interpreted performance on Native Image.</li><li>Standardized WebAssembly features Extended Constant Expressions, Multiple Memories, Relaxed SIMD, and Typed Function References are now enabled by default.</li></ul><p>For a detailed list of GraalJS and GraalWasm updates in this release, check <a href="https://www.graalvm.org/release-notes/25.1/">the release notes.</a></p><h3>Polyglot Runtime</h3><p>Also new in GraalVM 25.1, <strong>the Truffle sandbox is now available in GraalVM Community Edition</strong>, giving embedders a practical way to run guest JavaScript or WebAssembly behind a configurable security boundary. Sandboxing is becoming increasingly important as applications look for safe ways to run AI-written code: for example, an application could let users install AI-generated JavaScript plugins while limiting what those plugins can read, how long they can run, and which application APIs they may call. With the Truffle sandbox, applications can restrict host access, file and socket access, native calls, process creation, output, memory, CPU time, statement count, stack depth, and more. We will share more about the Truffle sandbox in upcoming posts, including a deeper technical look and a new project that builds on it to turn prompts into application plugins.</p><ul><li>Starting with Polyglot 25.1, the optimizing Truffle runtime is supported only with GraalVM 25.1 or later, including Oracle GraalVM and GraalVM Community Edition. On standard JDKs down to JDK 21, you can either use polyglot isolates, or the fallback runtime without runtime compilation.</li><li>The JavaScript polyglot isolate now includes support for WebAssembly.</li><li>Language launchers now use --polyglot by default, so you no longer need to pass it explicitly to use other languages in standalones.</li></ul><p>For a detailed list of polyglot runtime and Truffle framework updates in this release, check <a href="https://www.graalvm.org/release-notes/25.1/">the release notes</a>.</p><h3>Community and Ecosystem</h3><ul><li>After migrating to Native Image, <a href="https://www.trivago.com/">trivago</a> eliminated warm-up and reduced replicas from 43 to 12 for their GraphQL gateway, handling 9,000+ req/s per subgraph. <a href="https://medium.com/graalvm/inside-trivagos-graalvm-migration-native-image-for-graphql-at-scale-912bca9df841">Learn more about their migration process and recommendations</a>.</li><li><a href="https://medium.com/graalvm/javascript-in-java-driving-dynamic-uis-with-graalvm-at-scale-f552fb174df3">Learn how Picnic built a rule engine on GraalVM</a>, where rules are defined in JavaScript or Python and executed by a Java backend, handling 37M evaluations and 2M actions daily.</li><li>A local AWS emulator that starts in 24 ms and uses just 13 MB of idle memory? <a href="https://www.youtube.com/watch?v=wQVQHuJ0FC0">Learn how Hector Ventura built Floci </a>— free, open-source, and powered by GraalVM!</li><li>GraalVM accepts <a href="https://github.com/oracle/graal/blob/master/CODING_ASSISTANTS.md">AI-assisted contributions</a>, while the human contributor remains responsible for reviewing and verifying the submitted change;</li><li><a href="https://micronaut.io/2026/05/20/micronaut-framework-5-0-0-released/">Micronaut 5</a> was released, moving to a Java 25 and GraalVM 25.0.3 baseline.</li><li>Quarkus 3.35 offers an out-of-the-box PGO integration: enable it with quarkus.native.pgo.enabled=true, and use your existing integration tests for the profiling workflow.</li></ul><h3>Conclusion</h3><p>We’d like to take this opportunity to thank our amazing contributors and community for all the feedback, suggestions, and contributions that went into this release.</p><p>If you have feedback for this release or suggestions for features that you would like to see in future releases, please share them with us on <a href="https://graalvm.org/slack-invitation">Slack</a>, <a href="https://github.com/oracle/graal">GitHub</a>, or <a href="https://www.linkedin.com/company/graalvm/">LinkedIn</a>.</p><p>Now go ahead and try the <a href="https://www.graalvm.org/downloads/">new GraalVM</a>! 🚀</p><p>— the GraalVM team</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=13829606982e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/graalvm/graalvm-25-1-is-here-13829606982e">GraalVM 25.1 is here! 🎉</a> was originally published in <a href="https://medium.com/graalvm">graalvm</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accelerating the GraalVM Release Train]]></title>
            <link>https://medium.com/graalvm/accelerating-the-graalvm-release-train-26b0d7cff2ab?source=rss----7122626bf34b---4</link>
            <guid isPermaLink="false">https://medium.com/p/26b0d7cff2ab</guid>
            <category><![CDATA[graalvm]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[javascript]]></category>
            <dc:creator><![CDATA[Shaun Smith]]></dc:creator>
            <pubDate>Wed, 06 May 2026 12:39:57 GMT</pubDate>
            <atom:updated>2026-05-11T13:40:49.888Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*f8A75i6dKFHSGu8aCwqEiw.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@fikrirasyid?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Fikri Rasyid</a> on <a href="https://unsplash.com/photos/time-lapse-photography-of-white-train-3ba59K57wE4?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption></figure><p>Software is moving faster than ever, fueled by AI and rapidly evolving developer tools. To keep pace and get new features and improvements into your hands sooner, we are accelerating the GraalVM release schedule. We are introducing monthly feature releases while continuing to provide quarterly Critical Patch Updates (CPUs) and maintaining one stable release train per major version.</p><h3>What this means moving forward:</h3><ul><li>Oracle GraalVM 25.0 will continue to receive security and minor bug fixes and remains the stable release train.</li><li>GraalVM 25.1+ will provide monthly feature releases for users who want new features sooner. We intend to ship 25.1 in June 2026, followed by 25.2, 25.3, and subsequent releases in the months that follow.</li></ul><h3>What’s changing</h3><ul><li>GraalVM Community Edition previously shipped feature releases every six months, with each receiving two quarterly CPUs before being superseded by the next feature release. Going forward, GraalVM Community Edition will switch to monthly feature releases, incorporating CPUs when they are available each quarter.</li><li>Feature releases for both Oracle GraalVM and GraalVM Community Edition will now ship monthly on the 25.1+ release train. Feature releases are fully tested to ensure stability.</li></ul><h3>What stays the same</h3><ul><li>Quarterly CPUs remain aligned with Oracle and OpenJDK schedules, and all active releases receive the relevant security baseline.</li><li>Weekly Early Access (EA) builds will continue be made available to provide a channel for pre-GA feedback.</li></ul><h3>Why we’re doing this</h3><p>Monthly releases reduce the time from idea to impact. Smaller, more frequent drops make it easier for you to try features and provide actionable feedback, which helps us iterate faster. The ecosystem around AI-enhanced development and runtime optimization evolves weekly, and our cadence should reflect that reality. At the same time, security updates remain predictable through quarterly CPUs aligned with the broader Oracle and OpenJDK ecosystem.</p><h3><strong>Schedule for adopting new features for GraalVM languages</strong></h3><p>We pick up new features for GraalVM languages once the language is stabilized and starts seeing adoption from the library ecosystem. For Python, this generally means we support a Python version when it enters the security-fix phase (yellow) in the <a href="https://devguide.python.org/versions/">release chart</a> below. The current supported version of Python in GraalVM is Python 3.12, which entered yellow in 2025. Python 3.13 will be supported in 2026.</p><figure><img alt="Python release schedule." src="https://cdn-images-1.medium.com/max/828/1*tkD4DbzRi1lSMRyJapzE-Q.png" /><figcaption>Python Release Schedule</figcaption></figure><p>For JavaScript, a new <a href="https://ecma-international.org/publications-and-standards/standards/ecma-262/">ECMAScript standard</a> is generally published each June, with support in GraalVM implemented within a few months. This will continue.</p><p>The Java library ecosystem generally does not support anything other than Java LTS releases, so as previously announced, there are no plans for GraalVM releases of JDK 26, 27, or 28. GraalVM will track upstream changes and plans to provide Java 29 support shortly after its release.</p><h3>Understanding release numbers</h3><p>GraalVM continues to follow the JDK version numbering scheme described in <a href="https://openjdk.org/jeps/223">JEP 223</a> (MAJOR.MINOR.SECURITY). The MAJOR number indicates the Java language specification baseline. The MINOR number indicates the feature release train, such as 25.1, 25.2, and so on. The SECURITY number indicates the CPU level from the underlying JDK update, for example, .3 for April 2026.</p><h3>What this looks like on the calendar</h3><h4>Oracle GraalVM 25 LTS</h4><p>Updates for Oracle GraalVM 25 (LTS) continue to be released on the regular quarterly CPU schedule. For example, in 2026 there are four CPU releases:</p><ul><li>January 20th: 25.0.2</li><li>April 21st: 25.0.3</li><li>July 21st: 25.0.4</li><li>Oct 20th: 25.0.5</li></ul><h4>GraalVM <strong>feature releases</strong></h4><p>Monthly features releases for Oracle GraalVM and GraalVM Community Edition follow the same minor feature/security level numbering. Each quarter, a feature release will include the latest JDK Critical Patch Updates (CPU), which is reflected in version’s SECURITY digit. For example:</p><ul><li>June 2026 (Feature): 25.1.3</li><li>July 2026 (Feature + CPU): 25.2.4</li><li>Aug 2026 (Feature): 25.3.4</li><li>…</li></ul><p>See the Release Calendar for exact CPU days and planned versions: https://www.graalvm.org/release-calendar/</p><h3>What’s coming in monthly feature releases</h3><p>We will use the monthly 25.1+ feature line to deliver incremental capabilities across Native Image and language runtimes. The following highlights are in focus.</p><p><strong>Truffle — New Bytecode Interpreters</strong> are rolling out to GraalPy, GraalJS, and GraalWasm. These interpreters improve warmup performance and reduce memory footprint significantly before JIT compilation is performed. See <a href="https://github.com/oracle/graal/blob/master/truffle/docs/bytecode_dsl/BytecodeDSL.md">https://github.com/oracle/graal/blob/master/truffle/docs/bytecode_dsl/BytecodeDSL.md</a></p><p><strong>GraalPy (Python) </strong>— the major focus is on the bytecode interpreter and other optimizations to improve Python warmup as well as to reduce the cost of native method crossing. We continue to improve ecosystem compatibility and JVM interop (for example, support for using `java.util.List` as a Python list object). See <a href="https://www.graalvm.org/python/">https://www.graalvm.org/python/</a></p><p><strong>Native Image — Layers</strong> improves deployment by enabling layered native images with shared base layers that include the JDK, frameworks, and libraries. These layers can be mapped and reused to reduce memory usage and improve container efficiency. See <a href="https://github.com/oracle/graal/issues/7626">https://github.com/oracle/graal/issues/7626</a> for more information.</p><p><strong>Native Image — Crema</strong> enables an “Open World” model that supports dynamic class loading and execution at run time. It adds a bytecode interpreter and JIT compiler for classes loaded dynamically on top of an AOT compiled base layer. See <a href="https://github.com/oracle/graal/issues/11327">https://github.com/oracle/graal/issues/11327</a> for details.</p><p><strong>Native Image — Terminus</strong> makes the native-image tool self-hosting so the tool itself runs natively. It relies on a strong separation of host and guest heaps via Espresso and aims to deliver faster and more portable builds. See <a href="https://github.com/oracle/graal/issues/12236">https://github.com/oracle/graal/issues/12236</a> for more information.</p><p><strong>Native Image — Web Image</strong> targets WebAssembly to produce Wasm modules, plus a JavaScript wrapper, so JVM applications can run in browsers or in Node.js. See <a href="https://www.graalvm.org/latest/reference-manual/web-image/">https://www.graalvm.org/latest/reference-manual/web-image/</a> for details.</p><p><strong>GraalJS (JavaScript/Node)</strong> — modern ECMAScript support and first class embedding in polyglot scenarios. See <a href="https://www.graalvm.org/latest/reference-manual/js/">https://www.graalvm.org/latest/reference-manual/js/</a></p><p><strong>GraalWasm (WebAssembly) </strong>— execute and embed Wasm modules inside JVM applications for safe, high performance plugin architectures. See <a href="https://www.graalvm.org/webassembly/docs/">https://www.graalvm.org/webassembly/docs/</a></p><h3>Guidance for different users</h3><ul><li>Production applications should remain on the 25.0 LTS release train and upgrade to the latest CPU each quarter.</li><li>Applications on feature trains can adopt monthly feature releases on their own schedule to pick up new capabilities, but for maximum security should always upgrade to a release with the latest CPU.</li><li>Framework, library, and tooling authors should find monthly releases incremental and predictable. We will pre-announce noteworthy changes in EA and we welcome coordination during CPU weeks.</li><li>Early adopters are encouraged to try EA builds for upcoming monthlies to validate changes against their workloads and to provide feedback earlier.</li></ul><h3>Frequently asked questions</h3><ul><li><em>How do security fixes flow into monthly releases ?<br></em>For each release, the third version digit (i.e., the SECURITY digit) matches the JDK CPU level. After each CPU Tuesday, the next monthly feature release carries that CPU level, for example 25.2.4 in July 2026.</li><li><em>How long is each monthly 25.1+ feature release supported?<br></em>Each feature release is superseded by the next one. We do not backport fixes to older feature releases, so users should upgrade forward.</li><li><em>Will there be backports to earlier monthly releases?<br></em>No, users should upgrade forward to the latest monthly release to receive bug fixes and security patches. However, Oracle GraalVM 25.0 customers continue to receive support per policy on LTS lines.</li></ul><h3>All aboard!</h3><p>We all need to move faster, so join us on the new high-speed GraalVM release train!</p><p>Try Early Access builds and tell us what works and what does not. Adopt monthly feature releases to unlock improvements sooner. Stay current on CPUs for the latest security fixes.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=26b0d7cff2ab" width="1" height="1" alt=""><hr><p><a href="https://medium.com/graalvm/accelerating-the-graalvm-release-train-26b0d7cff2ab">Accelerating the GraalVM Release Train</a> was originally published in <a href="https://medium.com/graalvm">graalvm</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>