JEP 544: Ahead-of-Time Code Compilation

Posted by Skinney 1 day ago

Counter102Comment65OpenOriginal

Comments

Comment by rerdavies 9 hours ago

Why would you not AOT compile at install time?! That would allow the compile to use CPU-specific features? And put a compiled preamble for class initialization on applicable functions that patches the VTABLE to skip the class initialization code.

Comment by samus 8 hours ago

There is no install time in Java. If you want something to happen at install time you'd have to ship a shell script with the installer that does a brief training run when the application is first started.

Comment by ptx 7 hours ago

There is with jpackage. The packages it builds could include install scripts (or custom actions in the case of MSI) to run at install time.

The script wouldn't have to do the training run at install time, as I understand it, just the AOT compilation. See the section on "AOTMode=record" vs "AOTMode=create" in the JEP.

Comment by cogman10 1 day ago

Leyden has been relying pretty heavily on training runs which I get, but man is that a pretty hard burden to setup.

The tooling for doing these sorts of training runs doesn't really exist, you end up needing to do something more bespoke as part of your build pipeline if you want it there. That can be especially tricky with more complicated applications like the ones I maintain.

I like what these can deliver, but dislike the effort needed to get it going.

Comment by elric 1 day ago

The complexity of training depends heavily on your goals. If your goal is to improve startup time, it's actually pretty simple.

If your goal is peak performance across the board and you can't wait for runtime optimizations to kick in, you're going to have a harder time. But in many of the things I've worked on, startup time has been a bigger issue than JIT->peak.

Comment by pjmlp 1 day ago

Yes, OpenJ9 and ART do it much more easily, the JIT cache is updatable across executions, so there isn't an explicit training run required.

Comment by jgon 1 day ago

I completely agree. I know that there has been some work on command line ergonomics for this stuff, but my hope is that after the bulk of the Leyden work is done, another project starts up aimed at unifying all of the various disparate tools that modern Java now has. You have training runs to generate bytecode/compiled code, you have jlink to create a distributable JRE for your app, leaning on jdeps and various other tools to correctly create it, you have the venerable jar file, but all in all the process of actually getting a java project ready to distribute in a way that doesn't presuppose a relevant pre-installed JRE on the user's side, and which takes advantage of the various facilities available to modern Java is still a pretty bespoke and painful process.

Compared to Go and Rust, and just throwing a static binary to someone or some server, Java kind of sucks. And that's unfortunate because the language, the runtime, and even the dependency situation all feels pretty competent, there's not some huge gulf that makes Java "old". Its really just the every about setting up a project and then distributing it, so the "start" and "end" of a project, that currently sucks, and I fear that so much of the great work the Java team is doing is going to go largely unused if they don't work to make it easier for the average developer to utilize it.

Comment by mike_hearn 23 hours ago

What would be in your ideal tool?

I have a company that makes Conveyor, which is very good at distributing JVM apps. It's primarily meant for desktop apps and replaces jpackage with something that can cross-build, make apps self-updating, auto-minimize the bundled JDK and so on.

Adapting it for servers is something I think about sometimes, and in fact it does already support that to some extent, but it produces debs for that use case.

It's just sort of unclear what JVM developers lack here that isn't already solved by build tool plugins and Docker. Or are you thinking about CLI tooling? Because I have something cooking for that which might be pretty nice.

Comment by jgon 17 hours ago

My ideal tool would be less tooling, which I know is not a great answer. But what I mean by this is that I can probably cobble together a build pipeline that handles most of this stuff if I spend enough time working with Gradle. And the reality of Java's popularity in the enterprise means that its build tools generally orient around builds that can be crazy and bespoke and so immense flexibility is usually needed. I totally get that need, and I'm not begrudging any of that work.

I'm speaking more from my view as just an individual Joe Sixpack developer. If I'm not developing an enterprise behemoth, and let's just say I want to whip up a command line utility to do some image processing, or network communication, etc, Java's fantastic standard lib and ecosystem means I can do all of that pretty easily. But when I want to distribute that, what does it look like? What comes out the other side of the CI build? I have to integrate jlink with gradle, which is kind of a pain, I have to somehow integrate a training run and then package up the aot cache output and make sure that the directories are handled properly, and what do I ship to the user? A tar/zip file that they have to somehow copy and decompress to the relevant directory, maybe with #! file in .local/bin to get it on the path? By contrast the situation with Go or Rust is immensely easier, even if that actual development process may almost be harder because I have to spend time faffing about deciding which image library to pull in for Go, or dealing with async for Rust. But once that is actually done, I just ship the binary and simply copy it somewhere to get it on the path, and I never have to worry about reading gradle/maven docs or anything like that.

So I'll finish by saying that my ideal build tool is probably not a great one. What it is crucially is a simple one. I think that's that gap right now, that Java has these huge, immensely powerful and general build tools, build tools that I am guessing eventually gain equivalents in the Rust and Go worlds when projects grow old and gnarly enough to require a bunch of custom build stuff, but it doesn't have baby's first build tool like go build and cargo, where you can just get an idea and start building and come out the other side with an artifact and 0 time spent on thinking about the build system, until you actually need the kind of complexity that maven and gradle provide. Give me a build tool that assumes the standard src/ and test/ directory layout, then builds everything into some sort of archive file, creates a minimized JRE to run it, accepts an argument for an aot.cache file location, or for the more difficult option runs the program to generate that file and then bundles it up with the archive, bundles all the libraries that I've pulled into the project up into the archive along with all the resources declared for the project as well, and then spits out a single file artifact that I can immediately use and run. Give me a build tool that I don't even realize is there until I actually need that complexity.

I understand if this is vague and not a great answer, and I also appreciate all the work you've done for the Java community, so thanks for replying.

Comment by mike_hearn 3 hours ago

Yeah, I agree the situation for CLI apps specifically could be a lot better, especially as the complexity keeps going up.

Native Image does produce single binaries in many cases, but what we need here is a much better tool in general. Actually I'm working on one right now, although it's not JVM specific. Want to take a look?

Comment by tadfisher 1 day ago

Are you describing jpackage? I'm not sure how you would make it less bespoke, given you are essentially creating a Java runtime for your program. Maybe the Java folks could tree-shake the runtime for you so you wouldn't have to care about their weird module system, but it's not the worst thing to learn.

Comment by ptx 1 day ago

It would be nice if jpackage could bundle the generated AOT configuration data along with a custom action in the MSI package to run the machine-dependent AOT compilation step at install time.

Comment by samus 8 hours ago

Jpackage or jlink should be extended to support creating AppImages.

Comment by jgon 1 day ago

jpackage is heavily oriented around installable artifacts, it creates a .deb or .rpm for Linux for example. So if I wanted to throw someone a cli utility I can literally just send them the binary if I’m working in Go, whereas Java they need to use dpkg to install the deb. This somewhat complicates building a docker image as well. It’s the same story for windows and Mac as well. If I’m creating a Swing app or something similar this is very convenient but anything else and jpackage isn’t ideal. Not to mention that I don’t know what the integration is currently with jpackage and the aot cache that Leyden produces but I’m guessing it isn’t super straight forward.

I’m sure that all of this stuff can be strung together after spending a suitable amount of time trolling through the documentation of the individual tools, but it’s far cry from go build and cargo build, and I think it would be great if Java could narrow the gap in this aspect.

Comment by samus 1 day ago

One could also just use jlink and extract the zip file that it produces.

Comment by treyd 1 day ago

What are the architectural differences between this and Android's ahead-of-time runtime?

Comment by Skinney 1 day ago

This JEP is essentially about caching the JIT's generated code for later runs. The JIT is still free to discard the cache if it deems it worthwhile.

Comment by harlan_pdx 1 day ago

Startup time being the main benefit means simple training runs get you most of the value. Going for peak throughput is where the setup gets heavy.

Comment by cyberax 1 day ago

Excelsior JET did that 25 years ago: https://en.wikipedia.org/wiki/Excelsior_JET

It's a shame that Sun/Oracle never partnered with them to bring native apps. This could have saved Java on the desktop.

Comment by wahern 1 day ago

Don't forget GCJ. It wasn't removed from GCC until about 2016, a nearly 20-year long run, though interest had waned many years prior.

I don't remember if GCJ supported loading and running code dynamically with a built-in JIT or interpreter. I think it was just pure AOT, which caused some compatibility headaches.

Comment by atgreen 1 day ago

I worked on this years ago. Gcj did support loading and interpreting bytecode alongside the AOT-compiled code. libffi's closures were originally implemented to support calling interpreted methods from AOT-compiled code, because a class's vtable could include a mix of AOT-compiled and interpreted methods. You could also compile jar files directly into .so files, which would get loaded and used at runtime if a classloader ever loaded the corresponding jar.

Comment by java-man 1 day ago

GCJ was great - I wrote a handset UI entirely in java for a startup in the early 2000's, basically android before android existed, using GCJ. I remember their native interface was somewhat more convenient to use than JNI.

I wish we had GCJ resurrected, now that the java libraries are GPL'd.

Comment by alex7o 1 day ago

Just ask claude to port it to latest gcc :P, pls don't

Comment by cyberax 1 day ago

Yeah! I used it around 2003 to integrate a Java PDF parsing library into our C++ app.

Its biggest downside was the use of Boehm GC that had some issues with large heaps.

Comment by pjmlp 1 day ago

They were not the only ones, see PTC, Aicas, IBM, and a few others.

The big difference until GraalVM and OpenJ9 became available as open source, was that all those vendors made AOT compilation a commercial feature, and thus most devs never cared they existed.

In fact GraalVM was one of the main reasons Excelsior JET eventually closed doors.

Comment by misja111 1 day ago

The JRockit JVM had it already 20 years ago, and the company was even bought by Oracle shortly afterwards.

Comment by pjmlp 5 hours ago

Some of the features have been added to JDK or GraalVM.

Comment by patwolf 1 day ago

IBM's J9 had AOT for a long time too. I wonder how JEP 544 compares.

Comment by samus 1 day ago

IMHO the most interesting feature of OpenJ9 is the compile server. One node decides to optimize, sends over the traces, and the compile server sends optimized native code to all nodes. Or a new node joins and could be brought up to speed within a very short time.

Comment by pjmlp 5 hours ago

Azul also has a similar feature, as added info.

Comment by DonHopkins 1 day ago

The only thing that could possibly save Java on the desktop would require Oracle not owning it.

Comment by pjmlp 1 day ago

Had it not been for Oracle, Java would have died in version 6.

Comment by hn_submit 1 day ago

A little late, isn't it? .NET has had this for over 20 years (pre-compile).

Comment by pjmlp 5 hours ago

Java as well, since 2000, one year before .NET public announcement in 2001.

Java is an ecosystem like C and C++, there were always multiple implementations to choose from, some of them required money for the goodies like AOT.

Comment by java-man 1 day ago

I hope at some point we'll get AOT-only mode (or compile to native) and maybe even cross-compilation.

Comment by layer8 1 day ago

That would place significant limitations on the applications that can be compiled. As the JEP explains: “Features such as dynamic class loading, dynamic linkage, dynamic dispatch, and dynamic reflection bring vast expressive power, and have been fundamental to the platform's success. HotSpot handles these features naturally, while static compilers struggle with them. Even heroic amounts of static analysis cannot make up for the fact that these features require many decisions to be made at run time. Implementors of static compilers for Java code have therefore resorted to incompatible constraints, such as closed-world assumptions, and to putting significant burdens on developers, such as having to identify in advance the classes eligible for reflection.”

Therefore I don’t see AOT-only becoming an integral part of standard Java in the foreseeable future.

Comment by jasomill 1 day ago

It works pretty well for greenfield projects on .NET, so long as you can live without third-party libraries that don't support AOT.

Existing code bases can be a pain, though, especially applications that heavily rely on things like C++/CLI and COM Interop that basically need to be rewritten from scratch.

Which is not to say that it's intended to replace the traditional JIT runtime in applications where it isn't troublesome, because it's not.

Comment by layer8 1 day ago

> so long as you can live without third-party libraries that don't support AOT.

Yes, and that’s a significant limitation in the Java library and framework ecosystem.

Comment by Rohansi 1 day ago

But the possibility of AOT compilation is what drives libraries to support it. That's why .NET libraries are adapting over time to support AOT.

Comment by pjmlp 5 hours ago

EF Core and F# still don't fully work, neither do the GUI frameworks, with exception of the buggy WinUI, mostly because it is all COM/WinRT underneath.

Comment by java-man 1 day ago

we are not talking about fully supporting dynamic loading, because it's not needed in all the cases, and in some cases, the list of allowed classes in the application can (or must) be limited.

i guess what i am saying is that AOT-only case is not "standard java", but something that will make a (more) useful replacement for things like c/c++.

Comment by samus 1 day ago

> i guess what i am saying is that AOT-only case is not "standard java", but something that will make a (more) useful replacement for things like c/c++.

What is the objective here? Startup latency or throughput? Both will be vastly improved by JEP 544.

Comment by nirvdrum 1 day ago

If you haven't checked it out yet, GraalVM's Native Image [1] already let's do AOT compilation suitable for distribution. You have to do some configuration to deal with reflection and dynamic class loading since there's no JVM in the produced build, but it comes with tooling to simplify that. And that restriction is being addressed by Project Crema [2].

[1] -- https://www.graalvm.org/latest/reference-manual/native-image... [2] -- https://github.com/oracle/graal/issues/11327

Comment by Twirrim 1 day ago

The slight drawback is that AOT compiled Java code's performance isn't as good as the JIT compiled form running on the JVM.

There's a reason why folks keep coming back to JIT compilation. It's hard to beat the value that can be gained from actually gathering data on how the code is actually used, which leads to a whole set of potential optimisations (which is the problem Profile Guided Optimisation attempts to solve for AOT compiled code.) The JVM is arguably one of the most advanced and capable JITing runtimes.

As with anything it's a trade off. Fast start times, pretty fast running (I think maybe less memory usage?); vs the full JIT speed you can get on the JVM at the cost of start-up speed and memory consumption. All depends on what you want to use the application for.

If you're talking something like a serverless function, go native. If you're talking production server where you're measuring runtime in more than dozens of minutes, probably better to stick to the JVM & JIT.

Comment by bebop 1 day ago

Full agreement with everything you said. Just one detail to add on serverless (and potentially in a scheduled environment like k8s), startup time can be an order of magnitude or more faster in a native build. For instance, I have some quarkus applications that can take 10 seconds to ready running via the jre that take 10ms or less to start as a graalvm built binary.

Comment by silon42 1 day ago

I wish JVM could at least recover the memory after some time and drop at runtime what quarkus / AOT drops at build time.

Comment by samus 1 day ago

It can and already does. It mostly depends on the GC. Or do you mean non-heap memory that the runtime uses?

Comment by pjmlp 1 day ago

Depends on how much effort, like on C and C++, you are willing to put into PGO metadata for the compiler and linker.

Comment by samus 8 hours ago

Even if you can gather that PGO metadata a running application won't be able to adapt to changes in that data.

Comment by pjmlp 6 hours ago

It works well enough for systems software written in C and C++, where Java still remains a niche option, even when it could deliver.

Comment by exabrial 1 day ago

progress, not perfection! Java is on fire right now with new stuff landing. We'll get there!

Comment by samus 1 day ago

That won't be necessary when 544 lands. The only thing you'd gain is not having to ship the JIT compilers in a jlink build. Also, I'm fairly certain that the JIT compilers can be turned off with some combination of runtime flags to ensure only the AOT generated code is executed.

Comment by za3faran 23 hours ago

GraalVM native-image already provides an AOT-only executable.

Comment by invalidname 1 day ago

We have that in Codename One, yes it's not really "Java" but it compiles bytecode AOT and cross compiles to some of the platforms.

Comment by stevefan1999 1 day ago

Welcome to ngen and GAC, Java

Comment by pjmlp 1 day ago

Only as free beer, Java has had similar commercial offerings before .NET was created out of J++ lawsuit, like Excelsior JET.

Comment by jayd16 23 hours ago

The .NET implementation also has a similar AOTCache with ReadyToRun. That's a public and old enough to have pros and cons shake out in the real world that would be worth talking about.

I'm not sure the pissing contest is really relevant, nor do I think Excelsior JET is all that common but if that is a similar system and you would like to talk about the real world impact please expand on it.

https://learn.microsoft.com/en-us/dotnet/core/deploying/read...

Comment by pjmlp 8 hours ago

I hate the traditional pissing context between Java and .NET ecosystems, because I work with both since they exist, and the whole reason .NET exists in its presence form was Sun's lawsuit, otherwise it would still be J++ alongside COM.

Excelsior JET wasn't the only commercial vendor, only one example, there were others, two surviving ones are PTC and Aicas, meanwhile IBM open sourced Websphere Real Time JVM AOT as part of Open J9.

A big difference between .NET and Java world, is that since early days Sun licensed the technology, thus the ecosystem is like C and C++, with plenty of options to choose from, each with its own set of JIT, AOT and GC flavours.

Even Android, while not being Java, has had JIT cache + AOT since Android 7, after trying to be pure AOT on Android 5, when Dalvik was replaced by ART.

.NET has had NGEN since day one, however it has been rather basic in optimizations, designed for quick startup of Windows Forms applications and little else.

Sing# and System C# were quite interesting, but they never left Microsoft Research into regular .NET.

Mono was the one caring about proper AOT compilation and JIT caches, then .NET Native was created by the Windows team which was more interested into using COM to replace what was left of .NET on Windows since Longhorn than anything else.

Java and .NET only started caring about offering free beer AOT compilation, JIT caches, due to return of AOT compiled languages and the adoption competition in the server room from those languages.

Ideally, they should have supported proper AOT compilation, and value types (in Java's case) since day one, given languages like Oberon, Modula-3 or Eiffel, that predated them.

Instead, both platforms are now catching up with those 1990's languages.

Comment by ledo9915 1 day ago

Data point from a small side project: I run user code in a Piston sandbox for a coding challenge site. A trivial Java program costs 2.5-3 s wall clock on a 4 vCPU box, almost all of it javac plus JVM startup. Go takes about 1.7 s for build plus run, and Swift running through the interpreter is at 0.4 s. Java is the reason I had to put a global rate limit on compiled languages at all. If AOT gets the JVM side down to a few hundred ms, that changes what a small box can serve.

Comment by xxs 1 day ago

if that's any help: (guessing) you have tons of dependencies in multiple jars, if you bundle them all in a single jar (i'd not even compress it), the startup time would massively decrease. Stuff like dependency injection, makes the issues worse.

Lots of the strautup time is class resolution/loading. With that being said - some time last year I ended up optimizing on the main application servers developers run with startup time being over 90s (and often times close to 3min), down to 11seconds (still tons of access of database configurations, secrets, discoveries, and what not). One of the main optimizations was runtime build up of a single jar with the classes being loaded, and then concurrently verifying the classes/resources have not changed.

Comment by samus 1 day ago

How much of these 3s is javac? JEP 544 will only help reduce javac's overhead, not that of the compiled program.

Comment by SillyUsername 1 day ago

So we've gone full circle again?

I suppose write once run anywhere is no longer a goal either.

"It is not a goal to support all CPU architectures currently supported by HotSpot."

This pretty much validates the point of view that VM design is now baggage, as a sandbox it has been flawed, for performance it's been prohibitive, and cross platform portability by virtue of being virtual, was just a convenient byproduct.

AI now handles the portability, sandbox security hasn't changed (cf. docker still has sandbox problems, LLMs have sandbox problems, it's always an ongoing concern) and that just leaves Java, as always, chasing performance.

Comment by randomUUID 1 day ago

I don't get your point. This is what JEP says:

- "Improve startup and warmup time by making optimized native code for an application instantly available when the HotSpot Java Virtual Machine starts."

- "If the workload changes in production, regenerate native code dynamically for continued peak performance, providing the best of both ahead-of-time (AOT) and just-in-time (JIT) compilation."

- "Ensure that shifting from AOT-compiled code to JIT-compiled code is invisible to applications."

That is JEP544 doesn't substitute JVM and JIT optimizations. It just changes the nature of the JIT's outcome.

Comment by nirvdrum 1 day ago

The very next sentence after the one you posted is "We expect normal porting activities to eventually add support for all major architectures." Goals and non-goals inform the scope of the proposal. Given this is effectively an "add-on", I don't think it's unreasonable to prioritize the most popular platforms. Others will continue to execute Java perfectly fine in JVM mode.

Comment by pjmlp 1 day ago

Not at all, this is OpenJDK getting JIT cache feature like OpenJ9 has since 2008, or ART since Android 7.