♾️ Live Reloading on JVM

This post is also available in Russian.

Here, I want to summarize everything I know about hot and live reloading on the JVM and then show how I ended up implementing a universal live-reloading solution for any web application on the JVM.

In short, in this article:

  • We’ll try to answer what types of reloading we know.
  • Then we’ll take an extended overview of existing reloading solutions on the JVM.
  • Finally, we’ll look into the details of implementing the universal framework-agnostic live-reloading solution.

TL;DR: See ♾️ seroperson/jvm-live-reload repository.

Introduction

I’ve been missing runtime reloading every time I code in Scala. If we’re talking about typelevel / ZIO stacks, it doesn’t exist there at all. The only framework that has it in the Scala ecosystem is Play; everything else is more related to Java or Kotlin.

But the situation isn’t much better for either Java or Kotlin if you’re not using major web frameworks but some small web libraries. Although there are some solutions, all of them have significant cons.

But before diving in, let’s define what exactly we’re talking about. As many of you probably know, reloading is a mechanism that swaps code at runtime and allows your application to run with new code without explicitly stopping and then starting the whole process. There are numerous ways to do it, so let’s enumerate them. We won’t cover such horrible things as reloading an application running in production, although some solutions I’ll describe next are probably suitable for this.

So, definitions. Actually, there are no strong definitions of existing types of code reloading, but from the information on the internet, we can define the following types of runtime reloading:

  • Hot Module Replacement (HMR) - replacing some specific modules of an application at runtime. It means that some specific module unloads and then loads into a running application with new code. That’s what OSGi is.
  • Live Reloading - patching runtime with the whole new application code with a “partial” restart. In the JVM world, it would mean that during reloading, we will only re-create the application classloader without touching the JVM itself and system classes.
  • Hot Reloading - patching only changed pieces of code without any restart. This is the most advanced technique, and it almost has no suitable implementations on the JVM (we’ll talk about it in detail later). It means that we’ll only upload changed bytecode directly to a runtime, and no classloader magic is necessary.

Do we really need reloading in development mode?

It sounds crazy, but I really saw people saying things like “incremental compiler is fast enough,” “just split your code better,” “if something compiles too long, your project is misconfigured,” “just start build in continuous mode.” Although such people exist, there are also many people who really miss the reloading feature in modern web server solutions:

Besides people’s wondering, many major web frameworks (like Spring, Play, Quarkus, and others) already support reloading out-of-the-box, and it once more proves that any sane reloading is faster than nothing.

Which reloading solutions are available on the JVM?

But which solutions do we have right now in the JVM world (and Scala specifically)? Let’s sum up everything I found during my research and discuss every approach. You can skip the speech and jump to the summary.

sbt-revolver and “Triggered execution”

When it comes to hot-reloading discussions in Scala, sbt-revolver is the first thing everybody mentions. However, it’s not even any reloading at all. What this plugin does is watch for your sources and restart the whole application process when they change. I mean, it just stops the JVM and starts it again. No runtime patching is involved.

Here also comes the built-in ~ sbt feature (so-called Triggered execution) and other analogues, which do completely the same.

It’s completely replaceable with a simple shell script, I guess, and in practice, it just wastes your CPU resources continuously recompiling and restarting after every change. It’s rarely usable, and that’s definitely not what we’re looking for.

OSGi

OSGi is the thing that’s also mentioned quite often in hot-reloading discussions. It allows you to use the HMR approach, but your application has to be written in a very specific way using the OSGi framework, which is not what we’re looking for, especially if we want it only for development purposes.

Java Instrumentation API

Java Instrumentation API allows you to attach some “observer” to an application “for the purpose of gathering data to be utilized by tools”. This API also contains methods for hot-reloading, but they are limited only to method bodies, which makes it completely unsuitable for our purposes (quote from javadoc):

The redefinition may change method bodies, the constant pool, and attributes. The redefinition must not add, remove, or rename fields or methods, change the signatures of methods, or change inheritance.

There are more APIs with similar functionality (JDI, JVMTI), but all of them share the same limitations. Actually they look like the same API, and probably they really are. In short, these APIs provide true hot-reloading, but they’re limited to patching method bodies, so they’re not usable too much in practice.

Custom Virtual Machine

The Dynamic Code Evolution Virtual Machine (DCEVM) is a modification of the Java HotSpot VM that allows unlimited redefinition of loaded classes at runtime (in other words, implements hot-reloading). The initial project is dead, but it was implemented as part of other projects like JetBrains Runtime. There is also a HotswapAgent project, which enables you to actually use it hassle-free (almost).

This technology allows you to get rid of previously mentioned limitations and swap basically everything, but it too has some drawbacks:

  • The most noticeable one is that you actually have to run some specific JVM: JBR (Java 21, Java 17), TravaJDK (Java 11), DCEVM (Java 8).
  • Configuration may be tricky, and you’ll probably have to implement custom plugins to make it work with your stack.

It’s worth noting another solution, Espresso VM, which has the same capabilities as DCEVM and still the same drawback, as it requires you to run a specific VM. However, it also has some additional pros, which come from its tight integration with the Graal ecosystem.

It sounds very promising and may be the best choice, but sticking to a specific JVM implementation isn’t what we’re looking for.

JRebel

There is also a commercial solution JRebel, which doesn’t require you to run a specific VM and still provides true hot-reloading. The information on how it works exactly is limited; everything that’s said is that it uses the Java Instrumentation API, but how exactly they bypass restrictions isn’t clear. There are some assumptions, but anyway, this solution is paid, so that’s not what we’re looking for.

Dynamic Proxy API

In short, this API allows you to intercept class interactions and wrap them with custom logic. Technically, we can watch for file changes to recompile it and then implement a proxy, which changes its destination every time a file changes and a new loaded class is available. That’s how the scf37/hottie library works.

Sounds like a good idea, but it has a very limited number of applications as it isn’t scalable on the whole codebase and forbids complex changes.

In a nutshell, that’s the same “classloaders” approach.

Using separate classloaders

This approach is the most common in the JVM world, and actually, that’s what we call live reloading. The idea is that you have two classloaders:

  • The unreloadable one, which contains third-party jars and everything else you can’t reload.
  • And reloadable, which contains your frequently-changed code, usually all the code you have in your /src directory.

When a code change occurs, an application stops, the “reloadable” classloader is thrown away, and an application starts again with a new classloader created using changed sources. That’s the way Play, Spring Boot, Quarkus, Apache Tapestry reloaders work.

It’s a relatively simple yet working solution. It has some drawbacks, but usually, you won’t even notice them if you code using mentioned above frameworks:

  • The reloadable classloader could be big, so it might not be as fast as more advanced solutions.
  • When you drop a classloader, the old instances of changed code are still in charge, and they won’t be reloaded. If we’re talking about Scala, probably it won’t be a big problem as everything is usually stateless, but there are still such things as connection pools that won’t go anywhere. If your application isn’t completely stateless, memory leaks are possible, so you must take special care of stopping and cleaning existing resources to not allow leaks and undefined behavior.
  • It can conflict with libraries that mess with classloaders.
  • It’s only available as part of a specific framework.

It could be our choice, if we hadn’t to stick to a specific web framework (spoiler: we don’t have to anymore).

BLUF

Well, if we drop the duplicates and non-reloading solutions, all we have is:

  • Hot-Reloading using the Java Instrumentation API, but it restricts you from changing the class schema.
  • Hot-Reloading using a custom VM, but it sticks you to a specific VM.
  • Hot Module Replacement using OSGi, but it sticks you to OSGi.
  • Hot-Reloading using JRebel, but it’s a commercial solution.
  • Live-Reloading using several classloaders, but it’s (yet) only available as part of specific web frameworks.

In short, there is no good solution 🤡

Implementing an universal solution

That’s when the idea of a universal framework-agnostic reloader was born, and when the ♾️ seroperson/jvm-live-reload project was started. To try it right now, you can jump right to the Installation section in the repository.

Preview

This project implements a Play-like approach, which we know as the “ClassLoader replacement” approach from the categorization above. For those who don’t know Play, here’s how it works in detail:

  • The user sets up a plugin for the build system (gradle, sbt, and others). A plugin implements all the reloading logic and provides the communication bridge between your application and the build system.
  • The user starts an application in development mode.
  • When a request arrives at a Play web server, it asks the build system whether there were any new code changes.
  • If there were any changes, then the build system compiles them, and a plugin reloads application classloaders.
  • If not, everything proceeds as always.

So, you code as much as you want, then do a curl request, and, while request is waiting, everything compiles + reloads, and then responses using a new code. No CPU waste, no JVM restarts, and everything is fast enough.

And nothing is stopping us from making this solution universal. Everything we needed is:

  • Re-implement “application -> build system” communication logic for each build system.
  • Implement a web proxy, which will stand in front of our application to decide whether to reload it or not. It allows us to leave application code almost untouched, so you can just apply the plugin and nothing more.
  • Also, we need to be able to start and stop the application programmatically and be able to release all its resources. There are no problems with starting, as basically, it’s the static void main call, but stopping and resource cleanup could be tricky to implement universally. We’ll discuss it further.

Everything was implemented for Gradle, SBT, and mill build systems, and it even seems to be working. Let’s see which difficulties I encountered and how they were solved.

Necessary tradeoffs

I wanted this to be a solution that doesn’t require you to make any additional changes to an application’s code - just apply the plugin and go. Unfortunately, it didn’t happen, but still, you won’t be forced to change a lot of things to make this work.

Here is the list of necessary changes you need to make your application live-reloading ready:

  • Implement a /health endpoint. It must respond successfully when your web application is ready to receive requests. It’s necessary because without it, we can’t know when an application is actually started and running.
  • Your application must be interruptible. Read the article ⏹️ Making your JVM application interruptible for more details. Shortly, your static void main must handle InterruptedException by stopping your application and releasing all its’ resources. It’s required to be able to programmatically stop your application and ensure all its resources are cleaned.
  • Your static void main method must only finish when your application is completely stopped. When the method exits, we consider that the application is stopped, and we can start a new instance.

Major web frameworks with live-reloading features have full control over an application’s lifecycle, but our universal solution doesn’t have such information, so we need all these requirements to be followed.

Future plans

I hope this project will find its place and help users improve their development experience. It’s in the alpha stage right now, so the very first milestone is stabilization, and then, based on feedback, we’ll form a roadmap with necessary missing features, which will be implemented until version 1.0. Honestly, I have no idea what important features are missing right now, so I’m all yours.

In the future, I also want to look into researching a true JRebel-like Hot Reloading solution, which seems pretty challenging but still possible (in the end, the JRebel guys did it).

Conclusion

Thank you for reading this little article, I hope it will be useful to someone. Feel free to reach me if you have something to say.

And also take a look on the posts on similar topics: