众所周知,Java的类加载机制采用了双亲委派模型,导致在进行类加载的时候会有多个加载器,这种复杂的机制,有时候会导致‘Exception in thread main java.lang.NoClassDefFoundError’这个异常,虽然可能你认为相应的类和jar包就在某个类加载器中。下面的文字,会试图尝试解释为什么会发生这种情况。
下面提供了一个简单的java程序来帮助理解问题的发生。
默认的JVM的类加载委派模型
默认的类加载委派模式是从下向上的,也就是双亲委派。这意味着JVM会从下往上来委托类加载器去查找和加载用户的类,如果父加载器没能加载相应的类,这时才会尝试在当前线程上下文的类加载器中去加载,一般而言是子加载器。
NoClassDefFoundError这种异常时有发生,比如,用户自己的jar包打的不对,再比如,依赖的第三方jar包或者容器注入的类型,这些情况都有可能导致问题的发生。
在以上这些场景中:
- JVM将程序的部分类型在父加载器中加载了(比如系统或者父加载器)
- JVM将程序的其他部分想要在子加载器中加载(比如容器或者用户自定义加载器)
当在父加载器中加载的类尝试去通过子加载器加载相应的类时会发生什么?当然是NoClassDefFoundError!
因为父加载器对子加载器一无所知。只有当引用的类在是父加载器或当前线程上下文加载器中,才可能会去加载,否则就会抛出java.lang.NoClassDefFoundError。
下面的代码将会展示这些问题:
样例Java程序
为了演示,程序被如下分割:
- 主程序NoClassDefFoundErrorSimulator会被打包在MainProgram.jar
- 日志输出程序JavaEETrainingUtil也被打包在MainProgram.jar
- caller类CallerClassA被打包在caller.jar
- 被引用的类ReferencingClassA被打包在referencer.jar
然后执行下面的任务:
- 创建一个子加载器(java.net.URLClassLoader)
- 将caller.jar和referencher.jar分配给上面创建的子加载器
- 将当前线程上下文加载器更改为上面的子加载器
- 尝试从当前线程上下文加载器加载和创建CallerClassA类的实例
- 日志输出用来帮助理解类加载器的变化和线程上下文加载器的状态
下面将展示错误的打包方式和默认的类加载委派模型是如何产生NoClassDefFoundError这个异常的。
** JavaEETrainingUtil source code can be found from the article part #2
1 #### NoClassDefFoundErrorSimulator.java 2 package org.ph.javaee.training3; 3 4 import java.net.URL; 5 import java.net.URLClassLoader; 6 7 import org.ph.javaee.training.util.JavaEETrainingUtil; 8 9 /** 10 * NoClassDefFoundErrorSimulator 11 * @author Pierre-Hugues Charbonneau 12 * 13 */ 14 public class NoClassDefFoundErrorSimulator { 15 16 /** 17 * @param args 18 */ 19 public static void main(String[] args) { 20 21 System.out.println("java.lang.NoClassDefFoundError Simulator - Training 3"); 22 System.out.println("Author: Pierre-Hugues Charbonneau"); 23 System.out.println("http://javaeesupportpatterns.blogspot.com"); 24 25 // Local variables 26 String currentThreadName = Thread.currentThread().getName(); 27 String callerFullClassName = "org.ph.javaee.training3.CallerClassA"; 28 29 // Print current ClassLoader context & Thread 30 System.out.println(" Current Thread name: '"+currentThreadName+"'"); 31 System.out.println("Initial ClassLoader chain: "+JavaEETrainingUtil.getCurrentClassloaderDetail()); 32 33 try { 34 // Location of the application code for our child ClassLoader 35 URL[] webAppLibURL = new URL[] {new URL("file:caller.jar"),new URL("file:referencer.jar")}; 36 37 // Child ClassLoader instance creation 38 URLClassLoader childClassLoader = new URLClassLoader(webAppLibURL); 39 40 /*** Application code execution... ***/ 41 42 // 1. Change the current Thread ClassLoader to the child ClassLoader 43 Thread.currentThread().setContextClassLoader(childClassLoader); 44 System.out.println(">> Thread '"+currentThreadName+"' Context ClassLoader now changed to '"+childClassLoader+"'"); 45 System.out.println(" New ClassLoader chain: "+JavaEETrainingUtil.getCurrentClassloaderDetail()); 46 47 // 2. Load the caller Class within the child ClassLoader... 48 System.out.println(">> Loading '"+callerFullClassName+"' to child ClassLoader '"+childClassLoader+"'..."); 49 Class<?> callerClass = childClassLoader.loadClass(callerFullClassName); 50 51 // 3. Create a new instance of CallerClassA 52 Object callerClassInstance = callerClass.newInstance(); 53 54 } catch (Throwable any) { 55 System.out.println("Throwable: "+any); 56 any.printStackTrace(); 57 } 58 59 System.out.println(" Simulator completed!"); 60 } 61 }
1 #### CallerClassA.java 2 package org.ph.javaee.training3; 3 4 import org.ph.javaee.training3.ReferencingClassA; 5 6 /** 7 * CallerClassA 8 * @author Pierre-Hugues Charbonneau 9 * 10 */ 11 public class CallerClassA { 12 13 private final static Class<CallerClassA> CLAZZ = CallerClassA.class; 14 15 static { 16 System.out.println("Class loading of "+CLAZZ+" from ClassLoader '"+CLAZZ.getClassLoader()+"' in progress..."); 17 } 18 19 public CallerClassA() { 20 System.out.println("Creating a new instance of "+CallerClassA.class.getName()+"..."); 21 22 doSomething(); 23 } 24 25 private void doSomething() { 26 27 // Create a new instance of ReferencingClassA 28 ReferencingClassA referencingClass = new ReferencingClassA(); 29 } 30 }
1 #### ReferencingClassA.java 2 package org.ph.javaee.training3; 3 4 /** 5 * ReferencingClassA 6 * @author Pierre-Hugues Charbonneau 7 * 8 */ 9 public class ReferencingClassA { 10 11 private final static Class<ReferencingClassA> CLAZZ = ReferencingClassA.class; 12 13 static { 14 System.out.println("Class loading of "+CLAZZ+" from ClassLoader '"+CLAZZ.getClassLoader()+"' in progress..."); 15 } 16 17 public ReferencingClassA() { 18 System.out.println("Creating a new instance of "+ReferencingClassA.class.getName()+"..."); 19 } 20 21 public void doSomething() { 22 //nothing to do... 23 } 24 }
问题重现
为了复现问题,简单的将caller和referencing分别打包并且赋给不同的加载器。现在,可以先以正确的jar包部署来执行程序:
- 主程序和工具包被部署在父加载器 (SYSTEM classpath)
- CallerClassA 和ReferencingClassA被部署在子加载器中
1 ## Baseline (normal execution) 2 <JDK_HOME>in>java -classpath MainProgram.jar org.ph.javaee.training3.NoClassDefFoundErrorSimulator 3 4 java.lang.NoClassDefFoundError Simulator - Training 3 5 Author: Pierre-Hugues Charbonneau 6 http://javaeesupportpatterns.blogspot.com 7 8 Current Thread name: 'main' 9 Initial ClassLoader chain: 10 ----------------------------------------------------------------- 11 sun.misc.Launcher$ExtClassLoader@17c1e333 12 --- delegation --- 13 sun.misc.Launcher$AppClassLoader@214c4ac9 **Current Thread 'main' Context ClassLoader** 14 ----------------------------------------------------------------- 15 16 >> Thread 'main' Context ClassLoader now changed to 'java.net.URLClassLoader@6a4d37e5' 17 18 New ClassLoader chain: 19 ----------------------------------------------------------------- 20 sun.misc.Launcher$ExtClassLoader@17c1e333 21 --- delegation --- 22 sun.misc.Launcher$AppClassLoader@214c4ac9 23 --- delegation --- 24 java.net.URLClassLoader@6a4d37e5 **Current Thread 'main' Context ClassLoader** 25 ----------------------------------------------------------------- 26 27 >> Loading 'org.ph.javaee.training3.CallerClassA' to child ClassLoader 'java.net.URLClassLoader@6a4d37e5'... 28 Class loading of class org.ph.javaee.training3.CallerClassA from ClassLoader 'java.net.URLClassLoader@6a4d37e5' in progress... 29 Creating a new instance of org.ph.javaee.training3.CallerClassA... 30 Class loading of class org.ph.javaee.training3.ReferencingClassA from ClassLoader 'java.net.URLClassLoader@6a4d37e5' in progress... 31 Creating a new instance of org.ph.javaee.training3.ReferencingClassA... 32 33 Simulator completed!
在基准测试中,主程序可以正常运行,CallerClassA和他引用的类都可以被加载和实例化。
现在再次尝试运行程序,但是以一种错误的打包和部署方式来进行:
- 主程序和工具类都被部署在父加载器(SYSTEM classpath)
- CallerClassA和ReferencingClassA被部署在子加载器
- CallerClassA (caller.jar)也被部署在父加载器
1 ## Problem reproduction run (static variable initializer failure) 2 <JDK_HOME>in>java -classpath MainProgram.jar;caller.jar org.ph.javaee.training3.NoClassDefFoundErrorSimulator 3 4 java.lang.NoClassDefFoundError Simulator - Training 3 5 Author: Pierre-Hugues Charbonneau 6 http://javaeesupportpatterns.blogspot.com 7 8 Current Thread name: 'main' 9 Initial ClassLoader chain: 10 ----------------------------------------------------------------- 11 sun.misc.Launcher$ExtClassLoader@17c1e333 12 --- delegation --- 13 sun.misc.Launcher$AppClassLoader@214c4ac9 **Current Thread 'main' Context ClassLoader** 14 ----------------------------------------------------------------- 15 16 >> Thread 'main' Context ClassLoader now changed to 'java.net.URLClassLoader@6a4d37e5' 17 18 New ClassLoader chain: 19 ----------------------------------------------------------------- 20 sun.misc.Launcher$ExtClassLoader@17c1e333 21 --- delegation --- 22 sun.misc.Launcher$AppClassLoader@214c4ac9 23 --- delegation --- 24 java.net.URLClassLoader@6a4d37e5 **Current Thread 'main' Context ClassLoader** 25 ----------------------------------------------------------------- 26 27 >> Loading 'org.ph.javaee.training3.CallerClassA' to child ClassLoader 'java.net.URLClassLoader@6a4d37e5'... 28 Class loading of class org.ph.javaee.training3.CallerClassA from ClassLoader 'sun.misc.Launcher$AppClassLoader@214c4ac9' in progress...// Caller is loaded from the parent class loader, why??? 29 Creating a new instance of org.ph.javaee.training3.CallerClassA... 30 Throwable: java.lang.NoClassDefFoundError: org/ph/javaee/training3/ReferencingClassA 31 java.lang.NoClassDefFoundError: org/ph/javaee/training3/ReferencingClassA 32 at org.ph.javaee.training3.CallerClassA.doSomething(CallerClassA.java:27) 33 at org.ph.javaee.training3.CallerClassA.<init>(CallerClassA.java:21) 34 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) 35 at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source) 36 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source) 37 at java.lang.reflect.Constructor.newInstance(Unknown Source) 38 at java.lang.Class.newInstance0(Unknown Source) 39 at java.lang.Class.newInstance(Unknown Source) 40 at org.ph.javaee.training3.NoClassDefFoundErrorSimulator.main(NoClassDefFoundErrorSimulator.java:51) 41 Caused by: java.lang.ClassNotFoundException: org.ph.javaee.training3.ReferencingClassA 42 at java.net.URLClassLoader$1.run(Unknown Source) 43 at java.net.URLClassLoader$1.run(Unknown Source) 44 at java.security.AccessController.doPrivileged(Native Method) 45 at java.net.URLClassLoader.findClass(Unknown Source) 46 at java.lang.ClassLoader.loadClass(Unknown Source) 47 at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source) 48 at java.lang.ClassLoader.loadClass(Unknown Source) 49 ... 9 more 50 51 Simulator completed!
发生什么了?
- 主程序和工具类都正常的从父加载器加载成功 (sun.misc.Launcher$AppClassLoader)
- 线程上下文加载器被更改为包含caller和reference的子加载器。
- 但是我们会看到CallerClassA 会被父加载器加载 (sun.misc.Launcher$AppClassLoader) ,并没有被子加载器加载
- 由于ReferencingClassA 没有被部署在父加载器, 所以他不能在当前加载器中加载,又由于父加载器对子加载器的无知,所以也不可能被子加载器加载。然后就报错NoClassDefFoundError。
关键点在于理解为什么CallerClassA会被父加载器加载。答案就在于默认的类加载委派模型,尽管子加载器和父加载器都包含caller的jar包,但默认的委派模型是父优先,这就导致caller只会在父加载器中被加载。但同时caller引用的ReferencingClassA却被部署在了子加载器,所以java.lang.NoClassDefFoundError自然会发生。
正如你所见,由于默认的类加载委托模型的存在,代码的打包或者第三方的api都会导致这个问题的发生。所以,很重要的事情就是,你需要检视你的类加载器链条,确定是否有相同的代码或类库被重复部署在不同的父加载器和子加载器中。
建议和策略
如下是给出的建议和策略:
- 仔细检视异常java.lang.NoClassDefFoundError并确定到底是哪个类导致问题
- 检视受影响的程序的打包方式,看看是否能找到是相应的类存在重复或者错误的部署方式。(SYSTEM class path, EAR file, Java EE container itself etc.).
- 如果找到了,那就需要将响应的类库从受影响的类加载器中移除。(有时会很复杂)
- 启用jvm的属性verbose,比如–verbose:class很有帮助,会帮助你定位类是从哪个加载器中加载的。
翻译自:https://javaeesupportpatterns.blogspot.com/2012/08/javalangnoclassdeffounderror-parent.html