devxlogo

Minimize Java Distributions with an Automated, Custom JAR File

Minimize Java Distributions with an Automated, Custom JAR File

his article presents a solution to a problem that you might not have known you had?the size of your Java code.

Java programs can run the gamut from very small to very large. In fact, the variance is much larger in Java than in any other language. This is partly because the Java runtime (JRE) contains a great deal of library code that doesn’t need to be included in an application, allowing applications to be smaller than they otherwise would.

However, if you’ve ever worked on a suite of Java programs?especially one that used a set of evolving libraries?you know that Java programs can grow extremely large. Java encourages modular design and code reuse, and it’s not uncommon to find yourself including code you’ve never seen before into a bundled application, simply because it’s part of your company’s library set.

In such cases, you might be tempted to remove some of the dead code, but this isn’t something you want to do by hand, and the larger and more complex the program, the more reluctant you should be to attempt this.

Classloader to the Rescue
Fortunately, Java provides a solution. You can use the power of Java Classloaders to track the code that your application uses. More specifically, you can get a very precise idea of which classes you are using and which you aren’t.

In this article, I’ll develop a utility that will run a program under a monitor. This monitor will track the classes that are loaded, and when the program is done, it will create a JAR file containing only the classes you need. I call this program ‘JarMaker.’ Along the way, you’ll learn a lot about the Java Classloader. I’ll start there, with the basics of the Java Classloader.

The Classloader Concept
Java programs are made of individual components called classes. From the point of view of the everyday programmer, a class is a collection of Java code?class declarations, methods, and so on. This is the language-oriented view of a class.

At the same time, a class is a collection of bytes stored on disk somewhere, which must be loaded before it can be run. This is the runtime-oriented view of a class.

A Classloader is an object that converts a class from a collection of bytes into a Java class and is generally responsible for reading those bytes from a disk or some other permanent storage medium. A Classloader, thus, crosses the boundary between the language-oriented view and the runtime-oriented view of what a class is. Working with Classloaders requires you to understand the guts of the runtime environment, as well as other issues that you generally don’t have to deal with.

The System Classloader
Fortunately, you don’t normally have to worry about Classloaders. When you run your Java program, the JRE automatically creates a Classloader and uses it to load all the classes from disk. Or in the case of an applet, the browser’s JRE automatically creates a Classloader that loads the classes over the network. You (and your program) are none the wiser.

The default Classloader that the JRE provides is called the system Classloader, and its job is to load classes from disk, searching directories and JAR files in the user’s path to find the necessary classes.

Custom Classloaders
Just as a browser has a special Classloader that loads classes from the network, you can create a custom Classloader to deal with your own special circumstances. For example, we want to load classes from disk as usual, but we want to track which classes are loaded while we do that. For that, we need a custom Classloader.

A Classloader is actually pretty simple to understand. When you create a custom Classloader, you create a class that extends Classloader and you implement one method:

public class JMClassLoader    extends ClassLoader {  public Class loadClass(    String name,    boolean resolve ) {    // ...  }}

The loadClass() method has the burden of loading a class. The system passes loadClass() the name of the class, and loadClass() must return a Class object.

Any Classloader?regular or custom?is used by first asking it for a class, like this:

Class theClass =  Class.forName( name, true, jmcl );

Class.forName() is the standard method for loading a class, but we’re using the version that takes a Classloader as an argument. This way, you tell the system that you want to load the class using a particular Classloader.

Once you’ve done that, if this class requires any other classes, it uses the same Classloader to load them. If you load the first class in an application using a custom Classloader, then the entire application will loaded through that Classloader.

Our Custom Classloader
The custom Classloader for this project is called JMClassLoader, where JM stands for ‘JarMaker.’ Remember, you want to create a ClassLoader that tracks the classes that are loaded and creates a JAR file containing only those classes.

Listing 1 contains the code for JMClassLoader.loadClass(). The line-by-line comments explain how it works.

The most important thing to understand about this Classloader is that it doesn’t do anything strange in the actual loading of the classes. To load a class, it simply looks in the current directory (or in subdirectories of the current directory). What makes this Classloader different is that, after it loads the class, it remembers that it loaded it by storing a reference to it in a collection called loadedClasses. Later, it uses this set to generate the JAR file.

A Note About ClassLoaders
It used to be that the correct method of implementing a custom Classloader was to override loadClass(). But these days, the correct way is to implement findClass(), which uses the newer ‘delegation’ model of Classloader customization. JMClassLoader uses the older, loadClass() method, because JarMaker has special needs. In particular, we don’t want to use the default implementation of loadClass(), because this would make it impossible for us to track which classes were loaded. To understand this, we need to look at how loadClass() works.

In the base class, loadClass() does a number of things. It checks a cache to see if it has already loaded the class, and if it hasn’t, then it checks the so-called ‘parent’ Classloader. If it still hasn’t found the class, it calls the findClass() method. Finally, it calls resolveClass(), which links the class with other classes that it requires.

If you don’t want to implement all these steps, you can choose to override the findClass() method rather than the loadClass() method. This way, the default implementation of loadClass() will take care of these details for you.

In fact, using findClass() is the preferred method of implementing a custom Classloader, since it’s more likely that your Classloader will behave as expected. However, you must overload loadClass(), because The default implementation does something which we dont want it to do?it loads classes from the filesystem.

The default implementation of loadClass() would call the system Classloader, which would find the class and return it. It would never call the findClass(), and therefore you wouldn’t get to record it. Thus, loadClass() is bad for my purposes, since I want to make sure that JMClassLoader loads the class.

Furthermore, if my implementation of loadClass() were to simply call the system Classloader itself, the system Classloader would find the class. I would record this class, which is good, but the class would be marked as having been loaded by the system Classloader, and further classes would be loaded by the system Classloader as well. As a result, only a single class would be loaded and I would not be able to build the JAR file.

So you have to load the class yourself, directly from the filesystem. For the sake of simplicity, the implementation provided in the source code (see link in left column) only looks in the current directory (and subdirectories thereof), but it could easily be extended to look in the classpath for other places to find classes.

Calling the Classloader
Now that the Classloader is in place, you need to learn to use it. The ‘JarMaker’ class is a program that will run another program through JMClassloader.

To see how JMClassloader works, you need to load a program through it. This article includes a sample program called ‘CommandLine,’ which you can use to test JarMaker. CommandLine is a simple Java shell which allows you to call methods in other classes by typing them in at the command line. Run it with the command:

% java Test

You’ll see a command prompt, at which point you can enter a Java class name, method name, and arguments. For example:

$ commandline.calculator.Calculator add 3.4 4.5

This will load up a class called commandline.calculator.Calculator (also included with this article) and call its ‘add’ method with the arguments ‘3.4’ and ‘4.5.’ Calculator prints the result:

7.9

CommandLine is a great way to test the Classloader because it loads classes dynamically. The plan is to make a JAR file containing only the classes that needed to add 3.4 and 4.5.

% java JarMaker my.jar Test

Instead of running Test, I run JarMaker, passing the class name ‘Test’ as the second argument. The first argument, of course, is the name of the JAR file that I want to create. Entering the command runs the program as before, and, just as before, I need to enter the additional command:

$ commandline.calculator.Calculator add 3.4 4.5 7.9

This time, however, when I try to quit the program, by any means, it dumps a JAR file containing the necessary classes and lists them.

Starting dump.adding commandline/CommandLineException.classadding Test.classadding commandline/CommandLine.classadding commandline/calculator/Calculator.classDump done.

Now, I have a fresh new JAR file called ‘my.jar’ containing the listed classes.

Shutdown Hooks
There’s just one more piece of the puzzle to look at. How did I create the JAR file after the program had run? I installed a ‘shutdown hook’. A shutdown hook is something that you run when the program is finished. You do this by creating?but not starting?a thread, and passing that thread to the Runtime.addShutDownHook() method, as follows:

Thread thread = new Thread( this );Runtime.getRuntime().addShutdownHook(   thread );

Take a look at the source code for ‘JarMaker’ in Listing 2. The thread’s run() method takes care of dumping the JAR file. This hook is guaranteed to run when the Java Virtual Machine (JVM) starts to shutdown, even when the shutdown is caused by an abrupt termination. Also be sure to have a look at the utilities in Listing 3 and the command line test in Listing 4.

Extending Your JarMaker
The pieces are now all in place. JarMaker is a utility program that lets you run another program under a kind of monitor. This monitor tracks the classes that are loaded, and when the program is done, it builds a JAR file from those classes.

It’s important to note that you do not add files in the java.* class hierarchy to the JAR file. There are a couple of reasons for this. First, you don’t need them?they are included in any JRE installation. Second, there are way too many of them?the resulting JAR file would end up being much larger than the original program.

You’re not generally allowed to redistribute the core java classes yourself, according to the standard JRE/JDK licensing agreement. However, if you happened to have permission to distribute a custom JRE, you could use this system to create a JAR file containing only the java.* classes that you needed, rather than the entire set. You would have to modify the source code to include the java.* classes in the output JAR file and to look in the JRE libraries for classes to add to the JAR file, but it would be worth it. The JDK on my computer has over 27 MB of Java class files, most of which I rarely use. This method could substantially reduce that size

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist