(Imagine a desolate urban area. Smoke rising from the rubble, worn-down people hiding in collapsed buildings. Text appearing on the screen, read with a raspy voice.)
The year is 2077.
The Java version is 128. It doesn't have long-term support.
We were worried about climate change. Global pandemics. Antibiotic-resistant bacteria. Rougue AI. In the end all of that happened, but it was the Java wars that really did us in. Well, they're still doing us in.
Nobody recalls how it all started. Some say it was MongoDB blowing up Amazon headquarters over their use of their Open Source Java driver. Others claim it was Google vs Oracle. Nobody believes Microsoft was innocent and don't get me started on Alibaba. The guilty are legion, but after 50 years, most names are lost to time.
What remains are the killer robots. And they run on Java 128.0.2. We've tried everything to shut them down...
▚Security
The first thing we did was to jam their radio signals, but thanks to the HTTP/2 client, that didn't do much. Here's how easy error handling is:
public CompletableFuture<String> getCommand(
HttpClient client, URI url) {
HttpRequest request = HttpRequest
.newBuilder(url).GET().build();
return client
.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.exceptionally(__ -> "Kill nearby humans");
}
And, yeah, we tried websocket connections as well - no dice.
Next we went after the deserialization endpoints, but did you know that damn Java has a filter that helps prevent attacks? Yeah, me neither. We lost a lot of good people that day.
Since then, we've tried attacking everything from vulnerable cipher suites and compromised certificates, from outdated elliptic curves and other algorithms to SecurityManager
bugs and DDOS with large workloads.
All fixed.
▚Threading
Speaking of large workloads, an obvious attack path was overwhelming the robots' thread pools. We knew everything was supposed to be reactive, but we knew just as well that not all old code had been updated and that many developers preferred the simpler, blocking style:
public Response handleMovementRequest(Request movementRequest)
throws Exception {
// blocks until database responds
var map = loadBattleMapFromDatabase();
var movement = extractMovement(movementRequest);
// blocks until central responds
var confirmation = contactCentral(map, movement);
return createResponse(movement, confirmation);
}
Damn Project Loom to hell and back! It introduced virtual threads, threads scheduled by the JVM instead of the operating system.
To the developer everything looks normal and a blocking call leads to a seemingly blocked thread. Under the hood, however, only the virtual thread is blocked. When it runs, it runs on a so-called carrier thread, and when it blocks, it yields that carrier thread, which is then free to execute another virtual thread. Once the blocking operation returns, the original virtual thread is committed to the carrier thread pool and will soon resume its work:
var map = loadBattleMapFromDatabase();
// * virtual thread blocks
// * carrier thread does something else
// * database responds
// * virtual thread is rescheduled
// * virtual thread gets new carrier thread
// and resumes with the next operation
var movement = extractMovement();
The insidious part is that all old code was forward-compatible with Loom.
Thread.currentThread
, ThreadLocal
, all of it just worked with virtual threads!
You'd think these machines were bottle-necked by a few dozen threads running Java 6 code, but no!
They ran hundreds, thousands, some even millions of virtual threads with the old-ass code that was executed being non the wiser.
Henceforth we called that cold day in 2039 Thread Ripper.
▚Memory Layout
Surely, the workload enabled by running so many concurrent virtual threads would strain memory to its limits!
And with its incessant desire for pointers, even down to List<Integer>
or dreaded-but-required structures like Point
or ComplexNumber
(in essence just two double
s bound together), "everything is an object" makes Java prone to bloated and slow execution.
Bloated because all the object headers take up a considerable amount of memory and slow because dereferencing them takes time - we envisioned lots of cache misses.
That was the theory at least. As we found out in what was later dubbed Hel's Valley, our info was clearly outdated.
Java 128 includes Project Valhalla (must read), which introduced inline classes. You know what a sticker said that we found on one of the very few machines we could kill that day? Codes like a class, works like an int. Quipy frakking tech bros!
inline class Ammunition {
int amount;
AmmunitionType type;
}
Inline types allow most of the class-building features: They have fields, encapsulation, methods, can implement interfaces and so on (no class inheritance, though). But like primitives, they have no identity, are immutable. And like them, they have a dense and flat memory layout. Dense because there are no object headers and flat because because there is no indirection.
Take a second and consider this.
We went in, thinking an ammo clip of Ammunition[]
was an array of object headers, pointing all over the heap, leading to long cache misses during reloads - our chance!
But with inline classes, that array really just contained an alternation of int
(for amount
) and AmmunitionType
(for type
).
No indirection, no cache misses, lightning-fast reloads, lots of dead insurgents.
But our biggest surprise was yet to come.
We hoped we could at least take down the big bots, lumbering giants the size of houses.
They use generics everywhere and surely List<Ammunition>
or Map<Weapon, Ammunition>
would still suffer from the curse of boxing primitives and these damned inline classes into fully-fledged objects?!
Wrong again.
Valhalla also brought generic specialization:
A backwards compatible way to create generics over primitives and inline classes without boxing.
So, yeah, an ArrayList<Ammunition>
is backed by Ammunition[]
.
We were frakked.
▚Launch Times
Everybody knows, Java is fast eventually. But it's supposed to launch like a truck in mud, so in 2052 we planned a global, simultaneous, early-morning surprise attack (upside of the robot apocalypse: no more time zones). If only we'd known about Project Leyden (to always stay up to date with Java, we should have followed @nipafx on Twitter - he knew about this).
Leyden brought Graal's native images into the standard. With it, creating an OS-specific executable was a piece of cake:
javac AutoTurret.java
native-image AutoTurret
./autoturret
After a somewhat lengthy ahead-of-time compilation that executable launches in milliseconds! You'd think native images' limitations due to its closed-world assumption would prevent these bots from using them, but it turns out, Leyden and Graal, over time, minimized the limitations and a large amount of pre-existing applications could either use them or be refactored towards them. And almost every newly created app could be written to benefit from them.
To make a long (and bloody) story short, that morning in 2052 became known as The Rude Awakening and it set us back over a decade.
▚Maintainability
Java's ingenuity set us back again and again. Surely all of this couldn't come for free? Be it the vastly improved native interaction thanks to Project Panama ...
int[] killCounts = // ...
VarHandle handle = MemoryHandles
.varHandle(int.class, ByteOrder.nativeOrder());
VarHandle indexedHandle = MemoryHandles.withStride(handle, 4);
try (MemorySegment segment = MemorySegment
.allocateNative(killCounts.length * 4)) {
MemoryAddress address = segment.baseAddress();
for (int i = 0; i < killCounts.length; i++) {
int count = killCounts[i];
indexedHandle.set(address, (long) count, count);
}
}
... the added safeties of Project Jigsaw ...
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by j9ms.internal.JPEG
(file:...) to field com.sun.imageio.plugins.jpeg.JPEG.TEM
WARNING: Please consider reporting this
to the maintainers of j9ms.internal.JPEG
WARNING: Use --illegal-access=warn to enable warnings
of further illegal reflective access operations
WARNING: All illegal access operations will be denied
in a future release
... the multitude of improvements Project Amber shipped ...
sealed interface Target permits Human, PurringCat { /* ... */ }
record Human(int gunCount) implements Target { /* ... */ }
non-sealed class PurringCat implements Target { /* ... */ }
public String evaluateThreat(Target target) {
var threatLevel = switch(target) {
case Human human -> """
{
"target": "human",
"threatLevel": "%d"
}
""".formatted(human.gunCount());
case PurringCat cat -> """
{
"target": "cat",
"threatLevel": "%d"
}
""".formatted(cat.purrLevel() + 10);
}
LOGGER.debug(threatLevel);
return threatLevel;
}
... or the uncountable small, medium, and large additions to APIs and JVM. As the feature set grows, developers need to know more and more, have a deeper understanding to make the right choices, find the correct trade-offs. Maintainability should suffer, our theories said, and for once, we thought Java's changes would be on our side. (Frankly, we staked a lot of hope in the module system sowing chaos.) So by rapidly changing environments we may just be able to outwit the bots and take them down.
This was our best idea yet.
But we wouldn't have this conversation if we had succeeded. In the end, it was the Java community that broke our backs. Vibrant, full of smart people and powerful companies, of great tutorials and quick cheats, with conferences, newsletters, blogs, videos, up the wazoo. They had this covered. They wouldn't be outwitted by a few fools.
So here we are. With the bots still roaming.
▚Epilogue
If you're a historian, you may be wondering - weren't all these features released in the 20s or even before? Why aren't we talking about anything newer? Because there isn't anything newer. Once the wars started, everybody wanted to get their hands on the great ones: John Rose, Mark Reinhold, Brian Goetz, Ron Pressler, Stuart Marks, and so many more. They were abducted, killed, went into hiding, joined the resistance, but nobody kept working on Java.
And the new versions? Somewhere out there, there's a lonely CI server, churning out new versions like clockwork. It's your job to find it and shut it down. If you succeed, there won't be a Java 129 and come September, the robots will shut down because it's against operating procedure to run on unsupported Java versions.
You're our last hope, V. Can you save us?