I spent most of Tuesday fighting with a Docker image that refused to shrink below 400MB. You know the feeling. You strip the OS, you multi-stage the build, you pray to the gods of compression, and yet—it’s still bloated. It’s enough to make you want to go back to writing Go.

Almost.

But then the new release cycle hit, and I started looking at what BellSoft is doing with Liberica JDK 20. While everyone else is arguing about the syntax sugar in the new Java specification, I’m looking at the runtime. And honestly? This might be the release that finally makes me switch my default base images.

The Musl Advantage (and Why You Should Care)

Most of us default to standard OpenJDK or maybe Amazon Corretto because it’s “safe.” I’ve used Corretto for years—it’s solid, predictable, and AWS supports it. No shade there. But BellSoft has been carving out this weird, hyper-optimized niche with Liberica that targets Alpine Linux users specifically.

If you’ve ever tried to run standard Java on Alpine, you’ve probably hit the glibc vs. musl nightmare. It crashes. It throws obscure linking errors. It haunts your dreams.

Liberica JDK 20 continues their trend of native Musl support. This isn’t just a wrapper; they’re building specifically for it. The result is Alpaquita Linux—their distro—which claims to be smaller than Alpine. I tested a simple Spring Boot app (okay, maybe not “simple,” it had a few too many dependencies) and the startup time difference wasn’t massive, but the footprint? Noticeable.

Docker shipping container concept - The Mental Model Of Docker Container Shipping
Docker shipping container concept – The Mental Model Of Docker Container Shipping

What’s actually in JDK 20?

Let’s talk about the Java features themselves. JDK 20 is a “feature release,” which is code for “don’t run this in production unless you’re brave or bored.” But for those of us experimenting on the side, the updates to Scoped Values are the real story.

ThreadLocals have been a pain for fifteen years. They leak memory like a sieve if you aren’t careful, and passing context between threads is clunky. Scoped Values (JEP 429) in JDK 20 are trying to fix that. It’s cleaner. It’s immutable. It’s what we should have had a decade ago.

Here’s a quick look at how I’m messing around with it in Liberica 20:

import jdk.incubator.concurrent.ScopedValue;

public class ScopedValueTest {
    // Define a scoped value
    final static ScopedValue<String> USER_ID = ScopedValue.newInstance();

    public void handleRequest() {
        // Bind the value only for the scope of the runnable
        ScopedValue.where(USER_ID, "user-12345")
                   .run(() -> {
                       process();
                   });
    }

    private void process() {
        // No passing arguments down the stack!
        System.out.println("Processing for: " + USER_ID.get());
    }
}

It looks simple, but this changes everything for frameworks. Speaking of which, the ecosystem is scrambling to keep up.

The Framework Shuffle: Quarkus, Micronaut, and the Beta fatigue

It’s not just the JDK. The whole stack is in flux right now. I saw that Quarkus dropped 3.0.0.Beta1 recently. This is a big deal because they’re finally moving to Jakarta EE 10 packages. If you’ve been ignoring the javax to jakarta namespace change, your time is up. It’s happening.

I tried upgrading a service to Quarkus 3 last night. It broke immediately. Obviously. That’s on me for using a Beta 1 release, but the migration scripts are actually getting better. If you’re using Hibernate, expect some friction.

Docker shipping container concept - Docker for beginners: a guide to understanding the core concepts
Docker shipping container concept – Docker for beginners: a guide to understanding the core concepts

Micronaut is also pushing 4.0.0-M1. It feels like everyone decided to break compatibility at the same time. It’s exhausting, but necessary. We can’t keep dragging legacy implementations forever.

Helidon and Open Liberty

I don’t use Helidon as much—mostly because I’m stuck in the Quarkus/Spring ecosystem—but seeing version 3.2.0 pop up is interesting. They were one of the first to really push Virtual Threads (Project Loom) hard. With JDK 20 iterating on Loom (second preview), Helidon Níma is looking more and more like the future of high-throughput services. Blocking code that scales like reactive code? Yes, please.

And Open Liberty 23.0.0.3-beta… well, it’s there. IBM’s workhorse keeps chugging along. It’s solid, but it doesn’t excite me the way a sub-50MB container image does.

So, do you upgrade?

Here’s my take: If you are running a monolithic banking app, stay on JDK 17 (or 21, if you’re feeling spicy). Don’t touch JDK 20. It’s a short-term release.

But if you are building microservices and you pay your own cloud bills? Give BellSoft Liberica JDK 20 a shot, specifically the Alpine/Musl builds. The memory savings are real. The startup times are snappy. And with frameworks like Quarkus 3 and Micronaut 4 prepping for the next generation of Java, you might as well get your environment ready now.

Just maybe don’t deploy the Beta versions to production on a Friday. I learned that one the hard way.

Questions readers ask

Why does standard Java crash on Alpine Linux with glibc errors?

Standard OpenJDK builds are compiled against glibc, but Alpine Linux ships with musl libc instead. This mismatch causes obscure linking errors and crashes when running standard Java on Alpine. BellSoft Liberica JDK 20 solves this by providing native musl support built specifically for Alpine rather than as a wrapper, alongside their Alpaquita Linux distribution which claims a smaller footprint than Alpine itself.

What do Scoped Values in JDK 20 fix about ThreadLocals?

Scoped Values, introduced via JEP 429 in JDK 20, address the long-standing problems with ThreadLocals, which have leaked memory and made passing context between threads clunky for fifteen years. Scoped Values are cleaner and immutable, binding a value only for the scope of a runnable using ScopedValue.where(…).run(…). This lets you access context like a user ID without passing arguments down the call stack.

Should I upgrade my production app to JDK 20 or stay on JDK 17?

If you’re running a monolithic banking app or similar, stay on JDK 17 or move to 21 if you’re feeling spicy. JDK 20 is a short-term feature release and isn’t recommended for production unless you’re brave or bored. However, if you build microservices and pay your own cloud bills, Liberica JDK 20’s Alpine/Musl builds are worth trying for the real memory savings and snappy startup times.

What breaks when upgrading to Quarkus 3.0.0.Beta1?

Quarkus 3.0.0.Beta1 finally moves to Jakarta EE 10 packages, meaning the long-delayed javax to jakarta namespace change is now mandatory. The author’s upgrade attempt broke immediately, though that was partly due to running a Beta 1 release. Migration scripts are improving, but Hibernate users should expect friction during the transition. Micronaut 4.0.0-M1 is pushing similar breaking changes at the same time.