Vitaly has posted 10 posts at DZone. View Full User Profile

A Tale of Four JVMs and One App Server

03.13.2009
| 8173 views |
  • submit to reddit

This article proposes a way to reveal latent bugs in Java applications by running them on different JVMs. It is illustrated with a real world example involving a popular Java application server.

Starting point

A user has recently reported in the JBoss issue tracker that JBoss AS 5.0.0 GA fails to start on the IBM JRE. One may say: So what? Didn't you hear that the IBM JRE "fits better" for another app server?

Independently, our QA team has encountered the same problem when testing JBoss 5.0 on a new version of Excelsior JET JVM. A quick check confirmed that the problem manifests itself on Oracle JRockit as well.

At the same time, on the Sun JRE everything worked like a charm...

Well, I respect the Sun reference implementation but something suggested to me that the other three JVMs, which all support Java 6 and passed the Sun JCK, cannot have exactly the same bug.

Further investigation has shown that the issue was in the Java code that worked on the Sun JRE by a lucky coincidence.

Just follow the specification

The root cause of the problem is that JBoss 5.0 (intentionally or not) relies on the order of elements in the array returned by the method

Class.getDeclaredConstructors()

and under the Sun JRE the order happens to be "right". However, the Java SE API specification says "...The elements in the array returned are not sorted and are not in any particular order."

The full transcript of the root cause analysis of that issue is available in this Excelsior's blog post. We have also posted a comment to the JBoss issue tracker.

Now one may say: I don't care about it. I deployed, deploy, and will deploy my application with the Sun JRE whatever other Java implementations would offer. OK, no problem but...

Kind of magic

Consider this simple program that reflects the declared constructors and prints the result:

import java.lang.reflect.*;

class Test{
Test() {}
Test(Object o) {}
Test(String s) {}

public static void main(String args[]){
for (Constructor c: Test.class.getDeclaredConstructors())
System.out.println(c);
}
}

If run on the Sun JRE 6, it prints:

    Test()
Test(java.lang.Object)
Test(java.lang.String)

So far so good. It looks like the constructors appear in the order they are declared in the source code. Let’s add a few instance methods to the code to make it a little bit more realistic:


void dont() {}
void rely() {}
void on () {}
void it () {}

and run the program on the same version of Sun JRE. Now it prints:

    Test(java.lang.String)
Test(java.lang.Object)
Test()

Surprise! If you want to find the rationale behind this behavior, you may dig into the OpenJDK sources starting from the method sort_methods() in the file hotspot\src\share\vm\runtime\classfileparser.cpp.

The lesson learned: Java applications may not rely on JVM features that are not enforced by the Java specification.

On speculative assumptions

Some observant readers may notice that the order of constructors from the examples above matches either direct or reverse order of their declaration in the source code. I however would not recommend to exploit this observation. Just add one more method:

    void ever() {}

to the code and run it to see yet another (different) order of the reflected constructors!

I must say that I did not get the last example out of my head. The quite popular JNA library used to rely on exactly this assumption regarding the method Class.getDeclaredFields(): the order of reflected fields was deemed either direct or reverse. Fortunately, in JNA 3.0.5, “support for JVMs with unpredictable order of fields” has been added though I would rather call it “improved compliance with the Java specification”.

Conclusion

Neglecting the Java spec, you still have a chance to get a working application. But you also have a chance that it will stop working if you modify the code or upgrade the underlying JVM. As a result, you may have to spend (waste) time on hunting subtle bugs or, even worse, get stuck with an outdated Java version if such bugs lurk in third-party components (yeah, J2SE 1.4.2 still has its “thankful” audience).

What would be a solution for this problem? I cannot imagine a “rule checker” that tests Java applications for compliance with the Java spec. Implementing such a tool is unlikely possible because the semantics of programs is involved.

For now, the only practical option is testing your Java application on different JVMs provided they support the same Java version (with the same level of Java compatibility). If the application does not work on one or more of them, it may be an issue worth investigating.

P.S. By the way, another large cluster of latent bugs that this testing approach can help you reveal is data races (aka random features) in multi-thread Java applications but that is another tale.

Published at DZone with permission of its author, Vitaly Mikheev.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Alex Scott replied on Fri, 2009/03/13 - 9:26am

I'm trying this on Ubuntu with 'Java(TM) SE Runtime Environment (build 1.6.0_07-b06)'. If I try running with;

       for(Constructor c : Test.class.getDeclaredConstructors()) {

I get only;

run:
Test()
BUILD SUCCESSFUL (total time: 1 second)

 

However if I change that line to;

         for(Method c : Test.class.getDeclaredMethods()) {

I get;

run:
public static void Test.main(java.lang.String[])
void Test.Test(java.lang.Object)
void Test.Test(java.lang.String)
void Test.Test()
BUILD SUCCESSFUL (total time: 1 second)

Vitaly Mikheev replied on Fri, 2009/03/13 - 9:51am

>>I get only;

>>run:
>>Test()
>>BUILD SUCCESSFUL (total time: 1 second)

How do you build and run it?

It seems that stdout was not flushed before the process terminated

Alex Scott replied on Fri, 2009/03/13 - 10:49am

Build and running inside netbeans (6.5). Even adding a flush() only gives me one Constructor.

    public static void main(String[] args) {
        Properties p = System.getProperties();
        Enumeration keys = p.keys();
        while (keys.hasMoreElements()) {
          String key = (String)keys.nextElement();
          String value = (String)p.get(key);
          System.out.println(key + ": " + value);
        }

       for(Constructor c : Test.class.getDeclaredConstructors()) {
            System.out.println(c);
        }
       System.out.flush();
    }

Gives;

run:
java.runtime.name: Java(TM) SE Runtime Environment
sun.boot.library.path: /usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/i386
java.vm.version: 10.0-b23
java.vm.vendor: Sun Microsystems Inc.
java.vendor.url: http://java.sun.com/
path.separator: :
java.vm.name: Java HotSpot(TM) Server VM
file.encoding.pkg: sun.io
sun.java.launcher: SUN_STANDARD
user.country: GB
sun.os.patch.level: unknown
java.vm.specification.name: Java Virtual Machine Specification
user.dir: /home/alexscott/Projects/JavaApplication2
java.runtime.version: 1.6.0_07-b06
java.awt.graphicsenv: sun.awt.X11GraphicsEnvironment
java.endorsed.dirs: /usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/endorsed
os.arch: i386
java.io.tmpdir: /tmp
line.separator:

java.vm.specification.vendor: Sun Microsystems Inc.
os.name: Linux
sun.jnu.encoding: UTF-8
java.library.path: /usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/i386/server:/usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/i386:/usr/lib/jvm/java-6-sun-1.6.0.07/jre/../lib/i386:/usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/i386/client:/usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/i386:/usr/lib/jvm/java-6-sun-1.6.0.07/jre/../lib/i386:/usr/java/packages/lib/i386:/lib:/usr/lib
java.specification.name: Java Platform API Specification
java.class.version: 50.0
sun.management.compiler: HotSpot Tiered Compilers
os.version: 2.6.24-23-generic
user.home: /home/alexscott
user.timezone:
java.awt.printerjob: sun.print.PSPrinterJob
file.encoding: UTF-8
java.specification.version: 1.6
java.class.path: /home/alexscott/Projects/JavaApplication2/build/classes:/home/alexscott/Projects/JavaApplication2/src
user.name: alexscott
java.vm.specification.version: 1.0
java.home: /usr/lib/jvm/java-6-sun-1.6.0.07/jre
sun.arch.data.model: 32
user.language: en
java.specification.vendor: Sun Microsystems Inc.
java.vm.info: mixed mode
java.version: 1.6.0_07
java.ext.dirs: /usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/ext:/usr/java/packages/lib/ext
sun.boot.class.path: /usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/resources.jar:/usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/rt.jar:/usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/sunrsasign.jar:/usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/jsse.jar:/usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/jce.jar:/usr/lib/jvm/java-6-sun-1.6.0.07/jre/lib/charsets.jar:/usr/lib/jvm/java-6-sun-1.6.0.07/jre/classes
java.vendor: Sun Microsystems Inc.
file.separator: /
java.vendor.url.bug: http://java.sun.com/cgi-bin/bugreport.cgi
sun.io.unicode.encoding: UnicodeLittle
sun.cpu.endian: little
sun.desktop: gnome
sun.cpu.isalist:
public Test()
BUILD SUCCESSFUL (total time: 1 second)
 

Hung Huynh replied on Fri, 2009/03/13 - 4:02pm

This is what I got on Vista

 

jdk1.5.0_16
com.tc.license.util.Test(java.lang.Object)
com.tc.license.util.Test(java.lang.String)
com.tc.license.util.Test()

jdk1.6.0_11
com.tc.license.util.Test()
com.tc.license.util.Test(java.lang.Object)
com.tc.license.util.Test(java.lang.String)

 

 

Vitaly Mikheev replied on Fri, 2009/03/13 - 4:11pm in response to: Hung Huynh

Yeah, the order is different for JDK1.5 and JDK1.6

Osvaldo Doederlein replied on Sun, 2009/03/15 - 1:20pm

Some time agor I posted a similar story here. You can run into such screwups easily, with much simpler APIs, and you don't haven have to move to other vendor's JVM... such bugs are 100% developer's fault, they are not the JVM's fault.

I always test my code on a seconday JVM (different provider than the production JVM's), just to make sure that I don't have such kinds of bugs. Otherwise, the application could blow up even the next maintenance release of the same JVM from the same vendor.

Vitaly Mikheev replied on Mon, 2009/03/16 - 5:31pm

Hi Osvaldo,

It’s great to hear from you again.

>>Some time ago I posted a similar story here.

(Bad) news are that I’m going to post a similar story about the Eclipse Runtime (Equinox) that is fundamentally flawed and fragile when the starting of OSGi bundles is concerned. Today it works, not sure about tomorrow ;)

>>I always test my code on a seconday JVM (different provider than the production JVM's)

It’s professionally.

Vitaly Mikheev

Excelsior Java Team

http://www.excelsior-usa.com/jet.html

Grame smith1 replied on Tue, 2009/04/28 - 8:26am

As a result, you may have to spend (waste) time on hunting subtle bugs or, even worse, get stuck with an outdated Java version if such bugs lurk in third-party components (yeah, J2SE 1.4.2 still has its “thankful” audience). label design | Custom Logo Design | brochure design

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.