本文翻译自javacodegeeks 作者:Pierre Hugues Charbonneau 译者:TonySpark 校对:郑旭东
大多数Java开发者比较熟悉这个普通的 java.lang.ClassNotFoundException。这个问题的根源逐渐被开发人员所了解(在ClassPath中找不到相关类或者类库,类加载器委托问题等等),然而它对于整个JVM及性能的影响却鲜为人知。这个异常会应用程序的响应时间和可扩展性有很大的影响。
在部署了多个应用程序的大型Java EE企业系统中,由于运行期间有很多不同的类加载器,所以这种类型的问题出现的最多的。也就增加了面对未检测的ClassNotFoundException的风险,除非定义了明确的业务影响和实现了很好的日志监控,否则JVM类加载IO操作和线程锁竞争将会持续的影响应用程序的性能。
下文中的程序将演示你的客户生产系统中任何ClassNotFoundException都应认真对待并及时解决。
Java类Loading: 优化性能缺失的环节
只有正确的理解JAVA类加载模型才能正确的理解性能问题。ClassNotFoundException 本质上意味着JVM定位或通过下面的方法加载类是失败的:
1)Class.forName()方法
2)ClassLoader.findSystemClass() 方法
3)ClassLoader.loadClass()方法。
在JVM的生命周期中,应用程序中的类只会发生一次(当然也有动态重新部署功能),同时一些应用程序也依赖动态类加载操作。
然而,重复的成功或者失败的类加载操作是相当的惹人烦,尤其是试图使用JDK中 java.lang.ClassLoader 来进行加载操作。实际上,由于向后兼容性,在JDK1.7+ 除非类加载器被明确标记为具有并行能力(”parallel capable”)否则默认只会一次加载一个类。请记住即使在类的级别发生同步,一个重复的类加载失败还会根据你所处理的Java线程头发级别触发线程锁竞争。这种情况如果在JDK1.6中,当类加载实例级别进行同步时变得更加严重。
因为这个原因,像JBoss WildFly 8 这样的Java EE容器会使用他们自身的并发类加载器来加载你的应用程序类。这此类加载器在更精细的粒度上实现了锁,因此可以并发的从同一个类加载器实例来加载不同的类。这同样与最新的JDK1.7+中改善性的支持多线程定制类加载器( Multithreaded Custom Class Loaders )保持一致。这种多线程定制类加载器可以一定程度上阻止类加载器死锁现象。 话虽然是这么说的。像java.* 还有Java EE容器模块这样系统级别的类,他们的类加载器还依赖于JDK默认的类加载器。这就意味着重复的类加载失败仍然会触发严重的线程锁竞争。这恰恰是下文我们要重现和演示的。
线程锁竞争– 问题复制
我们按照以下规范创建了一个简单应有程序,来重现和模拟这个问题
- JAX-RS(REST)Web 服务采用一个假的类名“located”从系统包级别执
String className =”java.lang.WrongClassName”; Class.forName(className);
- JRE: HotSpot JDK 1.7 @64-bit
- Java EE 容器: JBoss WildFly 8
- 加载测试工具: Apache JMeter
- Java 监控: JVisualVM
- Java 并发问题分析: JVM Thread Dump analysis
这次模拟是采用20个JAX-RS Web service 线程来并发执行。 每一次调用都会有一个ClassNotFoundException. 为了减少对IO影响,我们禁用日志,并将关注点只放在类加载竟争上。
现在我们来看看JvisualVM中运行了30-60秒的结果。我们可以清晰的看到大量的BLOCKED线程等待在Object monitor 上获取锁。
分析JVM线程dump,可以清晰的暴露出问题:线程锁竞争。我们可以从JBoss将类的加载委托给JDK的ClassLoader的堆栈跟踪中看到。 为什么呢? 这是因为我们的错误的Java 类名被认为是系统类path的一部份。在这种情况下,JBoss将会把加载委托给系统类加载器,触发了针对那个特定类名的系统级同步,同时来自其它线程的waiters 等待获取一个锁来加载同样的类名。
许多线程等待获取 LOCK 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) …………………………………
现在我们通过一个被标记为 “application”包中的一部分的Java 类为替换我们的类名,并在同样的条件下重新测试。
String className =”org.ph.WrongClassName”; Class.forName(className);
正如我们所看到,不需要再应对BLOCKED线程.. 为什么这样呢?咱们一块看看JVM线程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类名不再作为Java系统包的一部分,就不会有ClassLoader的委托,因此也不会有同步操作。
- 自从JBoss认为JDK1.7+是个“安全”的JDK. ConcurrentClassLoader .使用LoadClassUnchecked()来实现 , 不会触发任何对象监控锁(Object monitor lock).
- 没有同步就意味着不存在因为不间断ClassNotFoundException错误而导致的线程锁竞争。
注意在这种情况下JBoss做了大量工作来阻止线程锁竟争,由于过多的JAR文件查找操作和IO开销,重复的类加载尝试将一定程度上降低性能。要解决这样的问题需立即采取纠正措施。
结束语
我希望你喜欢这篇文章并对因为 过度的类加载操作而导致潜在的性能影响有进一步的理解。当JDK1.7 和现在的JAVA EE容器针对像死锁和线程锁竞争这样类加载问题上做出很大的提升时,潜在的问题仍然存在。因此,我强烈建议您密切监控你的应用程序运行情况、日志,并及时改正像java.lang.ClassNotFoundException 和java.lang.NoClassDefFoundError 这样的类加载错误.
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/140539.html