Skip navigation

Recently I've seen several people ask what the cost of enabling JMX monitoring on an application is. If I run with-Dcom.sun.management.jmxremote and connect jconsole, how much will that affect the performance of my app? Here are the results of some highly unscientific experiments.

Here's the quick answer for people who look at the answers in the back of the book. Running with-Dcom.sun.management.jmxremote has no appreciable impact. Connecting jconsole has an impact of 3--4%.

The answer to this question is likely to depend very much on the nature of the app and what else is going on in the machine where it is running. My measurements here are on a toy application which computes digits of the mathematical constant eusing BigInteger arithmetic. This isn't intended to be an illustration of appropriate mathematical techniques. It's just a simple way to create an app that does a lot of computation and a lot of allocation of large short-lived objects. So my measurements here are likely to be interesting for apps like that, and probably not wildly irrelevant for other sorts of apps.

Here for reference is the method that the app spends most of its time in:

    private static void computeE(int digits) {
        BigInteger one = BigInteger.TEN.pow(digits);
        BigInteger e = BigInteger.ZERO;
        BigInteger invfact = one;
        BigInteger n = BigInteger.ONE;
        while (invfact.compareTo(BigInteger.ZERO) > 0) {
            e = e.add(invfact);
            invfact = invfact.divide(n);
            n = n.add(BigInteger.ONE);
        }
        System.out.println(e);
    }

Note by the way that the last few digits computed by this method are inaccurate due to truncation of the division results.

I ran this method with the digits parameter equal to 30000 on an elderly SPARC machine. I was logged in to the machine in an X Windows session too, so there was some background activity. I ran each measurement two or three times for each configuration and took the smallest time. (I did say this was unscientific. What are these repeatable test conditionsand standard deviations of which you speak?)

I created one thread per CPU to make sure that the machine was CPU-bound. Otherwise we might see zero overhead for enabling monitoring because it might be using an otherwise-idle CPU.

Startup time is slightly greater when you enable monitoring because it needs to do some extra work such as creating an RMI connector. I assumed that the figure of interest, though, is the cost while the app is running. So I measured the time between the creation of the computeE threads and their completion.

I used two Java platforms for the measurements: Sun's JDK 1.5.0_07 and a Mustang snapshot (1.6.0-rc-b92). The time for the computation was 37% better on Mustang, presumably because of improvements to the JIT compiler and/or heap management. Also the time is better still if you run with-server. But what we're interested is the relative difference between monitoring and not monitoring.

The first measurement was of the application running without any special command-line parameters. So monitoring was not enabled. This is the baseline for the other measurements.

         
TigerMustang
28.318.0

Then I ran the application with-Dcom.sun.management.jmxremote, which has two effects:

  • It creates the Platform MBean Server and populates it with the standard set of MBeans containing the JVM's instrumentation.
  • It creates and starts an RMI Connector Server and puts its address where jconsole can find it.

So with this option we could connect jconsole from the local machine. But I didn't connect jconsole for this measurement. The effect on performance was less than 1% in this case, and in fact any observed difference was less than the margin of error. So there was no significant difference in the time to do the computation. In other words, so long as you don't connect jconsole, enabling monitoring should have no effect on your app's performance.

I got the same result when I ran with
-Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.remote.ssl=false
so allowing jconsole to connect remotely but without security also has no effect on performance. Note that we strongly discourage running apps this way in production environments.

Running with
-Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.password.file=jmxremote.password
  (meaning there is security including the creation of an SSL socket) also has an impact of less than 1%.

Then I ran the insecure options again but this time connected Tiger's jconsole from another machine. Here are the figures I got:

         
TigerMustang
29.85 (+5.5%)19.1 (+6.1%)

So here there's a fairly big impact. Some of this is due to jconsole polling the various JVM MBeans so it can graph the changes in various values like heap size and number of threads over time. There is no way to stop jconsole from doing that, even if you're not interested in that information, but what you can do is change the polling interval. If you change the interval from the default 4 seconds to 1000 seconds, say, then you won't see much impact from polling. (The interval also applies to the graphing of your own attributes, though, so if you want that then you'll have to pay for the JVM polling.)

Running Tiger's jconsole with -interval=1000changes the figures to this:

         
TigerMustang
29.28 (+3.5%)18.7 (+3.9%)

Jconsole isn't polling, but there's still some overhead, presumably due to background activity from RMI and/or to increased memory usage because of the open connection.

Mustang's jconsole, besides being generally nicer, has an optimization that means it obtains the various JVM attributes much more efficiently, so the polling impact is less. Even if you can't migrate your apps to Mustang right now, you might want to download the Mustang JDK just so you can run its jconsole!

Here are the figures running the app with the insecure options and connecting with Mustang's jconsole from another machine:

         
TigerMustang
29.6 (+4.5%)18.8 (+4.4%)

So you get better behaviour from jconsole by default here. And of course you can still use the -interval=1000 trick, to get this:

         
TigerMustang
29.2 (+3.2%)18.7 (+3.9%)

Finally, I tried a few configurations with security enabled (-Dcom.sun.management.jmxremote.ssl=true -Dcom.sun.management.jmxremote.authenticate=true, which are the default settings) and did not see any signficant difference. Even with jconsole polling every four seconds, there was no observable extra overhead due to SSL.

Reminder

These results are not conclusive! They're merely indicative of one set of measurements on one type of app. The impact could be much greater if the addition of the JMX connection pushed the app into a different mode of operation. For example, the additional memory usage could be enough to change the behaviour of the garbage collector significantly and probably negatively. But you would have to be operating fairly close to the edge of such a possible change for that to happen, meaning it could happen even without the JMX connection if for example the app needed to handle a few more objects.

I often want to test that my MBeans work correctly when accessed remotely. For example it's easy to accidentally introduce non-serializable objects in them. It's a pain to set up a real remote connection, but you can make a loopback connection in the same JVM to test most of the same things. Here's how.

import javax.management.*;
import javax.management.remote.*;

...

MBeanServer mbs = ...something...;
// for example, java.lang.management.ManagementFactory.getPlatformMBeanServer();

// Make a connector server...
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi://");
JMXConnectorServer cs =
    JMXConnectorServerFactory.newJMXConnectorServer(url, null, mbs);
cs.start();
JMXConnector cc = null;
try {
    JMXServiceURL addr = cs.getAddress();

    // Now make a connector client using the server's address
    cc = JMXConnectorFactory.connect(addr);
    MBeanServerConnection mbsc = cc.getMBeanServerConnection();

    ...test logic here, using mbsc...
    ...e.g. String attr = (String) mbsc.getAttribute(objectName, "MyAttr");
    ...or   mbsc.addNotificationListener(objectName, listener, filter, null);
} finally {
    if (cc != null)
        cc.close();
    cs.stop();
}

If you do your tests using mbsc instead ofmbs, then MBean Server operations will go through RMI. Even though the connection never leaves the JVM, this enables you to test that your MBean code is network-friendly.

You'll typically want to stop the connector server before your test exits. Otherwise it will prevent your JVM from exiting promptly. I generally pop this into a try/finally so I'm sure it will get executed even if the test gets an exception.

Obviously you can wrap all this logic into a class that looks like this:

public class JMXRemoteTest {
    public static interface Test {
        public void test(MBeanServerConnection mbsc) throws Exception;
    }

    public static void remoteTest(MBeanServer mbs, Test test) {
        ...
        test.test(mbsc);
        ....
    }
}

and use it like this:

JMXRemoteTester.remoteTest(mbs, new JMXRemoteTester.Test() {
    public void test(MBeanServerConnection mbsc) throws Exception {
        // example test
        mbsc.addNotificationListener(objectName, listener, filter, null);
        mbsc.invoke(objectName, "provokeNotification", null, null);
        waitForNotification();
    }
}

Or if you have a bunch of remote tests, you can set up the MBeanServerConnection in your setup() method or equivalent, and use it for all the tests, then close the JMXConnector andJMXConnectorServer in the tearDown() method.

MBean proxies allow you to access an MBean through a Java interface, writing proxy.getFoo() instead of mbeanServer.getAttribute(name, "Foo"). But when you create a proxy, there is no check that the MBean actually matches the interface you specify, or even that the MBean exists. Why is that, and what can you do about it?

Here's a more concrete example. Suppose you have an MBean interface like this:

public interface CacheMBean {
    public int getSize();
    public void setSize(int x);
    public void dropOldest(int nEntries);
}

Also suppose that you have registered an MBean answering to that interface with a certain ObjectName, saysomedomain:type=Cache. Then you might make a proxy like this:

CacheMBean proxy = JMX.newMBeanProxy(mbeanServer, objectName, CacheMBean.class);

Well, that's the neato Mustang (Java SE 6) version. If you're using an earlier version, like Tiger (J2SE 5.0), then it's a bit more verbose:

CacheMBean proxy = (CacheMBean)
    MBeanServerInvocationHandler.newProxyInstance(mbeanServer, objectName,
                                                  CacheMBean.class, false);

(Of course that will continue to work on Mustang, but the first version is so much nicer that you'll want to use it if you can.)

Either way, this allows you to write things like:

proxy.setSize(proxy.getSize() * 2);
proxy.dropOldest(25);

instead of the code you would have to write without the proxy:

int size = mbeanServer.getAttribute(objectName, "Size");
mbeanServer.setAttribute(objectName, new Attribute("Size", size * 2));
mbeanServer.invoke(objectName, "dropOldest", new Object[] {25},
                   new String[] {"int"});

It's clear that the version with the proxy is much simpler to read and write, and much safer too since the compiler will check that the methods you are invoking are indeed in the interface and that you are using the right types. That's why we recommend using proxies like this when at all possible.

Poxy proxy

But all is not completely rosy. What happens for example if the MBean doesn't exist? You might expect that creating the proxy would fail in that case. But it doesn't. You can go right ahead and make your proxy, and then when you call proxy.getSize()you'll get an exception like this:

Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
        at $Proxy0.getSize(Unknown Source)
        at typesafeproxy.Test.main(Test.java:37)
Caused by: javax.management.InstanceNotFoundException: somedomain:type=Cache
        at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.getMBean(DefaultMBeanServerInterceptor.java:1094)
        at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.getAttribute(DefaultMBeanServerInterceptor.java:662)
        at com.sun.jmx.mbeanserver.JmxMBeanServer.getAttribute(JmxMBeanServer.java:638)
        at javax.management.MBeanServerInvocationHandler.invoke(MBeanServerInvocationHandler.java:262)
        ... 2 more

You'll immediately wonder

  • why is it only telling me now?
  • what's that UndeclaredThrowableExceptionabout?

JMX.newMBeanProxy doesn't check whether the MBean you name exists for the simple reason that even if it does exist at that point, there's no guarantee that it will still exist when you come to make a call through your proxy. So your code needs to be prepared to deal with that eventuality anyway.

The UndeclaredThrowableException comes about because the method you called, CacheMBean.getSize(), doesn't declare the exception InstanceNotFoundException in itsthrows clause. In other words, it is "not declared as throwable", which is mangled into the barely comprehensible nameUndeclaredThrowableException you see here.

We generally recommend that you declare throws IOException on every method in an MBean interface so that you will be forced to catch any network problems that arise when you make a remote call through a proxy. So we should have writtenCacheMBean like this:

public interface CacheMBean {
    public int getSize() throws IOException;
    public void setSize(int x) throws IOException;
    public void dropOldest(int nEntries) throws IOException;
}

However, InstanceNotFoundException is not anIOException so that wouldn't have fixed the problem we saw above. You could reasonably add JMException to the throws of every method too, and then you would seeInstanceNotFoundException directly.JMException is the parent class ofInstanceNotFoundException.

JMException is also the parent class of a number of other exceptions that you might see when you are accessing an MBean, whether through a proxy or not. For example, you might get an AttributeNotFoundException fromproxy.getSize() if the MBeansomedomain:type=Cache does exist but it doesn't contain an attribute called Size. Again, we could have checked that every attribute and operation in the proxy existed at the time we were creating the proxy, but that wouldn't guarantee that the MBean wouldn't later be replaced by another one where some attributes have been removed.

Pesky proxy

Nevertheless, usually you do know that your MBeans aren't suddenly going to disappear or delete their attributes, and in that case you might be happier if the checks were made upfront when the proxy is created. Can we achieve that somehow?

The answer is that we can. To start with, here's a simple class that defines a method that creates an MBean proxy, but throws an exception if the proxied MBean doesn't exist.

public class ExistentJMXProxy {
    private ExistentJMXProxy() {}  // no instances of this class

    public static <T> T newMBeanProxy(
        MBeanServerConnection mbsc,
        ObjectName name,
        Class<T> intfClass)
        throws IOException, InstanceNotFoundException {

        // Provoke IOException or InstanceNotFoundException now
        // rather than later
        mbsc.getObjectInstance(name);

        return intfClass.cast(
            MBeanServerInvocationHandler.newProxyInstance(
                mbsc, name, intfClass, false));
    }
}

Now you can write
CacheMBean proxy = ExistentJMXProxy.newMBeanProxy(
    mbeanServer, objectName, CacheMBean.class);
and you will get an InstanceNotFoundExceptionright there and then if the MBean called objectNamedoesn't exist.

We make a call to getObjectInstance because it's about the simplest MBeanServer operation you can do that will throwInstanceNotFoundException if the MBean doesn't exist and do nothing if it does.

The magic with <T> basically means "if theintfClass parameter is CacheMBean.classthen the return type is CacheMBean". This is because the type of CacheMBean.class isClass<CacheMBean>. But it's blackmagic, as usual with Java generics, tainted by the demon Erasure. That's why we writeintfClass.cast(blah) rather than just(T) blah in the last line. Casting toT would produce the dreaded warning: [unchecked] unchecked cast from the compiler.

Prickly proxy

We might be satisfied with our existence check, but in fact just because an MBean exists with the name we gave doesn't mean it's theright one. Nothing is stopping you from creating aCacheMBean proxy for an MBean that is actually aNoddyInToylandMBean. Once again, you'll only find out about it when you actually try to use the proxy. The MBean might even have some of the right attributes but not others (it's an older version, say), so the nasty surprise might be significantly delayed. What we really want is a check that every method we might call on the proxy will be valid on the target MBean.

If you're very familiar with the JMX API, you might think that a good and simple way to make this check would be to use MBeanServer.isInstanceOf to check that the MBean does indeed implement the CacheMBean interface.isInstanceOf throwsInstanceNotFoundException, so we could simply replace the mbsc.getObjectInstance(name) inExistentJMXProxy.newMBeanProxy with this:

if (!mbsc.isInstanceOf(name, intfClass.getName()))
    throw new InstanceNotFoundException("Wrong type MBean: " + name);

But actually that isn't a great idea. It will work if the MBean is a Standard MBean that implements the CacheMBeaninterface. But it won't work if the MBean is a Dynamic MBean, even if it exports the exact same attributes and operations that a Standard MBean would. And it won't work if you use the classjavax.management.StandardMBean to create a customized Standard MBean; if you've read other entries in this blog you'll know I'm very fond of doing that.

The problem is that isInstanceOf is making a test on the Java class providing the implementation of the MBean. We don't really care about that. What we really want to know is whether all of the methods we can call on the proxy will work.

Prolix proxy

So what we want to do is to check, when creating the proxy, that the named MBean exists, and that it has all the attributes and operations that the proxy can access. How might we go about doing that?

The simplest way is to generate the MBeanInfo corresponding to the proxy interface (CacheMBean) and compare it against theMBeanInfo from the MBean we want to proxy. Every readable attribute in the proxy's MBeanInfo must have a corresponding readable attribute in the MBean'sMBeanInfo. Every writeable attribute must have a corresponding writeable attribute. Every operation must have a corresponding operation.

We don't have to require the twoMBeanInfos to be identical. The MBean might have additional attributes and operations that we won't be able to access through the proxy, and there's no problem with that. Also, the attribute and operation types don't have to match exactly: the real type of an attribute might be a subclass of the type that the proxy expects, and that's OK provided the attribute is a read-only one. Likewise, the return type of an operation might be a subclass of the type that the proxy expects.

So let's look at some code. We're going to make a classTypeSafeJMXProxy with a methodnewMBeanProxy that will only create a proxy if the target MBean exists and exports the right attributes and operations. If you write
CacheMBean proxy = TypeSafeJMXProxy.newMBeanProxy(
   mbeanServer, objectName, CacheMBean.class);
then you can be sure that proxy.getSize() andproxy.setSize(n) and proxy.dropOldest(n)will all work, assuming the MBean doesn't disappear or mutate in the meantime.

public class TypeSafeJMXProxy {

    /** There are no instances of this class. */
    private TypeSafeJMXProxy() {
    }

    /**
     * Create an MBean proxy, checking that the target MBean exists
     * and implements the attributes and operations defined by the
     * given interface.
     *
     * @param mbsc the MBean Server in which the proxied MBean is registered.
     * @param name the ObjectName under which the proxied MBean is registered.
     * @param intfClass the MBean interface that the proxy will
     * implement by forwarding its methods to the proxied MBean.
     *
     * @return The newly-created proxy.
     *
     * @throws IOException if there is a communication problem when
     * connecting to the {@code MBeanServerConnection}.
     * @throws InstanceNotFoundException if there is no MBean
     * registered under the given {@code name}.
     * @throws NotCompliantMBeanException if {@code intfClass} is
     * not a valid MBean interface.
     * @throws NoSuchMethodException if a method in
     * {@code intfClass} does not correspond to an attribute or
     * operation in the proxied MBean.
     */
    public static <T> T newMBeanProxy(
            MBeanServerConnection mbsc,
            ObjectName name,
            Class<T> intfClass)
            throws IOException, InstanceNotFoundException,
                   NotCompliantMBeanException, NoSuchMethodException {

        // Get the MBeanInfo, or throw InstanceNotFoundException
        final MBeanInfo mbeanInfo;
        try {
            mbeanInfo = mbsc.getMBeanInfo(name);
        } catch (InstanceNotFoundException e) {
            throw e;
        } catch (JMException e) {
            // IntrospectionException or ReflectionException:
            // very improbable in practice so just pretend the MBean wasn't there
            // but keep the real exception in the exception chain
            final String msg = "Exception getting MBeanInfo for " + name;
            InstanceNotFoundException infe = new InstanceNotFoundException(msg);
            infe.initCause(e);
            throw infe;
        }

        // Construct the MBeanInfo that we would expect from a Standard MBean
        // implementing intfClass.  We need a non-null implementation of intfClass
        // so we create a proxy that will never be invoked.
        final T impl = intfClass.cast(Proxy.newProxyInstance(
                intfClass.getClassLoader(), new Class<?>[] {intfClass}, nullIH));
        final StandardMBean mbean = new StandardMBean(impl, intfClass);
        final MBeanInfo proxyInfo = mbean.getMBeanInfo();

        checkMBeanInfos(intfClass.getClassLoader(), proxyInfo, mbeanInfo);
        return intfClass.cast(MBeanServerInvocationHandler.newProxyInstance(
                mbsc, name, intfClass, false));
    }

The trick we use to get the MBeanInfo for our MBean interface (CacheMBean) is to create an MBean for it locally using the ever-useful javax.management.StandardMBean.StandardMBean implements the DynamicMBean interface (confusingly enough), so we can just call getMBeanInfo() on that interface and then throw the MBean away.

But to create the MBean we need an object that implements the MBean interface. We can use a dynamic proxy to get that object. No method in the object will ever actually be called, since we throw away the MBean as soon as we've extracted its MBeanInfo. So we can just make a proxy that implements the interface by returning null from all of its methods:

    private static class NullInvocationHandler implements InvocationHandler {
        public Object invoke(Object proxy, Method method, Object[] args) {
            return null;
        }
    }
    private static final NullInvocationHandler nullIH =
        new NullInvocationHandler();

The call to Proxy.newProxyInstance (see above) is where this actually gets used.

OK, we've got the MBeanInfo corresponding to ourCacheMBean interface (proxyInfo), and we've got the MBeanInfo of the target MBean. Now we need to check that they are compatible as we described above. That's whatcheckMBeanInfos will do.

    private static void checkMBeanInfos(
            ClassLoader loader, MBeanInfo proxyInfo, MBeanInfo mbeanInfo)
            throws NoSuchMethodException {

        // Check that every attribute accessible through the proxy is present
        // in the MBean.
        MBeanAttributeInfo[] mais = mbeanInfo.getAttributes();
    attrcheck:
        for (MBeanAttributeInfo pai : proxyInfo.getAttributes()) {
            for (MBeanAttributeInfo mai : mais) {
                if (compatibleAttributes(loader, pai, mai))
                    continue attrcheck;
            }
            final String msg =
                    "Accessing attribute " + pai.getName() + " would fail";
            throw new NoSuchMethodException(msg);
        }

        // Check that every operation accessible through the proxy is present
        // in the MBean.
        MBeanOperationInfo[] mois = mbeanInfo.getOperations();
    opcheck:
        for (MBeanOperationInfo poi : proxyInfo.getOperations()) {
            for (MBeanOperationInfo moi : mois) {
                if (compatibleOperations(loader, poi, moi))
                    continue opcheck;
            }
            final String msg =
                    "Accessing operation " + poi.getName() + " would fail";
            throw new NoSuchMethodException(msg);
        }
    }

Notice that we're comparing every attribute inproxyInfo against every attribute inmbeanInfo, so the execution time is quadratic in the number of attributes, and likewise for operations. We could improve this, but it is not as simple as it might seem (consider overloaded operations, for example), and the number of attributes or operations is rarely big enough to justify a more complicated algorithm.

For every attribute in proxyInfo, there must be a compatible attribute somewhere in mbeanInfo, determined as follows:

  • the name of the attribute must be the same;
  • if the proxy attribute is readable then the MBean attribute must be readable too;
  • if the proxy attribute is writeable then the MBean attribute must be writeable too;
  • if the proxy attribute is writeable then the MBean attribute must have exactly the same type;
  • if the proxy attribute is not writeable then the MBean attribute's type can also be a subclass of the proxy attribute's type.

These rules allow the MBean to have an attribute that is read/write even though the proxy is read-only or write-only. They are a bit too strict, in that we could allow the type of a write-only attribute in the proxy to be a subclass of the type in the MBean; but write-only attributes hardly ever occur so we ignore that case.

    private static boolean compatibleAttributes(
            ClassLoader loader,
            MBeanAttributeInfo proxyAttrInfo, MBeanAttributeInfo mbeanAttrInfo) {
        if (!proxyAttrInfo.getName().equals(mbeanAttrInfo.getName()))
            return false;
        if (!proxyAttrInfo.getType().equals(mbeanAttrInfo.getType())) {
            if (proxyAttrInfo.isWritable())
                return false; // type must be identical
            if (!isAssignable(loader,
                              proxyAttrInfo.getType(), mbeanAttrInfo.getType()))
                return false;
        }
        if (proxyAttrInfo.isReadable() && !mbeanAttrInfo.isReadable())
            return false;
        if (proxyAttrInfo.isWritable() && !mbeanAttrInfo.isWritable())
            return false;
        return true;
    }

Similar logic applies for operations. The return type of an operation in the MBean can be a subclass of the return type in the proxy but otherwise everything must match exactly.

    private static boolean compatibleOperations(
            ClassLoader loader,
            MBeanOperationInfo proxyOpInfo, MBeanOperationInfo mbeanOpInfo) {
        if (!proxyOpInfo.getName().equals(mbeanOpInfo.getName()) ||
                !isAssignable(loader,
                              proxyOpInfo.getReturnType(),
                              mbeanOpInfo.getReturnType()))
            return false;
        MBeanParameterInfo[] proxyParams = proxyOpInfo.getSignature();
        MBeanParameterInfo[] mbeanParams = mbeanOpInfo.getSignature();
        if (proxyParams.length != mbeanParams.length)
            return false;
        for (int i = 0; i < proxyParams.length; i++) {
            if (!proxyParams[i].getType().equals(mbeanParams[i].getType()))
                return false;
        }
        return true;
    }

Finally, we have to define what it means for a type in anMBeanAttributeInfo or MBeanOperationInfoto be a subclass of another such type, given that types are expressed as strings. Two type strings are obviously compatible if they are equal, but otherwise we must convert those strings into classes that we can compare. To do that we need aClassLoader. The best we can do is to use theClassLoader of the proxy interface. So we allow subclasses if they are known to that ClassLoader.

    private static boolean isAssignable(
            ClassLoader loader, String toClassName, String fromClassName) {
        if (toClassName.equals(fromClassName))
            return true;
        try {
            Class<?> toClass = Class.forName(toClassName, false, loader);
            Class<?> fromClass = Class.forName(fromClassName, false, loader);
            return toClass.isAssignableFrom(fromClass);
        } catch (ClassNotFoundException e) {
            // Could not load one of the two classes so consider not assignable
            // In real code we might like to log the exception
            return false;
        }
    }
}

Perplexing proxy

With that considerable wodge of code, we can now make proxies and be sure they will work. So long as the MBean they're connected to doesn't budge, anyway. In a future version of the JMX API, we may add this functionality to an alternative form ofJMX.newMBeanProxy.

Acknowledgement

The idea for this article came from an e-mail exchange with Sanjay Radia, now at Cassatt.