Inside Class Loaders: Debugging 作者:Andreas Schaefer

类别:Java 点击:0 评论:0 推荐:
Inside Class Loaders: Debuggingby Andreas Schaefer
06/30/2004

Though we discussed the basics of class loading in the previous article in this series, we still need more knowledge before we can delve into the advanced class-loading techniques. This article will show how to solve class-loading problems and to overcome some debugging limitations of the JDK class loaders.

Let me issue a warning here that dealing with class loaders is somewhat risky business, because problems seldom arise during the class-loading process itself. Therefore locating a problem is tricky and time-consuming. That said, custom class loaders can also offer many possibilities regular Java classes cannot achieve, and therefore are essential to advanced applications like servers, code-enhancing tools and dynamic modules. In the end, it is up to the architect to decide if the features outweigh the risks and to make sure that debugging and extended tests are provided.

Tricks of the Trade to Investigate Class-Loading Problems

The biggest challenge in dealing with class-loading problems is that problems rarely manifest themselves during the class-loading process but rather during the usage of a class later on. In addition, the messages provided by the JDK are, to say the least, inadequate, and the toString() methods of the JDK class loaders are not overridden to provide more data.

ClassNotFoundException

Let's consider several different class-loading error scenarios. First, we want to figure out what to do when a class could not be found and the application throws a ClassNotFoundException that looks like this:

java.lang.ClassNotFoundException: Class: com/madplanet/article/classloader/ part2/NotToBeFoundClass could not be found at com.madplanet.article.classloader. part2.MyClassLoader.findClass (MyClassLoader.java:41) at com.madplanet.article. classloader.part2.MyClassLoader. loadClass(MyClassLoader.java:56) at java.lang.ClassLoader. loadClass(ClassLoader.java:235) at com.madplanet.test.classloader. part2.MainTest.testClassNotFound( MainTest.java:48)

Here are some steps to resolve the issue:

Look for the line where your class loader's loadClass() call is involved (in our example, the last line). Figure out what class loader is used there. Figure out the parent class loaders recursively until you find the bootstrap class loader. Figure out why the class cannot be found by any of these class loaders.

This problem is caused when a class loader is asked to load a particular class and it cannot be found. Here are few causes:

An archive, directory, or other source for the classes was not added to the class loader asked to load the class, or to its parent. A class loader's parent is not set correctly. The wrong class loader is used to load the class in question.

Point two seems to be a little bit strange, but when a class loader is created, its parent is set once and for all, and does affects class loadings. For the simplicity of the example, let us assume that an application class loader would have the bootstrap class loader set as its parent. This means that this class loader cannot load any class added to the class path because these are made available through the System class loader. This scenario will rarely occur, except deliberately, because the bootstrap class loader is not made available like this. A case for doing this would be when an application wants to prevent users from adding classes via the class path.

Point three can happen more easily than you might think. For example, in J2EE, an EJB is required to use the thread context class loader (Thread.currentThread.getContextClassLoader()) to load any class from its application. It is up to the discretion of the application server to specify the class loader of the EJB itself. I've seen many applications fail because developers used Class.forName without specifying the thread context class loader as the current class loader.

Later I will show a way to figure out all of the necessary information needed here with a simple class loader toString() method call, even for JDK class loaders. If you still cannot figure out why it is failing, consider the possibility that a class loader is not following the JDK delegation model or is delegating to class loaders other than the parent.

Symbolic-Link Class Not Found

Another problem arises if a symbolic reference cannot be found, in which case a NoClassDefFoundError will be thrown, indicating that there is a linkage problem. That means the code has been successfully compiled, but at runtime, the class can no longer be found. This error is sometimes thrown between the loading of the class containing the reference and the use of the reference. The stack trace looks like this:

java.lang.NoClassDefFoundError: com/madplanet/article/classloader /part2/ClassNotAvailableDuringRuntime at com.madplanet.article.classloader .part2.ClassWithFailingReference.test (ClassWithFailingReference.java:24) at sun.reflect.NativeMethodAccessorImpl .invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl .invoke(NativeMethodAccessorImpl.java:39) at sun.reflect. DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:25)

As you can see, the outer class ClassWithFailingReference was created without a problem and then fails when test() method is invoked. This demonstrates that a class can be successfully loaded and used and still fail later when a particular reference is used. It is up to the JDK to decide when it attempts to load the class (and thus throw the error) due to the lazy class loading allowed in the Java language specification.

Note: In contrast to the previous problem, here the class loading is done implicitly by the JDK instead of invoking a class loader's loadClass() method. In addition, an Error is thrown in this case instead of an exception, and some applications tend to swallow errors.

Here are some steps to investigate and solve the problem:

Find the NoClassDefFoundError and print out its stack trace. Find the topmost line in the stack trace that is not class-loader-related. This most likely indicates the class containing the symbolic link that was not found. Figure out the class loader of the class containing the offending symbolic link. List the parent class loaders recursively. Figure out why the class is not available to any of these class loaders.

This error is thrown when an implicit class loading fails. Here are a few potential causes:

An archive, directory, or other source for the classes was not added to the class loader asked to load the class, or to its parent. A class loader's parent is not set correctly. Symbolic links in a class are unaccessible by the containing class's class loader, such as a child class loader.

The first two points are the same as for the "class not found" problem and should be treated the same way, with the only difference being the the implicit class loading.

The most likely reason is that a class is loaded by a class loader with references that are in an inaccessible class loader, such as a child class loader. Assuming Log4J is loaded by a J2EE server class loader that is a child of the System class loader (CL stands for class loader):

-- Bootstrap CL +-- System CL +-- J2EE server CL -> log4j.jar

To debug a problem, an archive is added to the class path containing a patched Log4J Category class:

-- Bootstrap CL +-- System CL -> org.apache.log4j.Category +-- J2EE server CL -> log4j.jar

When the J2EE server is started, Log4J classes are loaded by the server's class loader except for the patched Category, which is loaded by the System class loader. But this Category contains symbolic links to other Log4J classes (such as the class Priority), which are unavailable to the System class loader because they are only available to the server's class loader. To solve this problem, it is not only necessary to check if a class is made available to a class loader, but also to a class loader that is accessible by the class loader of the containing class. To make matters more complicated, the accessibility depends on the delegation model, and for the JDK's model, this means that any symbolic link class must be made available to the same class loader or a parent class loader of the containing class.

In a situation where there is no way to correct the problem, the only other solution is not to use a symbolic link but instead load this class manually and use reflection to use the instance. This is especially important when the class is not available on the class' class loader but the Thread context class loader, as in J2EE.

Note: A custom class loader has the freedom to delegate requests to any class loader, such as a child class loader or a class loader on another branch (any class loader is at least a child of the bootstrap class loader except the bootstrap class loader itself) at any time in its loadClass() method. The only exception are classes in the java. package that can only be loaded by the bootstrap class loader. So be warned and expect the unexpected when a custom class loader overrides loadClass().

Native Library Problems

One of the nastier problems actually has nothing to do with the class loader, but for the sake of completeness I will discuss it here. Whenever a native library is loaded (via System.loadLibrary() or Runtime.loadLibrary()), the JVM retrieves the class loader of the calling class. Whenever the archive is already loaded by a class with another class loader, the loading will fail with an UnsatisfiedLinkError like this:

java.lang.UnsatisfiedLinkError: Native Library C:\temp\dll.loading.tests\ myruntime.dll already loaded in another classloader at java.lang.ClassLoader.loadLibrary0( ClassLoader.java:1525) at java.lang.ClassLoader.loadLibrary( ClassLoader.java:1456) at java.lang.Runtime.load0(Runtime.java:737) at java.lang.System.load(System.java:811) at foo.dll.TestClass.load(TestClass.java:28)

The only way to resolve this issue is to make sure that you call loadLibrary() or load() from a class that is loaded by a general class loader (like the System class loader) as well as the classes containing the native method declarations (JNI bridge classes). If the JNI bridge classes are loaded by a different class loader the calls to it will fail, too. To solve these issues, I would suggest the following steps for an environment with multiple class loaders:

Create a central repository to load the native libraries as well as the JNI bridge classes. Any JNI bridge classes should only contain the native method declarations. Provide an archive that only contains the JNI bridge classes for a particular native library. Remember that his error has nothing to do with a class not being found, but with a mismatch of class loaders accessing native code.

Also remember that you will get UnsatisfiedLinkError when your bridge classes are not set up correctly, including situations where you have the wrong package name, class name, method name or method signature; all of which can be avoided by running simple tests.

Patching the JDK's Class Loader for More Debug Information

Due to the simple implementation of the JDK's class loaders, retrieving useful problem-solving data is painful and time consuming. One way to speed things up is to patch the JDK class loaders by adding a toString() method that prints out the resources it can load and the class loader it derives from, and then calls the toString() method of the parent, if set. With this, a single ClassLoader.toString() call provides you with all of the data you need. These are the steps to patch the JDK's class loaders:

Take the source of java.lang.ClassLoader, java.security.SecureClassLoader and java.net.URLClassLoader and copy them to a safe place. Add the desired toString() method and make sure that every class loader prints out its own address by using super.toString() so that you can test if class loaders from the same class are identical or not. Jar them up Add -Xbootclasspath/p:<the archive you just have created> to your script that starts Java.

Because we used /p: for the bootclasspath, the given archive(s) are loaded by the bootstrap class loader before any other archive, including rt.jar.

Note: Please do not patch classes of your own this way, because if any of these patched classes contain a symbolic link to an unpatched class, you will get a linkage error. This is because your classes are made available to the System class loader or other child class loaders of the bootstrap class loader (unless you added your archives to the JDK's lib directory, which is not very smart).

Now let us have a look at the patch code to see how you get your information about the class loading process. Note that java.lang.ClassLoader and java.security.SecureClassLoader do not need to be patched in this case, but when you want to add trace-logging statements into the loading process you can do so.

Patching java.net.URLClassLoader

We need three pieces of information from the class loader. First, we need an indicator of the instance, because we need to distinguish two instances of the same class. Then we need to know which archives are available to this class loader, so that we can check later if an archive/class is missing. Finally, we need to print out the information from the parent class loader, if it is available. For example, the System class loader must call toString() on the bootstrap class loader. This would look like the following:

public String toString() { if( getParent() != null ) { return "java.net.URLClassLoader:\n" + "hashcode: " + hashCode() + "\n" + "URLs: " + java.util.Arrays.asList( getURLs() ) + "\n" + "parent { " + getParent() + " }\n"; } else { return "java.net.URLClassLoader:\n" + "hashcode: " + hashCode() + "\n" + "URLs: " + java.util.Arrays.asList( getURLs() ) + "\n"; } }

Our code first checks if there is a parent. If so, we print the name of the class, its hashcode (which is a pretty good indicator for checking two instances), and then the list of archives available to the class loader. Finally, we delegate the call to the parent, if a parent is available. A printout can look like this:

Note: Indentation and wrapping were added by hand to illustrate the point!

java.net.URLClassLoader: hashcode: 33513127 URLs: [ file:/C:/private/madplanet/investigations/ classloader.part2.advanced/target/test-classes/, file:/C:/private/madplanet/investigations/ classloader.part2.advanced/target/classes/, file:/C:/Documents%20and%20Settings/aschaefer/ .maven/repository/log4j/jars/log4j-1.2.8.jar, file:/C:/Documents%20and%20Settings/aschaefer/ .maven/repository/junit/jars/junit-3.8.1.jar, file:/C:/java/maven-1.0-rc1/lib/ant-1.5.3-1.jar, file:/C:/java/maven-1.0-rc1/lib/ ant-optional-1.5.3-1.jar ] parent { java.net.URLClassLoader: hashcode: 10430987 URLs: [ file:/C:/java/jdk.1.4.2/jre/lib/ext/ dnsns.jar, file:/C:/java/jdk.1.4.2/jre/lib/ext/ ldapsec.jar, file:/C:/java/jdk.1.4.2/jre/lib/ext/ localedata.jar, file:/C:/java/jdk.1.4.2/jre/lib/ext/ sunjce_provider.jar ] }

It looks bad here, but in a log file it is quite readable. The listings show all the archives and directories of the System class loader (the upper part) and the Bootstrap class loader (the lower part). This should help to find the missing archives or directory.

By the way, if you like or need indentation, you can either add the method toString( int indentationLevel ) to the class loaders, but then you need to change your code when you patch the class loaders. You could also use a Thread Local to carry the indentation, which requires to create the thread local instance before you invoke toString on the class loader. Even though I deal with class loaders quite often, I prefer a solution where I do not have to change my code at all, even it requires me to look harder or do the indentation by hand. Of course, class-loader problems should not be our daily business, anyway.

Patching Your Own Class Loaders

I do prefer to change my own class loaders so that they contain a name, which can be used to determine which class loader is which. This allows me to provide a human-readable indicator for the class loader instance instead of relying on the hashcode. For example, in a J2EE server you could add the enterprise application name to the class loader so that when you print out the class loader, you know to which application it belongs. This, of course, requires you to change the class loader as well as the classes where the class loader are created, but I think it is worthwhile. Implementing a class loader name looks like this:

public class MyClassLoader extends ... { private String mName; public MyClassLoader(String pName, ... ) { super(); mName = pName; } ... public String toString() { return "MyClassLoader:\n" + "name: " + mName + "\n" + <print out the what this class loader can load> + "parent { " + getParent() + " }\n"; } ...

If you cannot change the code, you can still patch your class loaders the same way we did with the java.net.URLClassLoader, by overriding the toString() method. But keep in mind that you must add the patched class loader to the same class loader to which the non-patched classes is added. So if you add your class loader to the class path, then you must add the patch archive to the class patch as well, but just in front of the archive with the regular class loader. Adding the patched classes to the same class loader as the non-patched classes will prevent class-loading problems no matter what the delegation model.

Conclusion

Dealing with a class-loader problem is a time-consuming process, no matter what you do; preventing a class-loading problem in the first place is much faster than dealing with it. Therefore, I encourage you to write as many tests as you can and try to execute them in an environment that is as close as possible to the actual runtime environment. Linkage errors are caused by a runtime class loader environment that is different than the compilation environment. Normally, during compilation you have a flat class-loader hierarchy, adding all of the archives you need to a single class loader. At runtime, you often have a hierarchical class-loader structure for various reasons, which requires a great deal of testing. Nevertheless, it is pretty easy to run into a class-loading problem after all, especially when your code is hosted in some sort of a container, such as a J2EE server, a web server, etc. In this case, patched class loaders enable you to retrieve the data necessary to pin down a problem faster and easier than with either "trial and error" or by running a debugger, which is not always possible.

I wish you good luck in hunting your very own class-loading problems. Hopefully, the techniques discussed here will help.

本文地址:http://com.8s8s.com/it/it15828.htm