221 stories
·
1 follower

The Rest of the Story: August Edition - JVM Weekly vol. 189

1 Share

Two threads this month.

The first is languages shipping what the platform is still previewing. Groovy 6 put structured concurrency on Maven Central as a Java library while JEP 533 entered preview number seven. The Vector API entered incubator number twelve. Both in the same release.

Thanks for reading JVM Weekly! Subscribe for free to receive new posts and support my work.

The second I saw coming, because vol. 186 spent a section on five teams asking which part of the coding loop the AI doesn't get. August answered from a different angle: Databricks and JetBrains independently concluded that the semantic model of your code is the asset and the editor around it is a client. And Databricks has the numbers for that.

Plus a benchmark that did not survive contact with a profiler.

1. August: The Rest of the Story

Groovy 6 shipped structured concurrency as a Java library. Not a Groovy feature you can call from Java if you squint. A separate jar, groovy-concurrent-java, no Groovy runtime on the classpath, and this is the sample straight out of the release notes:

import groovy.concurrent.AsyncScope;
import org.apache.groovy.runtime.async.AsyncSupport;

var result = AsyncScope.withScope(scope -> {
    var a = scope.async(() -> fetchUser(id));
    var b = scope.async(() -> fetchOrders(id));
    return Map.of(
        "user",   AsyncSupport.await(a),
        "orders", AsyncSupport.await(b)
    );
});

That is StructuredTaskScope with the serial numbers filed off. Which arrives in JDK 27 as JEP 533, seventh preview, after two incubator rounds before that. Nine releases of refinement. Groovy needed one.

JEP 533, delivered in JDK 27 as the seventh preview of an API that started incubating in JDK 19.
JEP 533, delivered in JDK 27 as the seventh preview of an API that started incubating in JDK 19.

The jar also carries Actor, Agent, DataflowVariable, AsyncChannel, BroadcastChannel and ChannelSelect against plain java.util.function types, so Go-style channels and actors arrive in the same download. It is mutually exclusive with the full Groovy runtime (Gradle enforces that through a shared capability) and warns you at runtime if both jars show up.

Caveats, because I like you: 6.0 is still in beta, the whole toolkit is marked incubating, and the release notes open with a warning that the material on the page is under development. Nobody should put @ActiveObject in production this quarter. But it is a fair question why the platform needs nine rounds for an API a volunteer project shipped in one.

On the Groovy side you get async/await on automatic virtual threads (JDK 21+, cached pool on 17 to 20), generators via yield return, for await over channels and reactive streams, and defer for cleanup. GPars got dragged into groovy.concurrent and rebuilt around virtual threads, which is a nicer fate than most 2010-era concurrency libraries got.

The other half of Groovy 6 is a release-notes section titled "Designed for Human and AI Reasoning", written up by Paul King on the Groovy blog, and I have a book coming out on adjacent material, so discount accordingly.

The mechanism is design by contract. @Requires and @Ensures for pre- and postconditions, @Invariant and @Decreases for loop invariants and termination measures, @Modifies naming which fields a method may touch, @Pure for none of them. Then ModifiesChecker and PurityChecker verify the claims at compile time.

The verification is the point. An unverified annotation is a comment, and a model has no more reason to trust a comment than you do. The release notes put a cost model on it: without annotations, checking whether three calls left balance alone means reading every body and every callee, growing as O(fields × calls × call_depth). Which is roughly where every assistant starts saying it would need to see more context.

The Groovy 6 release notes, costing out what a reader has to verify by hand when the annotations are missing.
The Groovy 6 release notes, costing out what a reader has to verify by hand when the annotations are missing.

Shipping alongside: a NullChecker with a flow-sensitive strict mode that works on unannotated code and reads JSpecify-style @NullMarked declarations out of compiled jars, right down to package-info.class; a SqlInjectionChecker that flags a GString interpolated inside quotes (the pattern that silently defeats groovy-sql's parameter binding); and a CombinerChecker that verifies the function you handed injectParallel is associative. That last one catches a bug class that compiles, runs, and returns different answers depending on how the work got partitioned. Good luck finding it in a test suite.

Every JVM language is going to have the contracts argument. Groovy went first, which nobody had on their card.


Amber published a guide on switching over sealed types, and it corrects something I believed until three weeks ago.

The story we all told after JDK 21: seal the hierarchy, switch without a default, let the compiler flag you when someone adds a subtype. ADTs in Java at last.

Preparing for Change: Safe Switching over Sealed APIs, by Angelos Bimpoudis, Alex Buckley and Brian Goetz, points out the guarantee is compile-time only. Your switch covers the permitted subtypes the compiler saw when your code compiled. Library author adds a subclass, you don't recompile, and you get a MatchException at runtime with no arm to match and no default to fall into. Binary compatibility and exhaustiveness pull against each other and the language picked "fail loudly".

So some of this lands on API authors. Publish a sealed hierarchy and you owe clients a note on whether you intend to add to it, because that is what decides between an exhaustive switch and a defensive one. Adding a case to a public sealed interface is closer to adding an abstract method than the syntax lets on. The guide's own line is worth quoting: it is almost never necessary to use default when switching over a sealed type, and often not the best option.


JDK 28 has a shape. JEP 401 Value Objects and JEP 539 Strict Field Initialization are already integrated. JEP 535 Shenandoah Generational by Default, JEP 540 Simple JSON API and JEP 541 Deprecate macOS/x64 are targeted, with JEP 542 PEM Encodings Proposed to Target. Michael Redlich has the full rundown, drafts included.

Two of these already had their edition: Value Objects in vol. 180, Simple JSON in vol. 188. Skipping both.

JEP 541 deserves a re-read, because "Java drops Intel Macs" undersells it. The port stays in the tree. The build refuses to configure for it:

$ bash ./configure
...
checking compilation type... native
configure: error: The macOS/x64 port is deprecated and may be removed in a future release. Use --enable-deprecated-ports to suppress this error.
configure exiting with result code 1

--enable-deprecated-ports downgrades that to a warning and makes no promise the result builds, let alone runs. The line further down the JEP matters more: Oracle engineers stop maintaining macOS/x64 as of JDK 27, one release before the deprecation is formal. Maintenance already ended; JEP 541 is the paperwork. Apple laid the runway anyway, with macOS 27 dropping Intel and taking Rosetta 2 with it. Go check whether anyone still has a 2019 Mac Pro doing builds in a cupboard.

JEP 539 is quieter and more consequential. Strict field initialization reads as a language nicety but it is a JVM change: strict fields must be written before anything reads them, so 0 and null are never observable mid-construction. That is Valhalla plumbing, because an identity-free value object cannot afford a half-built window, and it is aimed at compilers emitting class files rather than at you.

JDK 27 goes GA on 15 September with nine JEPs, feature freeze covered in vol. 179.

One of those nine is JEP 537, the Vector API's twelfth incubator, with no substantial implementation changes since JDK 25, and it keeps incubating until Valhalla lands in preview. Twelve rounds. In the same release as preview seven of Structured Concurrency. Soonil N.'s hashtable benchmark in vol. 186 already complained that C++ runs on Swiss tables and Java doesn't, partly for this reason, and the situation has since improved by zero.

JEP 537: twelfth incubator, effort XS, no substantial implementation changes since JDK 25.
JEP 537: twelfth incubator, effort XS, no substantial implementation changes since JDK 25.

Databricks open-sourced Metals v2, and the justification is better than the software (which is saying something). Metals is the official Scala language server, and its maintainers sit at VirtusLab (hello there!). V2 was built inside Databricks by Ólafur Páll Geirsson, Iulian Dragos, Alessandro Patti and team, and it started as a fork. It came back to the upstream scalameta/metals repository at open-sourcing, under Apache 2.0.

For anyone who has only ever used IntelliJ: a language server is the process behind "go to definition" in every other editor. The classic design has it talk to your build tool over BSP to learn the project layout, which means a blocking sync before anything works, minutes on a large monorepo, and a very expensive text editor when that sync breaks.

Databricks' argument: this was fine when humans typed the code and is not fine now, because most of the code at Databricks is now written by agents, and an agent does not wait for a build sync. So v2 indexes sources directly and skips the build for navigation. Their Bazel monorepo is 26 million lines, 24 of them Scala. The index covers 2.9 million symbols across 142,000 files and weighs 936 MB on disk. Time to initial intelligence, meaning the point where fuzzy symbol search, jump-to-definition and find-usages all work, runs at a p50 of 8.7 seconds and a p90 of 36.7 seconds. Fuzzy search across those 2.9 million symbols has a p50 of 10 ms. BSP becomes optional rather than load-bearing.

Then the numbers that make it more than an architecture post. Databricks standardised on Cursor in May 2025; by July 2026, 92% of weekly active IDE users open Cursor against 12% for IntelliJ, and the company did not renew the majority of its IntelliJ seats this year. Cursor's share of Scala and Java file-open events went from 40% to 78% since the first v2 improvements landed in October 2025. In a shop where IntelliJ had been the only editor that could keep up with the monorepo.

v2 also has first-class Java support, which is why large Java shops are piloting Metals for Java alone, a sentence I did not expect to type. Stripe says in the same post that it started rolling it out a month ago. Set expectations: no extract-method, no inline-variable, import suggestions only. Navigation and diagnostics, not an IntelliJ replacement. Cursor, VS Code and Neovim today, with a slide deck for the internals.

The more interesting part is what happens next, which is easy to miss under all those numbers. The stable v2, the one that supersedes today's v1, is being prepared by Databricks with the Cursor team and the Metals maintainers at VirtusLab, and ongoing development is led by VirtusLab, which is also where the issues go. Databricks built this in house and handed ongoing development to VirtusLab. Metals itself is developed at the Scala Center and VirtusLab.

Krzysztof Romanowski, Head of Development Productivity at VirtusLab, in the same post: "AI is changing how developers use IDEs, and Metals v2 moves in the right direction: fast startup, reliable codebase orientation, and an architecture built for large codebases. Databricks validated the approach at exceptional scale, and VirtusLab is excited to help bring this work to the broader Scala and JVM community."

I picked wisely, we are so proud of Metals!
I picked wisely, we are so proud of Metals!

JetBrains is taking IntelliJ apart from the other end. After 2026.2 shipped agent skills and native Copilot support in July, August added two more: the core modularised to speak LSP, written up by Marco Behler, so the Java and Kotlin analysis engines detach from the Swing frontend and can serve VS Code, Cursor and headless agents, and ACP support from Anton Arhipov, a JSON protocol that decouples agent logic from IDE internals so you register Claude, Codex or something in-house in an acp.json.

The detail I'd flag is what Agent Skills, covered by Siva Katamreddy, reach: the Program Structure Interface, IntelliJ's semantic model, plus the project index and terminal. Ask a RAG-backed agent what calls a method and it runs a similarity search and hopes. Ask a PSI-backed one and it queries the resolver behind Alt+F7.

In vol. 181 I called JetBrains and Microsoft opposite bets on the agent harness. Vol. 186 revised that when Copilot support shipped first-party. August is moves three and four in the same direction, and Databricks got to the same place from a monorepo problem rather than a strategy deck.

Which leaves a question neither blog post touches: if the intelligence engine is the product and the editor is a client, what is JetBrains selling in 2029?


Two shorter ones from the same neighbourhood.

Engram wired into IBM Bob over MCP, by Markus Eisele, is a decent worked example of durable project memory as a local knowledge graph rather than a markdown file that grows until nobody reads it. A project rule tells the agent to consult it before non-trivial decisions and to record only decisions, constraints and failed approaches. Bob 2.0 exposes no harness hooks, so project rules are the only lever, which is also why the Claude Code session-start integration has no equivalent here. Check which Engram you are installing, incidentally, because there are at least five unrelated projects using the name and the JVM ecosystem apparently ran out of words for memory.

Agentic fitness functions on InfoQ, by Hemant Kumar Mahato, Łukasz Sieczkowski and Vijayasenthilkumar Kuppusamy, I expected to hate and mostly didn't. A fitness function, per Neal Ford, is an automated check guarding an architectural characteristic; on the JVM that means ArchUnit asserting ..domain.. never touches ..infrastructure... The piece argues for LLM agents with versioned rubrics covering what ArchUnit structurally can't say: boundary fidelity, semantic contract drift, workflow coupling, stale ADR assumptions.

It survives because the authors say the quiet part first. Deterministic fitness functions stay the primary enforcement mechanism for anything measurable, and the agentic ones earn their keep only when a finding recurs, the team agrees it is a real violation, and somebody writes it down as a rubric criterion or a deterministic rule. So it is a detector for rules you haven't written. Non-deterministic CI gates I would still argue about, loudly, with anyone who has debugged a flaky pipeline at 2am.

Same shape as Groovy's contracts, arriving through governance instead of the type system. Groovy has the compiler check the claim. InfoQ has a model check the claim.


Apache Fory JSON published big numbers, then somebody profiled them.

(Spelling note: Fury renamed to Fory. I got this wrong in a draft earlier this month, so consider this atonement.)

Fory's own figures: up to 10.91x Jackson's throughput on 1000 KB payloads, and up to 5.55x on the jvm-serializers MediaContent model, that second run on an M4 Pro under JDK 26.0.1. The mechanism is real, generating schema-specific serializers as bytecode at runtime instead of walking your object graph reflectively.

Then ej-technologies, the JProfiler people, profiled five frameworks to find where Jackson's time goes. Base64-encoding the byte[] thumbnails: 24% of the run. Decoding them: another 23%. Reflective invocation: 13%. So roughly half the time went to JSON not having a binary type, which is a property of the format rather than the library.

Five frameworks, one round trip: the format costs more than the library does.
Five frameworks, one round trip: the format costs more than the library does.

Switch Jackson to CBORMapper, one line, and it is 2.4x faster with payloads nearly as compact as Fory's. At which point the profile flips, data binding becomes about 40%, and the remaining gap is roughly 3.4x. Real, and a different sentence than "10x to 50x".

Order of operations, then. If humans or other tools read your payloads, Jackson and JSON stay the default. If you own both ends and serialization shows up in a profile, change the backend before you change the library. And most REST services are I/O-bound, so check that serialization is your bottleneck before you spend an afternoon on any of this.


A quick lap around the mailing lists.

On panama-dev, Liam Miller-Cushon opened a thread on 22 August about string processing and the Vector API, and it runs into the same wall as every attempt before it. Scalar charAt loops bottleneck parsers and regex scanning, but getting a String into a MemorySegment for SIMD work means Arena#allocateFrom(String) and a copy that eats the win. The obvious MemorySegment#ofString hits Compact Strings: a String is Latin-1 or UTF-16 internally depending on content, so exposing the array leaks that, and normalising recreates the copy. JNI's GetStringCritical never solved this either. See also: incubator twelve.

On amber-dev, someone floated a mixin construct for interface-scoped forwarding, roughly mixin MapWrapper(HashMap delegate) implements Map, with the compiler generating the forwarding methods. Composition over inheritance mostly loses because delegation is boring to type, and scoping forwarding to a named interface dodges the blanket-delegation objection Brian Goetz raised in the thread itself, that a delegate's optimized implementations go missing. Mailing-list idea, not a draft JEP. If it ever lands, Lombok loses another reason to exist.


Two to close. JetBrains Research has a migration finding worth an argument, in Vladimir Volokhonsky's write-up of the 2025 State of Developer Ecosystem survey: Kotlin is a "pull" migration. C++ to Rust is usually driven by memory-safety requirements, JavaScript to TypeScript by a mandate, Java to Kotlin by developers preferring it. Read charitably that is a compliment to the ergonomics. Read uncharitably, a migration that nothing forces is a migration that can stop half-finished, and you keep two languages in one repo for years. The report doesn't distinguish, and I have seen both.

Filed quietly next to it: Kotlin 2.4 started an 18-month security support window for the JVM standard library per release line, 2.4 running to 3 December 2027, with security fixes backported to every line still inside its window. The policy post went up in May and nobody noticed. More projects should publish one of these.

And Charles Nutter on Josh Long's Bootiful Podcast on JRuby riding the JDK roadmap: no GIL, so Ruby threads map to real JVM threads; invokedynamic still the performance floor; Loom raising the concurrency ceiling; Panama's FFM API as the modern answer to C extensions, historically the reason people couldn't leave MRI; and Valhalla eventually attacking Ruby's allocation costs. Guest languages remain the honest test of whether the big OpenJDK projects deliver, and JRuby has been sitting that exam since 2001.

2. Release Radar

Gradle 9.7.0

Gradle 9.7.0 landed on 6 August with Isolated Projects graduating from experimental to incubating. The feature configures projects in parallel, which only works if projects stop reaching into each other, so allprojects, subprojects and project(":other").someProperty all become violations and shared config moves into convention plugins.

The published numbers carry the argument, and Alex Semin laid them out in the announcement post. Gradle's own 300-subproject build: median IDE sync 84s to 47s. A 2,500-project pure-Java monorepo: configuration with build-script recompilation 10m53s to 2m59s. A 5,000-project Android monorepo: total Android Studio sync 5m09s to 2m44s.

Gradle's early adopters, with the parallelism each number was measured at.
Gradle's early adopters, with the parallelism each number was measured at.

Three things before you flip the flag on a Friday. It is off by default and not recommended for production. Configuration Cache compatibility is a hard prerequisite, and starting without it means debugging two configuration models at once. And each of those numbers came at a specific parallelism, from 6 to 96, so the speedup you get is the one your machine can pay for.

The property finally lost its shame prefix, org.gradle.unsafe.isolated-projects becoming org.gradle.isolated-projects with a --isolated-projects CLI option. There is also a diagnostics mode that reports violations without failing the build, which is how you should start. Elsewhere: ResolutionResult works as a task input under Configuration Cache, Java agents and TestKit stopped fighting, and a spurious cache invalidation triggered by an IntelliJ system property is gone. Embedded Kotlin moves to 2.4.0 and language version 1.9 is rejected. 9.7.1 followed with Resilient Sync, returning a partial model when the build fails on IDE import so you keep completion for the healthy parts instead of an all-red screen.

Release Notes

Quarkus 3.38

Quarkus 3.38 went out on 29 July, it is thin, and Guillaume Smet says why in the second paragraph: development effort has moved to Quarkus 4. That is the entry. After 3.37 shipped quarkus-jlink and reflection-free Jackson serializers by default (vol. 186), the 3.x line is winding down.

What did land: weight-based eviction for Hibernate second-level Caffeine cache regions. Previously only count-based, so you could cap entries per region but not the memory they consume, which is the wrong knob when entry sizes vary by an order of magnitude.

Contributor count is 1213, up from 1203 last month. One of the few honest health signals a framework publishes.

Announcement

Jakarta Agentic AI 1.0.0-M1

First milestone of the Draft 1.0 spec, on Maven Central, written up by Otavio Santana on Foojay. Vendor-neutral abstractions for model orchestration, tool calling and memory persistence, so agentic code stops being welded to one provider's SDK. The specification page is blunt about the status: non-final, draft, no release review, published for early feedback.

The open question is whether anyone wants a Jakarta spec here, or whether LangChain4j plus MCP became the de facto standard while the committee drafted. Jakarta's argument is the one it always makes and it isn't wrong: specs outlive vendors, and the agentic layer is the least stable part of everyone's stack right now. Milestone one is the moment to have that fight, not 1.0.

Ivar Grimstad, Jakarta EE Developer Advocate at the Eclipse Foundation, reports the Jakarta EE 12 Core Profile on track for JakartaOne Livestream 2026. Web Profile and Platform slip to the first half of 2027. Read against vol. 184, where an agent reimplemented Jakarta EE from the spec. Jakarta specifying agents while agents reimplement Jakarta is a loop nobody drew on the roadmap.

Specification

Apache Tika 4.0.0

Apache Tika 4.0.0 landed on 18 August. The interesting part has nothing to do with file formats.

Tika is the library that detects and extracts text and metadata from over a thousand file types, and it is what sits under a great many ingestion pipelines without anyone thinking about it. In 4.0 the default content handler is Markdown. tika-app, the server's /tika and /rmeta endpoints and the async CLI all emit Markdown instead of XHTML now; you ask for the old behaviour with -x, /tika/xml or --handler x. A text-extraction library changed its default output format because the thing consuming the output stopped being a search index and started being a model.

The new tika-inference and tika-vlm modules go further: vision-language-model parsers backed by Claude, Gemini and OpenAI for documents OCR can't read, emitting vlm:prompt-tokens and vlm:completion-tokens as document metadata. Token counts are a Tika metadata field now. tika-parser-tess4j-module adds in-process Tesseract alongside them.

The breaking changes are real, so read them before you bump the version. Java 17 minimum, where 3.x ran on 11. ForkParser and the whole org.apache.tika.fork package are gone from tika-core, with pipes taking over out-of-process parsing. A JSON config now beats a programmatic context.set(...), which inverts the old precedence. tika-app and tika-server-standard ship as zip distributions, so running the published jar on its own gets you a NoClassDefFoundError. The 3.x line continues at 3.3.2.

Changes

3. GitHub All-Stars

Five this month, and not one of them is a model. They are all wiring, which is where August's interesting work turned out to be.

native-langchain4j, the AOT metadata nobody wants to derive twice

native-langchain4j by Alina Yurenko is small, 29 commits and change, and does one useful thing: LangChain4j 1.19.0 plus OpenAI plus GraalVM Native Image with the reachability metadata already worked out. That version is three weeks old, shipped on 14 August with an MCP client tracking the 2026-07-28 revision of the protocol, so the demo is pinned to current rather than to whatever built cleanly last year. Agentic API, streamed responses, per-session chat memory, strict tool calling, one read-only local tool.

Why that is the whole value: Native Image compiles by reachability analysis, walking your code at build time and compiling only what it can see. Orchestration frameworks run on reflection and dynamic proxies, which are invisible to that walk, so classes exist at build time and vanish at runtime. Fixing it means declaring metadata by hand, and the build-fail-add-an-entry-build loop is where most people quit and go back to a JAR.

The README is careful about what it is not doing: the build uses the Native Build Tools Maven plugin and the application's own minimal metadata, with no fallback image and no deprecated builder-class options. That last bit matters, because a fallback image is Native Image quietly shipping a JVM inside your binary and calling it a day. Put a working example like this next to the 39% RSS reduction GraalVM measured from compressed references in 25.2, and the argument that Java is too heavy for serverless is running out of road.

ai-peer-live-coding-agent, constrain the interview, not the tools

ai-peer-live-coding-agent takes an angle on AI-assisted interviews I hadn't seen made concrete. Rather than banning the tools or letting a candidate generate a solution wholesale, it constrains the environment: .cursor/rules/ enforces a Controller-Service-Store structure with standard validation and error handling, a mandatory 10-minute spec-before-code phase, seven discrete testable tasks capped at eight, Docker Compose for a production-like setup, all inside a two-hour session. The rules file has a line I liked: copilot, not autopilot.

What it tests is whether someone can hold architectural intent while a model writes the code, which is closer to the actual job than whiteboard tree traversal has been for a decade. Whether I'd run hiring this way, I genuinely don't know. But it is an artefact in an argument otherwise conducted entirely in LinkedIn posts.

Same .cursor/rules/ and SKILL.md pattern that turned up all over vol. 186, and the same distribution problem James Ward's SkillsJars was trying to solve. These rules live in one repository and go nowhere.

spring-boot-skills, the rules somebody actually packaged

spring-boot-skills by Rrezart Prebreza is that complaint answered, for the framework most of you actually ship. Thirty-odd skills in two version trees, spring-boot-3 and spring-boot-4, MIT licensed, so you copy a folder into .claude/skills/ or adapt the same SKILL.md layout for Codex.

The README opens with the best one-line statement of the problem I have read this year: AI coding agents are great at Python, and they hallucinate in Spring Boot. Then it gets specific, and the list will be familiar. Field injection with @Autowired where you use constructor injection. A brand new exception hierarchy next to the one you already have. Schema SQL written out by hand because nothing told the model you use Flyway. Pre-GA Spring AI artifact names that no longer resolve on Maven Central.

The last one is not a style disagreement. It is a model emitting Maven coordinates that were real during training and are not real now, and prompting harder will not fix it.

So the repo dates itself: fast-moving integrations last verified in August 2026 against Boot 4.1, Spring AI 2.0 and MCP Java SDK 2.0. That date is also the bill, because skills rot the way documentation rots.

mini-claude-code, the agent harness with the lid off

mini-claude-code by DerekYRC is a stripped-down Java implementation of a coding agent, built to be read rather than run. Sixteen branches, s01-agent-loop through s16-mcp-plugin, each holding the smallest amount of code that makes one mechanism work: tool dispatch, permission control, hooks, todo, subagents, skill loading, context compaction, memory, background tasks, cron scheduling, multi-agent teams.

The same author did this before with mini-spring and mini-netty, and it is a genre I wish more of the ecosystem practised. If you want to know what context compaction actually does, s08 is a few hundred lines of Java instead of a blog post with a diagram.

The primary README is in Chinese, with an English one next to it, which is nobody's problem. The missing LICENCE file is. No licence means all rights reserved: read it, learn from it, and copy nothing out of it into anything you ship. Worth opening an issue about, when the entire point of the repository is that people read it.

tda, a Swing app from 2016 that grew an MCP server

TDA by Ingo Rockel is the Thread Dump Analyzer, a Swing desktop tool that reads Java thread dumps and heap information, and supports Java 1.4.x through 21+, which tells you how long it has been doing it. In 3.0, back in January, it grew a Model Context Protocol server, with 3.1 and 3.2 following in April and May.

You start it with java -Djava.awt.headless=true -jar tda.jar --mcp and it exposes analysis as tools rather than handing a model a wall of stack traces. analyze_virtual_threads finds virtual threads whose carrier is stuck in application code. get_zombie_threads returns threads with unresolved SMR addresses. That is the pinning problem from this issue's PS, answered by something that already knows what pinning looks like, rather than by a model pattern-matching on text.

The operational notes are the part I enjoyed most. MCP speaks JSON-RPC over stdin and stdout, so the documentation warns that anything else printing to stdout corrupts the stream and drops the client. And a Swing application has to be told to skip its own UI initialisation before it can serve at all, which is a sentence nobody expected to write about a desktop tool.


PS: JDK 27 goes GA on 15 September, so you have three weeks to find edge cases while fixes are still cheap. If anything of yours runs on virtual threads, the JDK-8377715 regression from vol. 186 is still worth a look. Meanwhile the fourth digit from vol. 188 is out in the wild: BellSoft shipped Liberica builds on 19 August across 26.0.2.1, 25.0.4.1, 21.0.12.1, 17.0.20.1, 11.0.32.1, 8u504, 7u513 and 6u513, 29 fixes and backports in total. If your base image pins a three-part version, it is now pinning to something that no longer gets the newest patches.

Thanks for reading JVM Weekly!

Thanks for reading JVM Weekly! Subscribe for free to receive new posts and support my work.

Read the whole story
jhunorss
16 hours ago
reply
Share this story
Delete

Comcast adds motion sensing to millions of its newer routers, with a privacy catch

1 Comment
A new feature added to Comcast's newest routers can detect if there is motion inside your home without needing traditional motion sensors.
Read the whole story
jhunorss
18 hours ago
reply
🙈
Share this story
Delete

Abliteration.ai is making a business out of removing AI guardrails

1 Share
Abliteration.AI is making powerful AI models without guardrails easier to access, arguing that giving defenders the same tools as bad actors could ultimately improve cybersecurity.
Read the whole story
jhunorss
4 days ago
reply
Share this story
Delete

Yunohost stands in solidarity with Autistici/Inventati

1 Share

The YunoHost project decided to sign the open letter published by Sabot.Media and We Will Free Us about the attack from the United States government against Autistici/Inventati, an italian hoster.

YunoHost stands in solidarity with Autistici/Inventati and other hosters. We hope that the United States government will reconsider its position and that decision-makers around the world will fully grasp the implications of this dangerous trend.

The said letter is reproduced below:

Defend Autistici/Inventati and the right to build resistant communication

On August 26, 2026, the United States designated the Italian volunteer technology collective Autistici/Inventati as a Specially Designated Global Terrorist and placed it on the Specially Designated Nationals list under Executive Order 13224. A temporary authorization covering certain wind-down transactions ends at 12:01 a.m. Eastern on September 25.

Autistici/Inventati has provided noncommercial email, websites, mailing lists, Noblogs and other communications services since 2001. It emerged from Italian hacklabs, autonomous media and social movements. Its infrastructure was built in response to censorship, covert surveillance and server seizures. It minimizes identifying data, uses distributed systems and treats privacy as a condition of political participation rather than a product.

The United States’ public announcements point to material and organizations that allegedly used A/I infrastructure. They do not publicly show that A/I planned the cited actions, selected targets, directed users or authored hosted material. The designation therefore raises a question extending far beyond one collective: can maintaining privacy-preserving communications infrastructure for disfavored movements itself be treated as terrorism?

That theory threatens independent hosts, encrypted communications services, radical libraries, movement archives, publishers and small volunteer projects everywhere. It invites banks, registrars, hosting companies and technology providers to sever relationships first and ask legal questions later. It turns data minimization into suspicion, privacy into concealment and infrastructure into guilt by association.

Large commercial platforms are ordinarily distinguished from the speech and conduct of their users through intermediary-liability principles, including Section 230 in the United States. Those protections are not absolute and do not override sanctions law. That is precisely why this designation is so consequential: executive sanctions power can impose economic isolation without resolving the ordinary question of whether a provider is legally responsible for third-party material in court.

Autistici/Inventati is not politically neutral, and neither are many of the publications, libraries, unions and civil-liberties organizations that may sign this letter. Political affinity, however, is not operational control. Providing an email account, publishing platform or server is not the same act as planning everything a user may later say or do.

We therefore call for:

The immediate revocation or prompt, transparent review of the designation, with publication of the factual and legal basis sufficient for meaningful public scrutiny and challenge.

A clear distinction between providing communications infrastructure and directing the conduct of its users.

Privacy, encryption, pseudonymity and refusal to retain unnecessary identifying data must not be treated as evidence of terrorism.

Protection for independent reporting, research, archiving and public advocacy.

Journalists, libraries, researchers and community archivists must be able to document this history without being treated as extensions of a designated entity.

Resistance to institutional over-compliance.

Banks, registrars, hosts, certificate providers and technology companies should disclose relevant restrictions, provide notice where lawful and avoid suppressing material or relationships beyond what the law actually requires.

A public response from digital-rights, free-expression and civil-liberties institutions.

The targeting of autonomous infrastructure cannot be allowed to become a precedent through silence.

We do not ask every signer to endorse every A/I user, publication or political position. We ask signers to defend a foundational distinction: communications infrastructure is not identical to the speech or conduct transmitted through it.

The purpose of economic designation is isolation. Our answer is independent, informed and disciplined solidarity: preserve the public record, examine the government’s claims, resist over-compliance and defend the ability of movements to communicate without surrendering themselves to corporate or state surveillance.

The list of signatories on sabot.media

2 posts - 2 participants

Read full topic

Read the whole story
jhunorss
4 days ago
reply
Share this story
Delete

Project Loom in IntelliJ IDEA: Virtual Threads, Scoped Values, and Structured Concurrency

1 Share

Java concurrency is a powerful feature, but it can be difficult to get right. Writing correct multithreaded code requires a deep understanding of thread pools, synchronization, cancellation, and error propagation. Even experienced developers regularly introduce subtle bugs like thread leaks, swallowed exceptions, and race conditions that only surface under specific circumstances.

Traditionally, Java concurrency has had several limitations: 

  • Scalability and resource costs from blocking threads.
  • Difficulty sharing contextual data between threads safely.
  • Problems managing threads such as leaks and cancellation delays.
  • Concurrent code that is hard to understand, debug, and maintain.

Project Loom aims to eliminate the tradeoff between simplicity and efficiency in concurrent Java code, making it easier to write, debug, profile, and maintain code that is correct, readable, and scalable. It does this through three features that work together:

  • Virtual Threads (JEP 444, stable since Java 21) – Platform threads are expensive and limited in number, making highly concurrent applications resource-heavy and hard to scale. Virtual threads are lightweight and managed by the JVM, allowing many more threads to run concurrently without the same overhead.
  • Scoped Values (JEP 506, stable since Java 25) – ThreadLocal variables are mutable, hard to reason about, and prone to memory leaks. Scoped values provide immutable, automatically cleaned-up data sharing that scales efficiently with virtual threads.
  • Structured Concurrency (JEP 533, seventh preview in Java 27) – Unstructured concurrency leads to thread leaks, cancellation delays, and code that is difficult to debug and maintain. Structured concurrency treats a group of related threads as a single unit of work, making cancellation and error handling predictable and consistent. It also provides a clear parent-child thread hierarchy that improves observability and makes concurrent code easier to trace and inspect.

In this post, we’ll give you an overview of these features, explain some of the problems they solve, show how they work together, and demonstrate how IntelliJ IDEA supports you along the way.

Problems with Java concurrency before Project Loom

To illustrate some of the problems with concurrency and how Project Loom can solve them, let’s look at an example of how we could write concurrent code without using any of the features of Project Loom. We’ll then rewrite this code to take advantage of these features and see how they compare.

As an example, we will use an application that loads a customer profile. It fetches order history and product recommendations for a customer in parallel. You can find the project’s source code here.

The method getProfile() in the CustomerProfileService (which you can find here) loads the customer profile using CompletableFuture to run the calls concurrently:

public CustomerProfile getProfile(String customerId) throws OrderServiceException, RecommendationServiceException {
        CompletableFuture<List<Order>> orderFuture =
                CompletableFuture.supplyAsync(() -> orderServiceClient.getOrders(customerId), executor);
        CompletableFuture<List<Recommendation>> recFuture =
                CompletableFuture.supplyAsync(() -> recommendationServiceClient.getRecommendations(customerId), executor);

        CompletableFuture<Void> allFutures = CompletableFuture.allOf(orderFuture, recFuture);

        try {
            allFutures.get(2, TimeUnit.SECONDS); // single timeout covering both futures
            return new CustomerProfile(customerId, orderFuture.join(), recFuture.join());
        }

        // catch block 

The catch block handles any exceptions. This code does what it is supposed to do: It runs the independent calls in parallel, has a timeout, and makes an effort to cancel remaining tasks on failure. But there are still several potential problems:

  • Thread leaks. cancel(true) marks the future as cancelled, but for CompletableFuture the interrupt flag is ignored and the work already running in the pool continues to completion unless it was explicitly wired to a cancellation signal.
  • Awkward error handling. ExecutionException wraps the real cause and must be unwrapped manually (shown in the code snippet below and also available here). The unwrapping chain will need to be updated every time a new exception type is introduced.
        catch (ExecutionException e) {
            Throwable cause = e.getCause();
            orderFuture.cancel(true);
            recFuture.cancel(true);

            if (cause instanceof RestClientResponseException ex && ex.getStatusCode().value() == 503) {
                if (orderFuture.isCompletedExceptionally()) {
                    throw new OrderServiceException("Order service unavailable", e.getCause());
                }
                if (recFuture.isCompletedExceptionally()) {
                    throw new RecommendationServiceException("Recommendation service unavailable", e.getCause());
                }
            }
            throw new RuntimeException("Unexpected error", e.getCause());
  • Duplicated cancellation logic. The cancel() calls are repeated across both the TimeoutException and ExecutionException catch blocks. If any additional parallel call is added later, it needs a cancel() call in both catch blocks, which is easy to forget. These blocks could drift out of sync as the code evolves.
  • Fragile context propagation. Passing contextual data such as a logged-in user’s session or a trace ID across threads using ThreadLocal is fragile. ThreadLocal variables are mutable, their values persist for the lifetime of a thread unless explicitly removed, and child threads do not automatically inherit them unless you use InheritableThreadLocal, which has its own pitfalls.
  • Poor observability. A thread dump shows a flat list of pool threads with no indication of which threads belong to which request, or which are still waiting on something that already failed. To see running threads, you can get a thread dump in IntelliJ IDEA when the program is suspended (either stopped at a breakpoint or paused). In the Debug tool window, click More and select Get Thread Dump while the service is handling a request.
Pause output and Get Thread Dump

Sidenote: To add the Get Thread Dump button to your Debugger tool window, right-click the Debugger tool window and select Customize Toolbar. In the popup, click Add, search for and select Get Thread Dump, and click OK.

Customize Toolbar with the Get Thread Dump button

Let’s take a look at how Project Loom addresses these problems.

Virtual Threads (JEP 444, stable since Java 21)

The first feature of Project Loom is Virtual Threads, which drastically improve throughput in Java applications with blocking code.  

Traditionally, the number of available threads in a Java application is limited because platform threads wrap operating system (OS) threads, and the number of OS threads is limited. Platform threads are also expensive; creating them can take milliseconds, each one consumes significant memory, and context switching between them has considerable overhead. To manage these costs, applications use thread pools – a fixed set of reusable threads managed by an ExecutorService.

In contrast, virtual threads are lightweight threads. They are cheap to create (taking microseconds instead of milliseconds), and since they are not tied to OS threads, they are not limited in number; you could run millions of them. When a virtual thread blocks, the underlying platform thread is released for other work and reassigned when the virtual thread is ready to continue. This means that virtual threads can significantly improve throughput for blocking workloads, such as I/O (anything that waits on databases, network calls, or file access), pauses, or synchronization.

In Java 24, an additional improvement was made to improve the scalability of Java code. With JEP 491: Synchronize Virtual Threads without Pinning, virtual threads that block in synchronized methods and statements release their underlying platform threads. You can see this in action in the What’s New in IntelliJ IDEA 2025.2 livestream.

We already briefly discussed virtual threads in Java 25 LTS and IntelliJ IDEA. For more information about using virtual thread dumps, have a look at Thread Dumps and Project Loom (Virtual Threads).

To debug problems with concurrent threads, check out the new, improved logpoint functionality described in Println Debugging Done Right.

Scoped Values (JEP 506, stable since Java 25)

The second feature of Project Loom is Scoped Values – a safer, more scalable alternative to ThreadLocal variables, designed with virtual threads in mind. They solve the problem of sharing contextual data across threads cleanly and safely.

To share data between components of an application, we can use thread-local variables, but these have several downsides. A ThreadLocal variable is mutable and, therefore, hard to reason about. Data persists for the thread’s lifetime unless manually removed (risking memory leaks and security issues), and child threads inherit copies that increase memory footprint.

ScopedValue provides a better model: A value is bound once within a defined scope, automatically available to all code running within that scope, and cleaned up automatically when the scope ends. The binding cannot be changed from within the scope, eliminating the risk of accidental mutation and guaranteeing that any code reading the value will see the same one. When used with structured concurrency, scoped values require no explicit propagation to child threads, making context sharing both safer and simpler.

Note that even if your code does not explicitly use ThreadLocal, frameworks like Spring use it under the hood.

For more details, see the section on Scoped Values in Java 25 LTS and IntelliJ IDEA.

Structured Concurrency (JEP 533, seventh preview in Java 27)

The third feature of Project Loom is Structured Concurrency, which is currently still in preview. Java 27 again brings some changes to this preview feature. As we have already added some support for this feature in IntelliJ IDEA, now is the perfect time to try it out.

Structured concurrency is designed to promote a style of concurrent programming that reduces common problems such as thread leaks and cancellation delays, duplicated cancellation logic, and awkward error handling. The core idea is that a group of related concurrent tasks is treated as a single unit of work with a clear owner, a clearly defined lifetime, and clear rules. Subtasks cannot outlive their scope, failures propagate cleanly, and cancellation flows automatically from parent to children.

The StructuredTaskScope lets you break a task down into concurrent subtasks that are coordinated as a single unit. Subtasks are forked to run on their own thread and joined as a unit when the work completes.

StructuredTaskScope has a factory method StructuredTaskScope.open(). This method has several overloads that allow you to provide a Joiner and/or a configuration callback. This lets you define the failure policy, a name for observability, and a timeout all in one place when opening the scope.

In our example, we want both methods (fetchOrders() and fetchRecommendations()) to succeed in order to correctly assemble the customer profile. We can provide a name for our scope and set a timeout for how long we are willing to wait on the results. If either of them fails, the other is cancelled. When a subtask fails or the timeout expires, join() throws an ExecutionException with the underlying cause. We switch on that cause to handle each case explicitly – including CancelledByTimeoutException, which is what the joiner uses to signal a timeout.

What if we don’t need results from all methods called in parallel? For example, imagine recommendations are available from two different caches and you only need the result of one of the calls to succeed. If one task succeeds, the other can be shut down. To accomplish this, we can use a different Joiner, anySuccessfulOrThrow(). As soon as one cache returns a result, the scope shuts down and the other task is cancelled automatically. If both fail, the join() method throws an ExecutionException with the exception of one of the failed subtasks as the cause.

To quickly scaffold a StructuredTaskScope in IntelliJ IDEA, use the built-in live template sts

Use the live template sts to create and open a StructuredTaskScope.

Structured concurrency is still a preview feature in Java 27, so it is not yet recommended for production use. That said, the feature has been relatively stable in its broad shape for several preview rounds, with some changes to the API, and now is a great time to experiment with it. To identify where you could use structured concurrency in your code, look for places where the code performs multiple tasks in parallel and awaits the results. This code is a candidate to be rewritten using structured concurrency.

Rewriting CustomerProfileService using Project Loom features

The “modern” branch of our demo project contains the same application rewritten using the features from Project Loom. The structure follows the same pattern as before: Orders and recommendations are fetched in parallel inside the scope.

The updated method getProfile() in the CustomerProfileService (which you can find here) now uses a StructuredTaskScope:

public CustomerProfile getProfile(String customerId) throws InterruptedException, TimeoutException {
        try {
            return ScopedValue.where(CUSTOMER_ID, customerId).call(() -> {
                try (var scope = StructuredTaskScope.open(
                        Joiner.awaitAllSuccessfulOrThrow(),
                        config -> config.withName("customer-profile").withTimeout(Duration.ofSeconds(2)))) {
                    var orderTask = scope.fork(() -> orderServiceClient.getOrders(CUSTOMER_ID.get()));
                    var recTask = scope.fork(() -> recommendationServiceClient.getRecommendations(CUSTOMER_ID.get()));
                    scope.join();
                    return new CustomerProfile(customerId, orderTask.get(), recTask.get());
                } catch (ExecutionException e) {
                    switch (e.getCause()) {
                        case StructuredTaskScope.CancelledByTimeoutException _ -> throw new TimeoutException("Request timed out");
                        case OrderServiceException ose -> throw ose;
                        case RuntimeException rte -> throw rte;
                        default -> throw new RuntimeException(e.getCause());
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Interrupted", e);
                }
            });
        } catch (InterruptedException | TimeoutException | RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

   

Notice that we no longer need the duplicated cancel() calls in two catch blocks. If either task fails or the timeout is exceeded, all remaining subtasks are cancelled automatically. There is no longer any need for manual cancel() calls.

The code now clearly expresses its intent: Fetch orders and recommendations in parallel, wait up to two seconds, and fail cleanly if anything goes wrong. Because the pattern of what the code does is clearly captured in the code, this code is easier to read, understand, and reason about.

To see the difference structured concurrency makes, run the updated service in IntelliJ IDEA and take a thread dump while requests are being processed. You can create a thread dump, as described earlier. From IntelliJ IDEA 2026.1, virtual threads forked within a StructuredTaskScope are grouped into containers representing their scopes. The IntelliJ IDEA debugger now shows you the structure in structured concurrency.

Get Thread Dump with StructuredTaskScope

Using Java 27 (EA) in IntelliJ IDEA

To try out the features described in this post, you will need Java 27. You can download it from inside IntelliJ IDEA via Project Structure | Project Settings | Project, and then open the SDK dropdown and select Download JDK. Set Version to 27 and select the Early-Access version. 

Download the JDK from IntelliJ IDEA

If you are using a different way to download JDKs, you can point IntelliJ IDEA to your installation. Go to Project Structure | Project Settings | Project, open the SDK dropdown, select Add JDK from disk, and point IntelliJ IDEA to your installation of Java 27.

If you’re using command-line tools like SDKMAN! or asdf, you can use inlay hints to make version management easier. If your .sdkmanrc or .tool-versions file specifies a JDK version that is not yet installed, an inlay hint will appear that allows you to download it directly. 

Download the JDK via .sdkmanrc

If the JDK is already installed but not configured for the project, you can use the inlay hint to set it as the project JDK.

Set the JDK via .sdkmanrc

For more information, see the documentation.

To get support for new language features, like structured concurrency, when using an early access version of the JDK, set the Language level to X – Experimental features.

If Java 27 has already been released when you’re reading this post, download the Java 27 distribution you want to use from IntelliJ IDEA or, if you already have Java 27 installed, point the IDE to your installation. To use structured concurrency, you also need to enable preview features. Set the Language level to 27 (Preview) – Primitive types in patterns, instanceof, and switch (5th preview) in Project Structure. IntelliJ IDEA will flag usage of preview features in the editor, so you are always aware which features are not yet stable.

Conclusion

Virtual threads, scoped values, and structured concurrency are designed as a cohesive system, each addressing a different dimension of the problem:

  • Virtual Threads improve scalability. They remove the need to manage thread pool sizes and make it practical to run one thread per task, even at high concurrency.
  • Scoped Values improve context propagation. This JEP solves some of the downsides of ThreadLocal (and framework workarounds), giving all tasks in a scope automatic, safe access to shared immutable context.
  • Structured Concurrency solves the structural problems in concurrency by giving concurrent tasks a clear lifetime, a clear owner, and a clean failure model, thus eliminating thread leaks, duplicated cancellation logic, and ExecutionException unwrapping.

Together, they let you write concurrent code that is much easier to read than traditional concurrent code, while being safe and scalable. The boilerplate that currently may take multiple steps to get right is replaced by code that is more concise and reads exactly like the problem it is solving.

You can use these features in IntelliJ IDEA. If you have questions or feedback, please let us know in the comments below.

Read the whole story
jhunorss
5 days ago
reply
Share this story
Delete

Software Architecture in the AI Era: What Doesn’t Change, What Does

1 Share
Read the whole story
jhunorss
15 days ago
reply
Share this story
Delete
Next Page of Stories