Java Hot Code Reloader
Why Java needs a restart for every code change, how hot reloading tools work around it, and the approach behind the Java Hot Code Reloader.
- Updated
- 6 min read
By Szymon Kokot Published Updated 7 min read
As Java is an object oriented language, the topic of what classes actually are and how they are loaded by the JVM is one that really goes to the heart of things. In this article, we will explore in more detail not only what is visible to every Java developer, but also what is hidden under the hood of the JVM.
Let’s keep it relatively simple for a while, and let’s start with something more “tangible” for a regular developer like the class loaders. Class loaders, most often, are Java classes extending java.lang.ClassLoader, which are responsible for loading other classes. To be more precise, they load the bytecode of a class, then they leave it to the JVM to do the linking, basically reading and analyzing the bytecode to create a new class.
By default, a Java application has three main class loaders:
Bootstrap loader: The first class loader to run, it is used to get the absolute basic system loaded - essentially java.base. It is not a Java class, it’s written in native code and it doesn’t perform any checks on the classes it loads, so for security reasons it doesn’t have a representation in Java (as a class or class instance, for example), and for this reason it’s referred to as null.
Platform loader: When the bootstrap loader is done loading the core of the system, the platform loader loads the other platform modules that the application depends on. This class loader is a Java class, as we said earlier, extending java.lang.ClassLoader, and it’s accessible through a static function (ClassLoader.getPlatformClassLoader()).
Application loader: The last loader to run is the most widely used. It’s basically the one that loads the application itself. It is also an instance of a Java class, just like the platform loader, and it’s the one Java calls the system class loader: ClassLoader.getSystemClassLoader() returns it. It can also be replaced when starting the application. So if you want to run your app with a custom system class loader, you just need to add to your command the -Djava.system.class.loader=my.custom.ClassLoader flag.
Generally speaking, a class in Java can’t be loaded twice by the same class loader, but it can be loaded multiple times by different class loaders. This is one reason why there is a hierarchy of class loaders. And the second reason is delegation, whose purpose is precisely to avoid the same class being loaded twice. A properly implemented class loader always delegates that task to its parent (the class immediately above in the hierarchy) before attempting to load it by itself. So, let’s say we want the application class loader to load the class A.
The application loader delegates the task to the platform class loader.
The platform loader delegates the task to the bootstrap class loader.
The bootstrap class loader has no parent, so it tries to load A and if it is able to do so, it returns it to the platform loader.
When the platform loader receives A from its parent, it just passes it through to the application loader. If not, it tries to load A by itself and returns it to the application loader.
If the application loader receives class A from the platform loader, there is nothing more to do. If it doesn’t, then the application loader is the last one to give it a try.
Of course, you may add your own, totally custom class loaders. And although it would be a good thing if those followed delegation, placing themselves beneath the app loader, in some cases they are implemented to break that rule on purpose.
A class loader is said to be the defining loader of a class A, if A was actually loaded by that class loader. So if the app loader was the one that started the loading process, but due to delegation it was the bootstrap loader that actually found and loaded A, then the bootstrap loader is the defining loader of A.
A class loader is said to be the initiating loader of a class A, if the class loader is either the one that started the process that loaded A or is also the defining loader of A. This means a class A can have two initiating class loaders. In the previous example, the initiating class loaders would be both the app loader and the bootstrap loader.
This is quite important, we will talk about it later on.
Now that we know what class loaders do, let’s focus on what the JVM does. This is where the fun really begins. This part is maybe a little bit more abstract as I will try to explain the concepts without getting into the details of specific implementations. The JVM is responsible for making sure everything is secure and optimized so there are no errors and no unnecessary calls to the class loaders.
When a class loader finds the bytecode of the class to be loaded, the JVM takes over and starts creating its own representation of the class. This is referred to as linking and it is done in four steps:
Verification: First, the JVM needs to make sure that the bytecode loaded is a valid representation of a class and that its code is well behaved.
Preparation: Once the bytecode is verified, the JVM starts allocating and getting the static variables of the class making them ready to initialize.
Resolution: Now, before initializing the class, the JVM tries to resolve the supertype and implemented interfaces, if any, of the class being linked. If the JVM is not able to resolve them, it first tries to load and link them before continuing.
Initialization: In this step, it’s the first time the bytecode of the class is actually being executed. In this step, all static fields are initialized as well as any static initialization blocks are run. When this is done, the class is ready to be instantiated.
The representation of a class used by the JVM is the “klass” and it is not a Java class, they are stored in the metaspace. Each klass is unique, and all instances of the class it represents are created based on that klass. Some of the information it contains is accessible from Java through the java.lang.Class, whose instances are Java representations of different classes. This is what makes reflection possible, but that is a topic for a different article.
In the metaspace, klasses are not just stored in any way, they are organized. Each class loader has a dictionary assigned to it. Each klass is stored in the dictionary of its initiating loader. And it’s also important to point out that each klass contains information about its defining loader.
Let’s consider a class A, which has been defined by the app class loader. And let’s also consider a class B that is being referenced from A.
When B is referenced while executing A, the JVM finds out from A’s klass which is its defining class loader, in our example it’s the app loader.
The JVM now checks in the app loader’s dictionary if it contains B. Which we could also express as, the JVM checks if the class loader already is an initiating loader for B. If it is, the JVM grabs the B klass and continues executing A.
But if the app loader is not an initiating loader of B, the JVM asks the app loader to load B.
Now all delegation steps explained before follow.
When B gets loaded, its klass representation gets stored inside its initiating class loaders’ dictionaries for future reference.
Klasses are the representation of Java classes in the JVM. They are not only identified by their name, but also by their defining class loader. This is the reason why a class can be loaded multiple times by different class loaders and get treated as completely different classes. Which is the reason why delegation and the class loader hierarchy are needed. Klasses are stored in the metaspace in a dictionary assigned to their initiating loaders, which helps the JVM to improve performance by not having to call the class loader each time some class gets referenced.