This chapter discusses three tactics for controlling class loading. Interestingly, all three tactics rely on the Java language's dynamic reflection features. While reflection isn't as fast as normal method dispatch, in many cases the benefits of using reflection outweigh the slight reduction in speed.
These reflection-based techniques aren't general-purpose solutions-they're designed to solve specific problems relating to class loading and footprint. As with most of the tactics presented in this book, you should carefully evaluate whether or not they are relevant to your application and measure the effects of your changes. In some cases, you might need to balance the requirements that your program should be robust and maintainable against small performance gains.
The three techniques discussed here offer varying degrees of improvement in the amount of RAM classes consume in your program. These techniques can help with the following problems:
Translator framework
To better understand this problem, consider a simple word processing program. A major feature of such a program is the ability to open and display a wide range of document types. The program could be designed with an elegant framework of Translator classes that handle different data types, as shown in Figure 6-1.
In this framework, Translator is a small interface, but each class that implements it can be very large. In fact, each translator might actually be a whole collection of classes. This means that the cost of loading a translator is large. Our example word processor uses a factory method to create the appropriate translator when a document is opened. This factory method is shown in Listing 6-1.
public static Translator getTranslator(String fileType) {
if (fileType.equals("doc")) {
return new WordTranslator();
} else if (fileType.equals("html")) {
return new HTMLTranslator();
} else if (fileType.equals("txt")) {
return new PlainTranslator();
} else if (fileType.equals("xml")) {
return new XMLTranslator();
} else {
return new DefaultTranslator();
}
}
Simple factory method
In the process of tuning the word processor, it becomes evident that all of the translators are loaded before any document is opened-not just the particular translator needed for the document. Figure 6-2 shows a segment of the output displayed when the program is run with the -verbose flag on.
Output from verbose class loading
In this example, all of these classes are loaded because they are referenced from inside the same method. The JIT compiler of the runtime being used (Sun's Java 2 SDK v. 1.2 with the Symantec JIT) requires knowledge of all classes referenced by the method in order to compile it. To compile the method, all of the referenced classes are loaded. Since each translator is very large, several hundred kilobytes of memory might be consumed because these extra classes are loaded. Obviously, preventing them from being loaded would result in a significant savings in footprint. The next section discusses how to control eager loading in this situation.
In the word processor discussed in Section 6.1, reflection could be used to avoid loading all of the Translator implementations whenever one is needed. Listing 6-2 shows how the factory method can be rewritten using simple class reflection to avoid explicitly referencing any of the Translator implementations. Functionally, it's identical to the method shown in Listing 6-1.
public static Translator getTranslator(String fileType) {
try {
if (fileType.equals("doc")) {
return (Translator)Class.forName(
"WordTranslator").newInstance();
} else if (fileType.equals("html")) {
return (Translator)Class.forName(
"HTMLTranslator").newInstance();
} else if (fileType.equals("txt")) {
return (Translator)Class.forName(
"PlainTranslator").newInstance();
} else if (fileType.equals("xml")) {
return (Translator)Class.forName(
"XMLTranslator").newInstance();
} else {
return new DefaultTranslator();
}
} catch (Exception e) {
return new DefaultTranslator();
}
}
Using reflection in a factory
In this version, there are no static references to the various Translator classes that the compiler can see directly. This prevents it from loading these classes early, even if it wants to. Instead, the classes are only loaded when the Class.forName method is called.
It's important to note that as runtimes improve, they'll prematurely load classes less often. There is already considerable variation in when classes are loaded from runtime to runtime. For example, the HotSpot compilers don't suffer from the specific problem described here, although many others do.
You shouldn't automatically use reflection everywhere in your program. In each case, you need to consider the benefits of controlling class loading against the costs of increased code complexity and reduced computational performance. This tactic is useful if large classes or a large number of classes are being loaded when you don't think they should be.
JDK 1.1 introduced inner classes to the Java programming language. These powerful constructs enable developers to deal with the flexible event model introduced by the JavaBeans component architecture. Figure 6-3 shows a simple application written using the Swing JButton class, which is a bean. When a button is clicked, this program simply outputs a string to the console. In the following sections, several different versions of this program are used to illustrate the costs associated with different coding styles.
Simple GUI application
The code in Listing 6-3 uses inner-classes to attach listeners to a set of buttons. An inner class is created for each action that the user can perform, and an instance of each class is attached to one of the buttons. The actionPerformed method in the inner class then dispatches the event to the appropriate method in the enclosing class. (In a real program, the open, close, and save methods would obviously do more than print to the system console.)
public class Listener1 extends JFrame {
public Listener1() {
JButton open = new JButton("Open");
JButton close = new JButton("Close");
JButton save = new JButton("Save");
getContentPane().setLayout(new FlowLayout());
getContentPane().add(open);
getContentPane().add(close);
getContentPane().add(save);
open.addActionListener(new OpenAction());
close.addActionListener(new CloseAction());
save.addActionListener(new SaveAction());
pack();
setVisible(true);
}
protected void open() {
System.out.println("Open a file");
}
protected void close() {
System.out.println("Close a file");
}
protected void save() {
System.out.println("Save a file");
}
class OpenAction implements ActionListener {
public void actionPerformed(ActionEvent e) {
open();
}
}
class CloseAction implements ActionListener {
public void actionPerformed(ActionEvent e) {
close();
}
}
class SaveAction implements ActionListener {
public void actionPerformed(ActionEvent e) {
save();
}
}
public static void main(String[] args) {
new Listener1();
}
}
Simple GUI with inner classes
While this is a very clean, object-oriented solution, it is somewhat costly-each inner class increases the program's RAM footprint. While the class files generated for the inner classes are only a few hundred bytes each, they occupy about 3K once loaded into RAM. (While the actual amount of RAM consumed is implementation-dependent, 3K is typical for several runtimes.)
In this example, the inner classes only add about 10K to the program's RAM footprint, which is negligible. A large, complex application, however, might need 100 listeners, which could consume several hundred kilobytes. If your application falls into the latter category, it's probably worth optimizing to reduce the number of classes that are loaded.
class ButtonAction implements ActionListener {
public void actionPerformed(ActionEvent e) {
JButton b = (JButton)e.getSource();
if ( b.getText().equals("Open") ) {
open();
} else if (b.getText().equals("Close")) {
close();
} else if (b.getText().equals("Save")) {
save();
}
}
}
Handling multiple events with a single listener
Listing 6-5 shows how this single listener class is used with each button. This implementation is functionally identical to the one shown in Listing 6-3. The advantage of this solution is that only one class is needed to handle the input from all of the buttons. Instead of three classes, only a single class needs to be loaded into memory.
ActionListener listener = new ButtonAction(); open.addActionListener(listener); close.addActionListener(listener); save.addActionListener(listener);
Adding the listeners
This solution is problematic, however. The primary problem is that it's more difficult to maintain. Whenever you see a switch structure like this in an object-oriented program, you know you're in trouble-it just doesn't scale well. If the program has 100 actions, you'd need 100 if-then-else clauses. Another problem with this approach is that the code is fragile. It's dependent on the names of the buttons, which are likely to change and might need to be localized. Although this optimization is an improvement in terms of footprint, the benefit isn't worth the impact on maintainability and stability.
Translator example in Section 6.1.1, reflection was used to
avoid loading classes. It can also be used to avoid creating classes at all. The code
in Listing 6-6 shows a generic class that can be used to dispatch an action from
any ActionEvent source to any zero-argument method on the target object.
class ReflectiveAction implements ActionListener {
String methodName;
Object target;
public ReflectiveAction(Object target, String methodName)
{
this.target = target;
this.methodName = methodName;
}
public void actionPerformed(ActionEvent e) {
try {
Class[] argTypes = {};
Method method = target.getClass().getMethod(methodName,
argTypes);
Object[] args = {};
method.invoke(target, args);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
Generic ActionListener
To use this class, you create an instance of it and pass the constructor a target object and the name of the method to be called on the target. When an event arrives, ReflectiveAction looks up the method and calls it. Listing 6-7 shows how ReflectiveAction is used to add listeners to the buttons in a simple application.
open.addActionListener(new ReflectiveAction(this, "open")); close.addActionListener(new ReflectiveAction(this, "close")); save.addActionListener(new ReflectiveAction(this, "save"));
Adding ReflectiveActions
Classes like ReflectiveAction are sometimes called trampolines because they spring you from one method to another. Trampolines are an attractive solution for a couple reasons.
Trampolines have some drawbacks, however. The biggest problem is that you lose compile-time type checking. If you mistype the name of a method, you'll wind up with a runtime error instead of a compile time error. There's also a minor performance hit because reflection is slower than direct method dispatch. However, the amount of time it takes to perform the lookup and call the method is so small that no user could perceive it. In many situations, using trampolines is a reasonable tactic for reducing footprint.
The Java 2 Standard Edition (J2SE) v. 1.3 introduces a new class called java.lang.reflect.Proxy. This class was designed to deal with just this situation. The Proxy class allows simple trampoline-style classes to be generated on the fly during runtime. This makes it possible to create a proxy builder that can generate a proxy for any listener type at runtime.
Note: java.lang.reflect.Proxy isn't recommended for use in your everyday programming. The code required is quite tricky and can be somewhat confusing. This tactic is best reserved for systems that demand maximum flexibility, such as JavaBeans assembly tools or other tools that allow users to configure actions. For such applications, Proxy is extremely powerful.
Listing 6-8 shows a class that builds dynamic proxy objects. This class implements the java.lang.reflect.InvocationHandler interface, which is a companion of the Proxy class. InvocationHandler defines a single method called invoke. This method allows you to specify how a particular call is to be routed through the proxy. This example simply ignores the arguments passed to the original function-no arguments are passed to the target object. A more full-featured implementation might pass along the arguments as well.
class DynamicProxy implements InvocationHandler {
Object target;
String methodName;
public static Object makeProxy(Object target,
String methodName,
Class impl) {
ClassLoader loader = target.getClass().getClassLoader();
DynamicProxy handler = new DynamicProxy();
handler.target = target;
handler.methodName = methodName;
// create a new proxy object
// the object will implement the interface passed
// into the impl parameter
// it will dispatch all method calls to the proxy
// to the target object via the invoke method below
return Proxy.newProxyInstance(loader ,
new Class[]{impl},
handler);
}
public Object invoke(Object proxy, Method method,
Object[] args) {
try {
Object[] noArgs = {};
Class[] argTypes = {};
Method targetMethod = target.getClass().
getMethod(methodName,
argTypes);
return targetMethod.invoke(target, noArgs);
} catch (Exception e ) {
e.printStackTrace();
return null;
}
}
}
Proxy generator
This DynamicProxy class provides a static method that creates a new proxy. You pass in the target for the proxy, the name of the method you want to call on the target, and the interface that the proxy will implement.
What goes on behind the scenes when you use a proxy is fairly complex. The first time you create a Proxy that implements a particular interface, a new class is created on the fly. For example, if the Proxy implements ActionListener, a class is created that is named something like $ProxyActionListener. You never directly interact with this class. Each time you call a method on the Proxy, it calls the invoke method of its InvocationHandler.
In the implementation of the invoke method, you redirect the method call however you see fit. In the DynamicProxy example, all method calls are redirected to a particular method on the specified target object. Listing 6-9 shows the code that creates the proxies and adds them to the buttons of our sample GUI application.
open.addActionListener(
(ActionListener)DynamicProxy.makeProxy(this, "open",
ActionListener.class));
close.addActionListener(
(ActionListener)DynamicProxy.makeProxy(this, "close",
ActionListener.class));
save.addActionListener(
(ActionListener)DynamicProxy.makeProxy(this, "save",
ActionListener.class));
Adding the proxies
How does this tactic compare with using a trampoline? The runtime costs are similar. After a proxy is created for ActionListener the first time, subsequent proxies for the ActionListener don't cause a new class to be generated. Instead, you're handed a new instance of the previously created class. The advantage of using a proxy instead of a trampoline is that you won't have to create a new trampoline each time you need to use a new listener interface-it's created for you automatically. The disadvantage is that using a proxy is not as straightforward as using a trampoline.
One example is the UIDefaults table, which contains much of the look-and-feel specific information for an interface. To avoid premature loading, it uses the javax.swing.UIDefaults.ProxyLazyValue class. The default CellEditors associated with the javax.swing.JTable class are loaded lazily using the same technique. These reflection-based solutions removed over 200 class loads for some of the applications we measured.
One solution to this problem is to run several programs inside the same JVM instance so that the programs can share loaded classes. This can result in a major reduction in the total memory used.
The following sections use a simple office suite of programs to illustrate the costs of running multiple JVM instances. Sections 6.3.2 and 6.3.3 discuss a tactic you can use to reduce these costs.
public class Spreadsheet {
public static void main(String[] args) {
JFrame f = new JFrame("Spreadsheet");
JTable table = new JTable(20,5);
JScrollPane scroller = new JScrollPane(table);
f.setContentPane(scroller);
f.setBounds(10,10,200,200);
f.setVisible(true);
f.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}});
}
}
Simple spreadsheet
public class WordProcessor {
public static void main(String[] args) {
JFrame f = new JFrame("WordProcessor");
JTextArea text = new JTextArea("Type Here");
JScrollPane scroller = new JScrollPane(text);
f.setContentPane(scroller);
f.setBounds(10,10,200,200);
f.setVisible(true);
f.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}});
}
}
Simple word processor
Running several applications
These programs can be run from the command line by entering java Spreadsheet or java WordProcessor. It is also possible to create simple batch files or shell scripts that execute these commands. This enables users on many operating systems to simply double-click the script files to run the programs.
The trouble starts when a user wants to run the spreadsheet and the word processor simultaneously, or run multiple copies of the same program. Memory requirements quickly start to spiral out of control. For example, the Windows NT Task Manager shown in Figure 6-4 shows the memory requirements for two spreadsheets and a word processor.
According to the Task Manager, each program is using nearly 9MB of RAM-about 26MB altogether! It's actually not quite as bad as it looks-some of the memory is taken up by native DLLs that are shared by all three programs. However, the shared space only accounts for 1 to 2MB per application-even taking the shared memory into account, together these applications are using 20 to 24MB. Obviously, this just isn't going to work on a system with limited RAM resources.
Flow chart for a simple launcher
When the launcher is asked to start a program, it first checks to see if there is a JVM already running that is willing to run the program. If there is, the launcher asks that JVM to run the program and then exits. If there isn't, the launcher runs the program in its JVM instance and listens for requests from other launchers to run additional programs. As long as the first launcher continues to run, other launchers start up, pass their requests onto the older launcher, and then shut down. If the first launcher shuts down, the next time a launcher starts it will run the requested program itself and begin handling requests from other launchers. Listing 6-12 shows the code for the Launcher class.
public class Launcher {
static final int socketPort = 9876;
public void launch(String className) {
boolean launched = false;
while (!launched) {
System.out.println("Trying to launch:"+className);
Socket s = findService();
if (s != null) {
System.out.println("found service");
try {
OutputStream oStream = s.getOutputStream();
byte[] bytes = className.getBytes();
oStream.write(bytes.length);
oStream.write(bytes);
oStream.close();
launched = true;
System.out.println(className);
} catch (IOException e) {
System.out.println("Couldn't talk to service");
}
} else {
try {
System.out.println("Starting new service");
ServerSocket server = new ServerSocket(socketPort);
Launcher.go(className);
Thread listener = new ListenerThread(server);
listener.start();
launched = true;
System.out.println("started service listener");
} catch (IOException e) {
System.out.println("Socket contended, will try again");
}
}
}
}
protected Socket findService() {
try {
Socket s = new Socket(InetAddress.getLocalHost(), socketPort);
return s;
} catch (IOException e) {
// couldn't find a service provider
return null;
}
}
public static void go(final String className) {
System.out.println("running a " + className);
Thread thread = new Thread() {
public void run() {
try {
Class clazz = Class.forName(className);
Class[] argsTypes = {String[].class};
Object[] args = {new String[0]};
Method method = clazz.getMethod("main", argsTypes);
method.invoke(clazz, args);
} catch (Exception e) {
System.out.println("coudn't run the " + className);
}
}
}; // end thread sub-class
thread.start();
}
public static void main(String[] args) {
Launcher l = new Launcher();
l.launch(args[0]);
}
}
Simple program launcher
To use the Launcher, you pass it the name of the class you want to run. The Launcher creates an instance of that class and calls its main method. Note that the Launcher also uses a class called ListenerThread. This class listens on a socket for requests to launch other programs.
Listing 6-13 contains the code for the ListenerThread. The run method of the ListenerThread class creates a ServerSocket and then runs in an infinite loop to serve launch requests.
public class ListenerThread extends Thread {
ServerSocket server;
public ListenerThread(ServerSocket socket) {
this.server = socket;
}
public void run() {
try {
while (true) {
System.out.println("about to wait");
Socket socket = server.accept();
System.out.println("opened socket from client");
InputStream iStream = socket.getInputStream();
int length = iStream.read();
byte[] bytes = new byte[length];
iStream.read(bytes);
String className = new String(bytes);
Launcher.go(className);
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("Failed to start");
}
}
}
ListenerThread
Figure 6-6 shows the memory consumption from running the same three applications using the Launcher. In this case, only one JVM is running instead of three. The total memory consumption is less than 10MB, rather than the 20+MB required to run them in separate JVM instances. The great thing about using a launcher is that the savings continue to scale up as you run more applications.
Launcher shown in Listing 6-12: Both the
Spreadsheet and the WordProcessor classes call System.exit(0). If the user
closes any one of these programs, all of them will close.
Running several applications with the launcher
Modifying the Launcher to work around this problem is fairly easy. You need to provide a mechanism through which a program can tell the Launcher it's closing, and track whether or not any programs are still running. The applications then needs to be modified to notify the Launcher when they quit instead of calling System.exit.
Adding a programQuit method to the Launcher class provides a way for programs to notify the Launcher that they are shutting down. This method is shown in Listing 6-14.
static int runningPrograms = 0;
public static void programQuit() {
runningPrograms--;
if (runningPrograms <= 0) {
System.exit(0);
}
}
Launcher programQuit method
You also need to add a runningPrograms++ operation to the Launcher.go method so the Launcher can track how many programs are still running. When this number hits zero, the Launcher itself should shut down so that its resources are freed up for other uses.
In each application to be started through the Launcher, you also need to replace System.exit with
You can download the complete source code for this and other examples from http://java.sun.com/docs/books/performance/.Launcher.programQuit(); e.getWindow().dispose();