相信很多Java开发人员都对这个常见却不招人待见的java.lang.ClassNotFoundException并不陌生。出现这个异常的原因大家都清楚(classpath路径下缺少class文件或者jar包了,或者是类加载器委派的问题等),不过对于它给JVM带来的性能影响可能就不了解了。这个异常可能会严重影响应用程序的响应时间和可伸缩性。
大型的J2EE企业级应用,可能会同时部署有多个应用,由于同时运行着多个类加载器,就很容易导致这类问题。除非发现有业务确实受到影响或者日志监控比较详细,否则很有可能出现了ClassNotFoundException你也不知道,这导致的结果是:JVM类加载的IO开销和线程间的锁竞争给系统带来的性能问题。
本文及文中的示例程序将会告诉你,应当谨慎对待生产系统中出现的ClassNotFoundException异常,并正确的解决它。
Java类加载:性能优化的死角
想要正确理解这个性能问题,首先你得明白Java类加载模型的机制。ClassNotFoundException意味着JVM无法找到或者加载某个Java类:
Class.forName()方法
ClassLoader.findSysteClass()方法
ClassLoader.loadClass()方法
尽管在JVM的生命周期内,你的应用程序里面的Java类应该只会加载一次,但有些应用可能会依赖于动态的类加载机制。
不管怎么说,不停地加载失败总是很影响性能的,尤其是JDK中默认的java.lang.ClassLoader进行加载的时候。事实上,1.7以上版本的JDK,为了向下兼容,同一个类不能同时被加载,除非这个类加载器被标记为“支持并发”的。你要时刻牢记,同步操作是在类这一级实现的,由于多个Java线程并发地加载,同一个类不停的加载失败会引发线程间的锁竞争。如果是JDK1.6以前则情况更糟糕,因为同步操作是在类加载器这一层完成的。
由于这个原因,像JBoss WildFly8等JavaEE容器都使用了它们内部的并发类加载器来完成应用程序的类加载。这些类加载器都在更细粒度上进行加锁,这样同一个类加载器的实例可以同时加载多个不同的类。这和JDK1.7的优化不谋而和,它支持的并发的自定义类加载器也是防止类加载器出现死锁的现象。
不过,像java.*的系统级别的类,它们的加载还是会由JDK默认的类加载器来完成。这意味着连续的类加载失败还是可能会引发严重的线程锁竞争。这也正是本文接下来要重现并演示的场景。
线程锁竞争——问题复现
为了重现并模拟这个问题,我们按照以下的规范写了一个简单的程序:
一个JAX-RS WEB服务执行Class.forName()来加载一个系统包下不存在的类:
String className =”java.lang.WrongClassName”;
Class.forName(className);
JRE: HotSpot JDK 1.7 64位
Java EE容器:JBoss WildFly8
压测工具:Apache JMeter
Java监控工具:JVisualVM
Java故障分析:JVM Thread Dump
JAX-RS服务同时有20个线程在并发的执行。每一次调用都会触发一个ClassNotFoundException。为了减少IO的影响,日志也完全关闭了,我们只关注类加载的竞争冲突。
现在我们来看下JVisualVM在半分钟到一分钟内的监测结果。可以很明显的看到,很多线程都阻塞住了,在等待获取一个对象锁。
分析JVM的threaddump很容易就能定位出问题:线程锁竞争。从运行栈的跟踪信息可以看到,JBoss把类加载的任务委派给了JDK的类加载器。为什么?这是因为这个不存在的java类被认为是属于系统类路径底下,因此JBoss会把它的加载委派给系统的类加载器。这样因为这个类触发了系统级的同步,其它线程只能等待获取锁才能加载它。
很多线程都在等待获取0x00000000ab84c0c8的锁:
"default task-15" prio=6 tid=0x0000000014849800 nid=0x2050 waiting for monitor entry [0x000000001009d000]
java.lang.Thread.State: BLOCKED (on object monitor)
at java.lang.ClassLoader.loadClass(ClassLoader.java:403)
- waiting to lock <0x00000000ab84c0c8> (a java.lang.Object)
// Waiting to acquire a LOCK held by Thread “default task-20”
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:356) // JBoss now delegates to system ClassLoader..
at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:371)
at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:119)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:186)
at org.jboss.tools.examples.rest.MemberResourceRESTService.SystemCLFailure
(MemberResourceRESTService.java:176)
at org.jboss.tools.examples.rest.MemberResourceRESTService$Proxy$_$$_
WeldClientProxy.SystemCLFailure(Unknown Source)
at sun.reflect.GeneratedMethodAccessor15.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
这个线程是罪魁祸首——default task 20
"default task-20" prio=6 tid=0x000000000e3a3000 nid=0x21d8 runnable [0x0000000010e7d000]
java.lang.Thread.State: RUNNABLE
at java.lang.Throwable.fillInStackTrace(Native Method)
at java.lang.Throwable.fillInStackTrace(Throwable.java:782)
- locked <0x00000000a09585c8> (a java.lang.ClassNotFoundException)
at java.lang.Throwable.<init>(Throwable.java:287)
at java.lang.Exception.<init>(Exception.java:84)
at java.lang.ReflectiveOperationException.<init>(ReflectiveOperationException.java:75)
at java.lang.ClassNotFoundException.<init>(ClassNotFoundException.java:82) // ClassNotFoundException!
at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at java.lang.ClassLoader.loadClass(ClassLoader.java:423)
- locked <0x00000000ab84c0e0> (a java.lang.Object)
at java.lang.ClassLoader.loadClass(ClassLoader.java:410)
- locked <0x00000000ab84c0c8> (a java.lang.Object) // java.lang.ClassLoader: LOCK acquired
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:356)
at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:371)
at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:119)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:186)
at org.jboss.tools.examples.rest.MemberResourceRESTService.SystemCLFailure
(MemberResourceRESTService.java:176)
at org.jboss.tools.examples.rest.MemberResourceRESTService$Proxy
$_$$_WeldClientProxy.SystemCLFailure(Unknown Source)
现在我们把要加载类的名字更换到应用程序的包下面并重新运行这段程序。
String className = "org.ph.WrongClassName";
Class.forName(className);
可以看到,不再有阻塞的线程出现了。为什么?我们来看下JVM的thread dump来更好的理解下为什么会出现这个变化。
"default task-51" prio=6 tid=0x000000000dd33000 nid=0x200c runnable [0x000000001d76d000]
java.lang.Thread.State: RUNNABLE
at java.io.WinNTFileSystem.getBooleanAttributes(Native Method)
// IO overhead due to JAR file search operation
at java.io.File.exists(File.java:772)
at org.jboss.vfs.spi.RootFileSystem.exists(RootFileSystem.java:99)
at org.jboss.vfs.VirtualFile.exists(VirtualFile.java:192)
at org.jboss.as.server.deployment.module.VFSResourceLoader$2.run(VFSResourceLoader.java:127)
at org.jboss.as.server.deployment.module.VFSResourceLoader$2.run(VFSResourceLoader.java:124)
at java.security.AccessController.doPrivileged(Native Method)
at org.jboss.as.server.deployment.module.VFSResourceLoader.getClassSpec(VFSResourceLoader.java:124)
at org.jboss.modules.ModuleClassLoader.loadClassLocal(ModuleClassLoader.java:252)
at org.jboss.modules.ModuleClassLoader$1.loadClassLocal(ModuleClassLoader.java:76)
at org.jboss.modules.Module.loadModuleClass(Module.java:526)
at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:189)
// JBoss now fully responsible to load the class
at org.jboss.modules.ConcurrentClassLoader.performLoadClassUnchecked(ConcurrentClassLoader.java:444)
// Unchecked since using JDK 1.7 e.g. tagged as “safe” JDK
at org.jboss.modules.ConcurrentClassLoader.performLoadClassChecked(ConcurrentClassLoader.java:432)
at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:374)
at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:119)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:186)
at org.jboss.tools.examples.rest.MemberResourceRESTService.AppCLFailure
(MemberResourceRESTService.java:196)
at org.jboss.tools.examples.rest.MemberResourceRESTService$Proxy$_$$_
WeldClientProxy.AppCLFailure(Unknown Source)
at sun.reflect.GeneratedMethodAccessor60.invoke(Unknown Source)
上述的栈运行信息可以看出:
由于类名不再认为属于Java系统包下,不会发生类加载委派,也就没有同步操作。
由于JBoss认为JDK1.7是一个”安全“的JDK,它会使用ConcurrentClassLoader.performLoadClassUnchecked()方法,也就不会触发对象监视器锁。
没有同步也就不会产生线程锁竞争,也就不会出现没完没了的ClassNotFoundException异常。
值得注意的是JBoss在防止线程锁竞争方面做出了很大的改进,不过重复的类加载的尝试仍然会在一定程度上影响应用的性能,因为搜索JAR包查找Java类会有一定的IO开销,因此也仍需要采取正确的手段进行处理。
总结
希望你能喜欢这篇文章并且对类加载可能会带来的性能影响也有了更好的了解。JDK1.7和现代的Java EE容器都在类加载方面做了很多的优化,比如死锁和锁竞争方面,但是还是会存在潜在的问题。因此,我强烈建议你能密切的关注你的应用程序的表现,记录日志并确保类加载相关的异常比如java.lang.ClassNotFoundException和java.lang.NoClassDefFoundError都能够正确的处理。