14 ms·
Using jlink to cross-compile minimal JREs
- mike_hearn 4y agoJLink 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/ https://hydraulic.software/
- oblio 4y agoI know that you're hawking your own software, but it looks quite cool. However, I wouldn't want to be a pioneer. Who uses it and for what? Do you have testimonials? Or maybe other HNers, have you used it?
- mike_hearn 4y agoWe now have a brands and testimonials section! https://hydraulic.software/ https://hydraulic.software/
- mike_hearn 4y agoYeah, hawking things is a bit of a new world to me. Good timing for the question, though. We've been collecting testimonials just last week and have a brands block for the website ready to go, pending one more approval. So that's coming RSN. Some firms who said we can point to their usage of it (these are all JVM based): - HEBI Robotics. They make robot kits and ship apps that allow you to control them with Conveyor. - GoToTags. They make NFC tags and just launched an app to work with them. - IonSpin. "mementō is a simple and modern file management solution". They aren't fully launched yet I think. - AdCentral. An app for managing various kinds of ad campaigns in physical stores (if I understood correctly). There are others, that's just a subset of the ones we've asked for testimonials. There's also some open source projects that use it. So far there's definitely a theme of apps that do things with specialist hardware, which isn't a big surprise. We also dogfood it for systemd managed servers, but that's kinda experimental. I'm still trying to figure out if there's anything useful to do there, like to go from a build.gradle to a set of pushed/updated servers in one step without Docker (or maybe with Docker). Like it'd make a linked standalone app, upload the files to the server(s), integrate it with systemd then start the apps. But maybe nobody would find that useful. Server ops is such a heavily invested-in space already.
- mwcampbell 4y agoSpeaking of JVM 'plugins' that are loaded reflectively, don't forget the Java Access Bridge on Windows, as I reported to Hydraulic some months ago. (Fortunately, that one is fixed in Conveyor.)
- choeger 4y agoReflection or any other kind of dynamic execution (JNI?) will break this, no?
- silon42 4y agoCould be "fixed" by disabling reflection.
- islon 4y agoNo. 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.
- monocasa 4y agoBut I think the parent's point is given reflection, how can jlink statically know the complete set of classes that your program needs?
- geokon 4y agoIt'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)
- sam_lowry_ 4y agoIs there something that can do the tree-shaking of the Java program?
- geokon 4y agoI 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.
- pron 4y agoThese 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.
- oblio 4y agoHow is modularization progressing in practice? Stuff like Hibernate, Spring, the big libraries. How easy is it for the average greenfield enterprise to start as a modular app? How easy is it for the average brownfield enterprise app started 10 years ago to convert to a modular app?
- pron 4y agoFirst, just to repeat, you don't need to modularise your app to use jlink. You modularise to enjoy additional security and evolution benefits. > How easy is it for the average greenfield enterprise to start as a modular app? As easy as a non-modularised one. > How easy is it for the average brownfield enterprise app started 10 years ago to convert to a modular app? This one is harder to answer because you can only modularise (i.e. encapsulate in modules) code that is actually modular (cleanly separates API and implementation into different packages, no circular dependencies among components that are to become modules etc.). So it depends on how modular your codebase already is. In many situations it could require a not-insignificant refactoring, and so worth it if you really want the best security and encapsulation (which was the case for the JDK itself, which is now fully modularised). In other situations it could make sense to leave things alone and only modularise new project components (modules and code outside modules can mix). Either way, modular or not, using link to produce a custom runtime is a good idea!
- zmmmmm 4y agocurious if it also improves startup time? For me that would be more of a win than the size.
- mike_hearn 4y agoIt does a little bit, depending on how much of your app is modularized. The win isn't large. A bigger win is AppCDS but most apps don't use that due to workflow issues. It's usually about a 30% startup time improvement, in my experience. The biggest win is native-image but that's also the biggest compatibility hit. Jlink and AppCDS are compatible with all (bytecode based) JVM apps.
- captainmuon 4y agoIt's a pity that system wide JREs are not a thing anymore. I understand that it was difficult to get an updated JRE on the system in the early 2000s, but nowadays almost every computer is online - certainly one where you are currently downloading an application on - and automatic updates are commonplace. You used to be able to just double click on a .jar, and that was a Java application. And it would be trivial engineering-wise to include a little shim that downloads the JRE if neccessary, or the OS could do it itself. And why stop with Java? Why not have the OS detect the most common cases of "hey you are about to open <thing>" (where thing is a Python / Java / .NET app, or a document you can't open yet) "click here to install the needed bits and pieces from a trusted source, it will take 300 MB and 3 minutes". I think this is a case of perfect is the enemy of the good. This is a relatively simple addition that would make computers much nicer IMO.
- erik_seaberg 4y agoYeah, I have trouble believing a private runtime image per app isn't going to add up to more than one complete runtime image provided by the system package manager or shared Docker layer.
- kaba0 4y agoBut unfortunately the state would be having `n` complete runtime images for different versions. This is not a Java problem, Java just followed suit, and seemingly the preferred way of delivering executables is bundling everything.
- erik_seaberg 4y agoYeah, that’s not my preference, and I haven’t seen a team do it.
- toyg 4y agoWe already have that, it's called a package manager. The concept has a number of problems (multiple runtime versions for multiple programs, who pays for and controls the repository, etc etc).
- toyg 4y agoJLink is very cool, it's a shame it came too late to make a significant difference in the market.
- aphexairlines 4y agoYou don't need to run jlink yourself to avoid shipping the entire JDK. Here's a java19 runtime docker image at 62MB: https://hub.docker.com/_/eclipse-temurin/tags?page=1&name=19-jre-alpine https://hub.docker.com/_/eclipse-temurin/tags?page=1&name=19... Not customizing the runtime per build or per app also potentially helps reuse of docker image layers across your container registries and clusters.
- oblio 4y agoYou're adding the whole of Docker as a dependency if you do that. Not everyone wants that.
- KronisLV 4y agoI wonder how this would play out in a container context, aside from the benefits mentioned in regards to including only what you need and other things like that, but focusing purely on distribution sizes and space reuse. For example, if you would have your application built as a .jar (perhaps with an embedded app server), but use a separate full JDK install, then the latter could be cached on the nodes running the containers and re-used, which would be often if it doesn't change much. With this setup, deliveries would look a bit like the following: # SIZE WHAT 1 ~100 MB base OS image (reused if not changed, cached) 2 ~340 MB JDK image (reused if not changed, cached) 3 X MB the app .jar file (changes with every release) Whereas with the approach in the article, it would look a bit like the following: # SIZE WHAT 1 ~100 MB base OS image (reused if not changed, cached) 2 Y MB minimal JRE + the app (changes with every release) For example, consider the example in the article: 36M zulu-hello-jre-linux-x64 338M zulu19.30.11-ca-jdk19.0.1-linux_x64 While it's hard to speak of how any given app would look like if it was just a .jar (or what other real world examples would be like, not just a "Hello world" program), it would only take 10 individual releases to exceed the size of the JDK. In this case, if you deploy once every day, then by the end of the first month, you'd be using ~2X more space for the minimal JRE approach. However, if you would update your JDK version more than once a month, then that might as well go out of the window. So it appears that it all depends on how much you care about space in the first place, as well as what technologies you use, since the above example only seems relevant for container technologies that have the whole layer mechanism, and only then if you don't squash them for up front space savings for individual container images. On the other hand, if you update your base image and/or the runtime often, then using jlink makes a lot of sense, since the approach with the separate JDK would send the whole thing often anyways.
- chriswarbo 4y agoThose caring about space usage presumably wouldn't include an entire OS in their containers?
- KronisLV 4y agoHmm, it depends - sometimes that's a decent tradeoff for development velocity vs distroless containers (from scratch) outside of particular project requirements, in other cases it can just be nice to have some common packages or even tools in your containers, for debugging/troubleshooting, especially if you don't change the base image too often, so it can also benefit from the caching. As for the (compressed) size of some common base images: SIZE WHAT 3 MB alpine:3.17.1 29 MB ubuntu:jammy-20221130 31 MB debian:stable-20230109-slim 53 MB debian:stable-20230109 32 MB almalinux:9.1-minimal-20221201 66 MB almalinux:9.1-20221201 44 MB rockylinux:9.1.20221221-minimal 61 MB rockylinux:9.1.20221221 In most cases the OS/userland related layers (which will generally be more cut down than a "full" OS install) will be smaller than the runtimes for languages like Java, .NET, Python, Ruby, Node and so on, though things can get interesting with updates (e.g. slower builds if you cut out package cache in any layer where you need to install software, so it doesn't bloat the layer size, if you need more than one install command per container).
- synergy20 4y agoThis is awesome to know, I'm using Dart these days(java like language) that has a much smaller runtime(helloworld is 6MB), any one tried it with swing for GUI and see how large the final size is about.
- mike_hearn 4y agoMinimal Swing app is about 27mb compressed, 28mb with FlatLaf. That's for Windows. A bit smaller if you drop ECC and accessibility support. That 6mb is just for Dart right? I thought a minimal Flutter app is about the same size as a minimal Swing app.
- synergy20 4y agothanks,yes it is dart only size,need measure flutter size vs swing as a single exe
- mike_hearn 4y agoA couple of years ago I did some experiments to see how small you could make HotSpot and still have something usable. I got it down to a 7mb download but that required some tricks: - .tar.zstd - A custom build of HotSpot that compiled out some optional features. - It was just java.base, no swing You can get smaller if you do DCE / tree shaking more aggressively, or use a custom JVM designed for size. The smallest I've seen was Avian which can produce GUI apps that are 1mb standalone binaries. It's abandoned unfortunately.
- brabel 4y agoI also use Dart to create native binaries! Just because it's so easy. I have a real CLI app that does quite a lot of stuff, and it's less than 8MB. And runs really fast! At least around as fast as a Java native app compiled with native-image (GraalVM), but I hate native-image because it takes MINUTES to compile and sometimes does not behave exactly like the JVM-version (Dart doesn't even need to compile, you can run from source, and when you want to compile to binary it still takes just a few seconds).
- cozzyd 4y agoNot to be confused with the embedded tool from Segger.