Live data from Hacker News

Using jlink to cross-compile minimal JREs

jakewharton.com

1–10 of 66 posts

Re: Using jlink to cross-compile minimal JREs

#2
JLink is a pretty nice tool - easy to use and understand, relatively fast, big app size wins. A few misc thoughts:

Figuring out what modules you need to ship can be done with the jdeps tool, but, you have to watch out for some gotchas. One is that you have to run it on each JAR or set of jars. It's slow so it helps to do this incrementally. Another is that some critical JVM functions are implemented as 'plugins' that are loaded reflectively, and the static analysis won't find them. The one that trips up my users the most is jdk.crypto.ec which implements elliptic curve cryptography. The Java TLS stack will be present but fail to connect to many TLS servers nearly at random if this module is missing. Nonetheless it still needs less configuration than a typical ProGuard/R8/native-image style dead code elimination analysis.

JLink can replace JARs with an optimized file format called "jimage". It uses a very interesting perfect hash algorithm to minimize the number of seeks/page faults required to find a class or file and it also creates a unified string table. So the space savings come not only from deleting dead code but also reducing the space required. Unfortunately it only works for JARs that are explicitly modularized (with a module-info.class file). Slowly more modules are getting this but most still don't. A good improvement would be to use the jimage format for everything. It also does a bunch of other optimizations, for startup time and the like. Also you have to figure out which modules are modular and put them on the module path at link time. There are some build system plugins that can help with that but they mostly don't do cross-linking.

Final thing to realize is that jlink doesn't create a bundled app. It just shrinks and customizes the JVM for your app. To allow your app to start up, be installed etc requires other stuff.

Conveyor [1] has extensive support for jlink. You can name a JDK or it will learn from your Gradle build, then it'll download the JMOD files you need for each platform, run jdeps on all your JARs incrementally, figure out which modules are explicit, figure out which modules are broken and have to be put on the classpath, add back the TLS ECC support, allow you to override all these decisions via config, run jlink for each target OS and architecture, finally bundle up the results in self-updating packages for each OS and do it all in parallel. It's doing a lot of work but the result is pretty magical - you just take the JARs from your build system e.g. via the Gradle plugin, feed it to the tool and out pops a nicely optimized set of downloads with HTML download page for your cross-platform app.

[1] https://hydraulic.software/

Re: Using jlink to cross-compile minimal JREs

#4
post #3

Reflection or any other kind of dynamic execution (JNI?) will break this, no?

No. You are thinking of GraalVM native image which will compile java to native machine code.

jlink is more similar to tree shaking: it strips the JRE of anything your program don't need.

Re: Using jlink to cross-compile minimal JREs

#5
post #4
post #3

Reflection or any other kind of dynamic execution (JNI?) will break this, no?

No. You are thinking of GraalVM native image which will compile java to native machine code. jlink is more similar to tree shaking: it strips the JRE of anything your program don't need.

But I think the parent's point is given reflection, how can jlink statically know the complete set of classes that your program needs?

Re: Using jlink to cross-compile minimal JREs

#6
post #5
post #4

Earlier quoted context omitted.

No. You are thinking of GraalVM native image which will compile java to native machine code. jlink is more similar to tree shaking: it strips the JRE of anything your program don't need.

But I think the parent's point is given reflection, how can jlink statically know the complete set of classes that your program needs?

It's only tree shaking the JRE itself, not your whole program (unfortunately..). So as I understand it, it means no dynamically calling arbitrary classes in the JRE, but that's a much narrower limitation.

Final binary size is naturally vastly reduced b/c you won't have the whole JRE, but last I tested you still end up with very chunky executables (minimal JFX GUIs were coming out to 100-200 MB)

Re: Using jlink to cross-compile minimal JREs

#7
post #6
post #5

Earlier quoted context omitted.

But I think the parent's point is given reflection, how can jlink statically know the complete set of classes that your program needs?

It's only tree shaking the JRE itself, not your whole program (unfortunately..). So as I understand it, it means no dynamically calling arbitrary classes in the JRE, but that's a much narrower limitation. Final binary size is naturally vastly reduced b/c you won't have the whole JRE, but last I tested you still end up with very chunky executables (minimal JFX GUIs were coming out to 100-200 MB)

Is there something that can do the tree-shaking of the Java program?

Re: Using jlink to cross-compile minimal JREs

#8
post #6

Earlier quoted context omitted.

It's only tree shaking the JRE itself, not your whole program (unfortunately..). So as I understand it, it means no dynamically calling arbitrary classes in the JRE, but that's a much narrower limitation. Final binary size is naturally vastly reduced b/c you won't have the whole JRE, but last I tested you still end up with very chunky executables (minimal JFX GUIs were coming out to 100-200 MB)

Is there something that can do the tree-shaking of the Java program?

I think if you do Graal native compilation then you'd effectively get that. The linker should chuck unused code. (though tbh I haven't tried it myself)

But to just say "no reflection" and tree shake your JVM code - not that I'm aware of unfortunately! I'd love to just treeshake entire unused dependencies. At the moment I do it manually - but it's a chore and it's hard to do comprehensively.

Now that post- Java8 you're supposed to jlink the JRE, I somehow doubt this will ever happen. The people that care about executable size would probably be doing Graal Native.

Re: Using jlink to cross-compile minimal JREs

#10
These days, all Java runtimes are created with jlink, including the one that's bundled in the JDK, so it's worth it to take a few minutes to learn how to do it yourself for a custom image. The result is not only drastically smaller, but more secure, as the potential attack surface area is much smaller.

If your application is modularised, it can become a part of the image, but, as the article shows, creating a custom runtime is easy and recommended even if your application is not modularised.

BTW, the JDK contains not just a Java runtime, but development tools, as well as an additional copy of the entire class library (this copy, in jmod files, is stored in a format that jlink uses as its input; i.e. it's there only to allow generating new runtime images). Use the entire JDK as an runtime is a real waste of space, since, among other things, it contains all libraries twice.

One minor comment, though. jlink produces Java runtime images or, in short, Java runtimes -- not a JRE. The name JRE refers to a particular kind of Java runtime from a bygone era when there was a global Java runtime environment, which was used by applets (and Web Start applications). When applets and Web Start were removed, the JRE and the very concept of one, was gone along with them. Some people still use the anachronistic term JRE to refer to a Java runtime (and some companies distribute pre-linked Java runtimes and call them JREs), but the real JRE is gone.

Post reply on HN