ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Multiprocess JVMs
Pages: 1, 2

What about terminating processes which spawn threads?

Related Reading

Java Performance TuningJava Performance Tuning
By Jack Shirazi
Table of Contents
Index
Sample Chapter
Full Description
Read Online -- Safari

The last section showed us how to terminate our pseudo-process. But it applied only to a single-threaded process. If a pseudo-process spawns threads, the terminate() method will only terminate the main thread, leaving other rogue threads still running. Fortunately, Java provides a simple way to handle all the threads the application will spawn: the ThreadGroup class. When a Thread is created, it is automatically a member of a ThreadGroup, and new ThreadGroups are normally created as members of the ThreadGroup holding the current thread. So if we create a new ThreadGroup for the pseudo-process' startup thread, all of the threads spawned by the pseudo-process will be in that ThreadGroup. Calling ThreadGroup.stop() will stop all the threads in that ThreadGroup, and all subgroups. The changes needed are relatively simple. We start the class with its own ThreadGroup (new lines of code emphasized):



public static Process startAnotherClassInItsOwnThread(
             String classname, String[] args)
  {
    Process process = new Process(classname, args);
    ThreadGroup threadgroup = new ThreadGroup("main");
    Thread thread = new Thread(threadgroup, process);
    thread.start();
    return process;
  }

We also need to terminate the ThreadGroup rather than our starting thread, so the Process class now looks as follows (new lines of code emphasized):

class Process {
  String theClassName;
  String[] theStartupArguments;
  ThreadGroup mainThread;

  public Process(String classname, String[] args)
  {
    theClassName = classname;
    theStartupArguments = args;
  }

  public void run()
  {
    mainThread = Thread.currentThread().getThreadGroup();
    startAnotherClass(theClassName, theStartupArguments);
  }

  public void terminate()
  {
    if (mainThread != null)
      mainThread.stop();
  }
}

There are two unlikely but possible problems with our new ability to terminate a multithreaded pseudo-process. Firstly, it may be possible for the pseudo-process to access ThreadGroups that do not belong it, thus allowing the creation of threads that would not be terminated. Currently, I believe this could only be achieved from a custom SecurityManager. In any case, if it is possible, it would be very difficult to achieve, and the vast majority of applications can be reliably assumed to contain only threads that will be stopped when the main thread's ThreadGroup is stopped.

The second problem is associated with the deprecation of Thread.stop(). Now that we are considering multiple threads, it is possible that while terminating the threads, some of the threads will be terminated, leaving the pseudo-process application with a corrupt state just before the remaining threads are terminated. This is unlikely, and even if it did occur, we can assume that it doesn't matter if the application, in its last few milliseconds prior to forced termination, is in a corrupt state. In fact, the application is probably already in an unexpected state, since we are being forced to terminate it.

What if an application calls System.exit()?

We still have one big hole in our multiprocess library. If any application calls System.exit(), the JVM terminates, and all the pseudo-processes will be destroyed with no warning. Fortunately, Java's design once again comes to our aid. Any call to System.exit() is first checked by the SecurityManager to see if the application has permission to terminate the JVM. We can install our own SecurityManager to catch the System.exit() call, disallow it, and terminate the pseudo-process instead. The SecurityManager is actually quite simple to define:

class ExitCatchingSecurityManager extends SecurityManager
{
  public void checkExit(int status)
  {
    Process.terminateProcessWithThreadGroup(getThreadGroup());
    throw new SecurityException();
  }
}

In addition, the SecurityManager should define all other checks so that they do not block pseudo-processes from running. A simple null call for all check* methods will work. We install our own SecurityManager by calling System.setSecurityManager(), i.e., by adding the following line near the startup of the multiprocess library:

System.setSecurityManager(new ExitCatchingSecurityManager());

The Process.terminateProcessWithThreadGroup() method is simple to define, by holding a collection of Process objects in the Process class, searching the collection to find the Process with the identical ThreadGroup, then terminating that Process.

Classpaths, class names, and class versions

Our multiprocess library now seems to provide everything needed to handle multiple Java processes in one JVM. However, JVMs are usually started with a particular classpath, which gives access to all classes. Are we restricted to having only one classpath for our multiprocess JVM? If so, all the classes will have to be in that classpath, and any name clashes from different applications will cause huge problems, as will new versions of a class created after an older version has already been loaded.

This particular problem has been discussed many times for server and development environments. The solution is to dedicate a separate ClassLoader for each pseudo-process. Having a dedicated ClassLoader avoids any name clashes and allows classpaths to be defined at pseudo-process startup. Classes are identified in a JVM by their full name, and also by the ClassLoaders that loaded them. Classes loaded by different ClassLoader instances are separate classes, even if they have the same name. In fact, the same class file loaded by two different ClassLoaders can result in two different classes in the JVM.

We need to make the following changes to our multiprocess library in order to support dedicated per process ClassLoaders. First we need to define a ClassLoader. Then we need to change the code that looks for the class to use our custom ClassLoader. The latter change is quite simple; we go back to the startAnotherClass() method, the very first method defined in this article, and change the line calling Class.forName():

Class classObject = Class.forName(classname);

To use the ClassLoader.loadClass() method, instead:

ClassLoader myLoader = new MyClassLoader(...)
Class classObject = myLoader.loadClass(classname, true);

The custom ClassLoader could be more challenging to implement. But in fact, we don't have to work hard to define a proprietary ClassLoader, since Java 2 comes with several useful ClassLoaders. In particular, the java.net.URLClassLoader allows for a complete specification of the classpath. Converting a standard classpath to a list of URLs is easier than defining a new ClassLoader that supports file systems and jar files. The following is a simple implementation of the MyClassLoader class. For simplicity, I've restricted this ClassLoader to allow only one extra directory to be added to the classpath:

class MyClassLoader extends URLClassLoader
{
  public MyClassLoader (File additionalClassPath)
  {
    super(new URL[0]);
    URL url = null;   
    try{
      url = new URL("file:///" +
             additionalClassPath.getAbsolutePath()+"/");
    }catch(Exception e){e.printStackTrace();}
    addURL(url);
  }

  //Change loadClass to public access
  public Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
  {
    return super.loadClass(name, resolve);
  }
}

Note that there are now two types of classes loaded by the multiprocess library:

  • Shared classes, which are found in the classpath set when the JVM is started.
  • Unshared classes loaded separately for each pseudo-process, which are found from the classpath of the per-process classloader.

Having a distinction between shared and unshared classes requires some management, but has the advantage that you can avoid loading the same class multiple times for each process, thus saving more memory. If the management overhead is an issue, you can restrict the shared classes to the SDK classes, and have all application classes unshared.

Echidna

When I first started researching this article, I hadn't encountered Echidna. So I was hugely interested to find this free, open source library which already covered all the issues presented in this article, and many more. Other issues considered by Echidna include:

  • Starting non-public classes which contain main(String[]) methods
  • java.lang.Process-like methods for the pseudo-processes
  • Other SecurityManager requirements
  • Priority levels of threads to ensure the process manager always has priority
  • ClassLoaders with multiple classpath entries
  • Managing shared and non-shared classes
  • Redirecting console I/O for different processes
  • A UI to view and manage pseudo-processes
  • Starting pseudo-processes from the command line while other pseudo-processes are already running

Further Resources

Jack Shirazi is the author of Java Performance Tuning. He was an early adopter of Java, and for the last few years has consulted mainly for the financial sector, focusing on Java performance.


Read more Java Design and Performance Optimization columns.

Return to ONJava.com.