4 free ways to hot-swap code on the JVM

Post date: Jan 21, 2017 5:49:42 PM

The JVM is a wonderful piece of engineering with a lot of great features. One feature that many developers don't seem to realize exists is that the JVM (I am referring to Oracle's HotSpot, but this applies to others as well) is capable of hot-swapping code. In plain English, you can load changed code into a running JVM and immediately see the effects of your changes.

In this blog post, I will present 4 different ways of doing this, none of which requires paying for a product or license, starting from the rawest, but simplest one, and moving towards more complex (but not necessarily harder-to-use) alternatives.

Unfortunately the simplest way is also the least powerful! Depending on your circumstances, however, it may be enough. But if you need more power, you can try one of the other, more complex solutions.

So let's get to it without further ado.

1. Using jdb (the Java command-line Debugger)

Basically, any Java debugger can hotswap code as this is a feature of the JVM itself. But before we get too serious, let's do it in the simplest possible manner so we understand what's really going on: we'll use the jdb tool, a Java command-line debugger from Oracle.

We'll need 3 command-line terminals open: one to edit and compile our Java file (Source Terminal), one to run it (App Terminal), and one to run the debugger (Debugger Terminal).

On the first terminal, using your favourite text editor, create a simple Java class like this one (this is just the main function that will simulate a long-running application):

Source Terminal:

import java.io.IOException;

public class Example {

   public static void main(String[] args) throws IOException {

       System.out.println("Hit enter to print a report, x to exit.");

       byte[] input = new byte[1];

       while (System.in.read(input) > 0) {

           if (input[0] == 'x') {

               break;

           }

           System.out.println(new Messenger().getMessage());

       }

   }

}

Save this file as Example.java, then create another class, the one we will be modifying, called Messenger.java:

 class Messenger {

   String getMessage() {

       return "Hello world!";

   }

}

Once you're done typing it (or just pasting), save the file and, from the same directory, compile the program with the following command:

App Terminal:

javac -g *.java

The -g option is used to generate debugging information so we can see useful data in the debugger.

Now, in the App Terminal, just run the Example class we've just compiled:

App Terminal:

java -agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=n Example

You should see the message we printed from Java, and now the application is waiting for user input to print out what the Messenger class returns. By hitting enter, you should see the expected Hello World! message.

Nice, now that we have an application to debug, let's start the debugger. If you look closely at the command above, you'll see that we started the debugger agent in the dt_socket port 8000. To attach the debugger to that port, just run this command on the Debugger Terminal:

Debugger Terminal:

jdb -attach 8000

You should see the Initializing jdb... message after it prints some less useful stuff. And this means we can now start debugging!

Before we continue, just for a bit of fun, let's play with the jdb debugger to see what it can do (and make sure it is working)!

To add a breakpoint on line 9 (where we check the user input), enter this command:

> stop at Example:9

Now, from the App Terminal, just hit Enter once to trigger this breakpoint. You should see that the breakpoint has been hit.

You can type some commands to inspect the current state of the application.

For example, to see the value of the input variable at that point, run the command dump input.

Here's an example iteration:

 > stop at Example:9

 Set breakpoint Example:9

 > 

 Breakpoint hit: "thread=main", Example.main(), line=9 bci=22

 9                if (input[0] == 'x') {

 main[1] dump input

  input = {

 10

 }

 main[1] step

 > 

 Step completed: "thread=main", Example.main(), line=12 bci=33

 12                System.out.println("Hello world!");

 main[1] cont

 > 

 Breakpoint hit: "thread=main", Example.main(), line=9 bci=22

 9                if (input[0] == 'x') {

 main[1] dump input

  input = {

 120

 }

 main[1] cont

 > 

 The application exited

To see the full list of commands jdb supports, type help.

Alright, now that we had enough fun with jdb, let's try to make some changes to our Example class, then reload it without stopping the program.

Make sure the application is running (if you killed it like I did above) and the debugger is attached before continuing.

In the Source Terminal, just change the message that we print out on each iteration to something different like "Oh my! No restart!!".

Save the file, then, from the App terminal, compile the app again:

App Terminal:

javac -g *.java

Now, tell the debugger you want to redefine the Messenger class:

Debugger Terminal:

> redefine Messenger Messenger.class

The first argument is the fully-qualified class name, the second is the location of the new class file (relative to the working directory).

Now, in the App Terminal, hit Enter to see what message the Messenger gives. If everything worked as it should, you will see the new message now!

Alright, this is all wonderful, but of course, in a real setting you'll be using more than the command line to manage your projects. Let's see what a decent IDE, such as IntelliJ, can do for us.

2. Using IntelliJ IDEA's debugger

With IntelliJ, things get a lot easier. So easy that it looks like magic! But I hope the previous session was enough to show that there's no magic, just great software under the hoods of the JVM!

To start using IntelliJ to debug and hotswap code for you, if you don't already have one, create a Java project (using Maven/Gradle or just pure Java) first. Create the same Java files as in the previous sections, so you can run them from IntelliJ in debug mode by right-clicking on the play button shown next to the main method or class, as shown below:

The app will start running in the Terminal window of the IDE. Hit Enter to confirm that the original message is being printed. Now, change the Messenger class as in the previous section, so that it returns a different message.

With the application still running, re-compile the Messenger class by pressing Cmd+F9 on Mac, or (probably) Ctrl+Shift+F9 on Windows/Linux. This can also be one from the menu bar under Run > Reload Changed Classes.

You should see a message popup confirming that the debugger reloaded 1 class. If you hit Enter again in the app terminal, you should see that you get the new message now, proving that the code was modified on the run.

Notice that with IntelliJ, you can also easily attach the debugger to a remote JVM, so it is not necessary to start the app from IntelliJ for this to work.

Both approaches explained so far use the JVM itself to do the class reloading, which is quite limited (it basically only works if you redefine the body of existing methods or the value of fields, including final fields, but not if you modify method signatures or add/remove methods, for example) as explained in detail on this Zeroturnaround blog post about reloading classes by the makers of JRebel (a paid tool that attempts to get over most of these limitations).

But there are other ways to achieve hot swapping which are based on improving the JVM itself and dynamic classloaders.

The next sections will discuss these other options.

3. DCEVM (JVM enhancement)

The DCEVM (Dynamic Code Evolution VM) is, according to their website:

... a modification of the Java HotSpot(TM) VM that allows unlimited redefinition of loaded classes at runtime.

The original project was forked and is currently maintained at https://github.com/dcevm/dcevm. I got the latest update (patch on Java 8 update 112 as of writing) from https://dcevm.github.io/.

Download the jar and run it with sudo (in Linux/Mac) or the equivalent in Windows, otherwise you won't be able to alter the JVM installation (please do it at your own risk, though I personally feel the DCEVM project can be trusted).

When installing it, you can choose to replace your JVM with DCEVM or use it only as an alternative. I chose the latter to be on the safe side.

When you install it as an alternative, you need to give the following option to the java process when starting your application to activate DCEVM:

 -XXaltjvm=dcevm

I tried it using the IntelliJ debugger as explained in the previous section and it worked amazingly well (it seems that Eclipse supports using it also). JetBrains seems to agree that DCEVM is safe to use and there's even a plugin for it (though I couldn't get it to work myself, it complained about server errors, but notice that the plugin is not needed anyway for this to work).

I was able to modify method signatures, add new methods, change existing class hierarchies, all of which would have failed with the previous solutions but worked flawlessly with DCEVM.

Even changes to generic types, which I had trouble hot-swapping when using JRebel, worked perfectly.

Although I have very little experience using DCEVM, it seems incredibly promising and I will continue following the project progress and keep this blog post updated regarding any findings (positive or otherwise) that I might make with further usage (hopefully at my workplace, if the guys agree to try it out there!).

4. OSGi + Gradle osgi-run plugin + Apache Felix File Install

This is a very different approach from the previous ones in that it does not require a debugger to be attached to the process in order for it to work at all.

It leverages OSGi, an old and battle-tested technology which is still around, innovating in 2017, when it is turning 18!

Though being arguably an elderly in software terms, OSGi is still miles ahead of similar technologies that offer modularity and dynamism... and being able to hot-swap code in OSGi has been a reality for decades, literally.

The catch is, of course, that it's not just any application can run in OSGi. The constraints OSGi imposes on the design of applications can be too heavy for many developers, and that's why it has never become nearly as popular as it deserves.

And that's where the other pieces of the puzzle kick in.

Gradle, together with the osgi-run plugin, (which was written by myself, by the way!) makes it quite trivial to run parts of an application under OSGi, while other parts can remain under the plain Java runtime. The parts that run under OSGi can be freely removed, modified, and added back to the system at any time.

The final component we will use is a simple bundle from the Apache Felix project (one of the main implementations of the OSGi specifications) called File Install, which can watch a directory containing jars, and when any jar is added/updated/removed from the directory, it can add/update/remove the respective bundle from the running OSGi system.

Using FileInstall is just a convenience, so we can re-build our jars and have them automatically updated in the running application. We could achieve this by simply using an OSGi CLI (command-line interface), as described in the osgi-run Tutorial, but for this post, we'll go with FileInstall because our example application is itself a CLI!

Here's how it works...

To start using the osgi-run plugin to run your application with OSGi you'll need to do the following:

4.1 Changing the application's entry point to be OSGi-friendly

To change your application's entry point may or may not be easy. It really will depend on how you bootstrap it.

If you can break the bootstrapper up so that it can be started either from the main method or from a separate start() method that OSGi can call, then it will be no problem.

In our example application, that's not very difficult as there's no command-line arguments parsing or other obstacles.

The easiest way to bootstrap an OSGi application is to use Declarative Services annotations.

You'll need to add the following dependency to your bootstrapper module (or create a new, OSGi-specific one):

compile "org.osgi:org.osgi.service.component.annotations:1.3.0"

So, whereas a normal Java application bootstrapper looks like this:

public class Example {

   public static void main(String[] args) {

   }

}

An OSGi bootstrapper looks like this:

import org.osgi.service.component.annotations.Activate;

import org.osgi.service.component.annotations.Component;

import org.osgi.service.component.annotations.Deactivate;

@Component(immediate = true)

public class Example {

   @Activate

   public void start() {

   }

   @Deactivate

   public void stop() {

   }

}

The @Activate method is called when the bundle is started (so we need to have one, otherwise our bundle will not do anything at all). The @Deactivate method is optional, but because bundles may start/stop at any time in an OSGi environment, we should always have one to at least attempt to stop the application cleanly.

Because our example application is a blocking CLI, we'll have the added complication of not being able to start/stop it very easily. An important rule with a declarative service Component is that you should not hang on the @Activate and @Deactivate methods as that would block the framework, so we'll need to manage a CLI Thread and allow it to be stopped.

Knowing the particulars of what is required for our application to allow the start/stop methods to be implemented correctly, without blocking the framework Thread, we know exactly what will be required for our application to run in OSGi. If this turns out to be too difficult in your application, then this option is definitely not for you, otherwise, read on.

In our Example application, the first thing to do is to create an alternative method of reading user input that is not blocking, so that we can easily interrupt it at any time.

I suggest this alternative implementation (without worrying about OSGi just yet):

public class Example {

   private final AtomicBoolean running = new AtomicBoolean(false);

   private void runCli() {

       System.out.println("Hit enter to print a report, x to exit!");

       running.set(true);

       byte[] input = new byte[1];

       try {

           while (running.get()) {

               int availableBytes = System.in.available();

               boolean gotInput = availableBytes > 0 &&

                       System.in.read(input, 0, Math.min(1, availableBytes)) > 0;

               if (gotInput) {

                   if (input[0] == 'x' || !running.get()) {

                       break;

                   }

                   System.out.println(new Messenger().getMessage());

               } else {

                   Thread.sleep(500L);

               }

           }

       } catch (InterruptedException e) {

           System.out.println("CLI interrupted");

       } catch (IOException e) {

           e.printStackTrace();

       }

       System.out.println("CLI Dying");

   }

   public static void main(String[] args) {

       Example example = new Example();

       example.runCli();

   }

}

This implementation is an improvement over the previous, simpler one, in that it makes it possible to stop the CLI cleanly (because it never blocks on read). It also makes main really simple, so implementing another start method is just as trivial.

Now that our application can easily be started and stopped, we can implement the start/stop methods that OSGi can call:

@Component(immediate = true)

public class Example {

   private final AtomicBoolean running = new AtomicBoolean(false);

   private final Thread thread;

   public Example() {

       thread = new Thread(this::runCli);

   }

   @Activate

   public void start() {

       thread.start();

   }

   @Deactivate

   public void stop() {

       System.out.println("Stopping CLI");

       running.set(false);

       thread.interrupt();

   }

   private void runCli() {

       // implementation is identical to the previous one

   }

   public static void main(String[] args) {

       Example example = new Example();

       example.runCli();

   }

}

We kept main so that our application now might run both in OSGi and in the usual Java way.

When OSGi restarts a Component, by default, it creates a new instance of it, so we don't need to worry about start being called more than once.

4.2 Assemble the application runtime environment with the Gradle osgi-run plugin

If you have a Gradle project already, this is easy. Just apply the osgi-run plugin to the build and declare which modules are part of the runtime.

For example, if your project is not a multi-module one, add these declarations to the build:

plugins {

   id 'java'

   id 'com.athaydes.osgi-run' version '1.5.2'

   id "org.dm.bundle" version "0.8.4"

}

repositories {

   jcenter()

}

dependencies {

   compile "org.osgi:org.osgi.service.component.annotations:1.3.0"

   runtime 'org.apache.felix:org.apache.felix.scr:2.0.2', {

       transitive = false

   }

}

runOsgi {

   bundles = [ project ]

}

bundle {

   instruction '-dsannotations', '*'

}

The runtime dependency on Felix SCR means that a Declarative Services implementation present in the OSGi environment.

When you run gradle createOsgi in a terminal, osgi-run will create an OSGi environment containing your project and all its dependencies. You can immediately run it with:

bash build/osgi/run.sh # or build\osgi\run.bat in Windows

To be able to hot swap the project jar, what we have to do now is add Felix FileInstall to the environment, as well as create a hot-swap directory for it to watch, where we're going to place our project's  jar.

So, first add this dependency:

runtime 'org.apache.felix:org.apache.felix.fileinstall:3.5.8'

Then, we need to change slightly the environment created by osgi-run so that only the jars we want to hot-swap are moved from the default bundles directory (which is build/osgi/bundle) to the new hot-swap directory.

The following declarations will achieve that:

final hotSwapDir = file('build/osgi/hot-swap')

runOsgi {

   bundles = [project]

   config += ['felix.fileinstall.dir'           : hotSwapDir.absolutePath,

              'felix.fileinstall.noInitialDelay': true]

}

tasks.createOsgiRuntime.doLast {

   hotSwapDir.mkdirs()

   file("build/osgi/bundle/java-tests.jar")

           .renameTo(new File(hotSwapDir, "java-tests.jar"))

}

Re-build the environment with gradle clean createOsgi, then from one shell, start the environment and, when the application prints its message, hit Enter to confirm the code is running as expected.

From another shell, build the project again with gradle createOsgi after making some code changes. After a few seconds, you should see the CLI is re-started and the new code is running.

And that's it!

Notice that you could use this approach even if you use Maven or other build system than Gradle. The osgi-run build could be used as a simple deployment script, while your project is kept separate as a Gradle/Maven/Ant project. All you need to do is use Maven local as a repository, so that when you install your project's artifacts in the local Maven repo, the osgi-run build can re-create the environment with the new jars, which will be picked up from the hot-swap directory if you place them there. 

If you have any dependencies that do not run well in OSGi, declare it as a systemLib dependency. See the osgi-run tutorial and documentation for more details!

This approach, although much more complex than the previous ones, has a lot of advantages.

Final remarks

I hope that one of the alternatives exposed in this blog post can help you get the productivity improvements hot-swapping offers into your work flow. Once you experience these improvements, you can't go back!

I must mention that this list is by no means exhaustive... as pointed out a couple of times above, there's also at least one paid solution, JRebel, and via the Java Instrumentation API, you could even roll out yourself some custom code reloading!

The possibilities are endless!