ClassNotFoundException: 真的会使你的JVM慢下来吗?

    xiaoxiao2024-03-28  10

    大多数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”从系统包级别执 1String className =”java.lang.WrongClassName”; 2Class.forName(className); JRE: HotSpot JDK 1.7 @64-bitJava EE 容器: JBoss WildFly 8 加载测试工具: Apache JMeter Java 监控: JVisualVMJava 并发问题分析: 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…

    01 "default task-15" prio=6 tid=0x0000000014849800 nid=0x2050 waiting for monitor entry [0x000000001009d000] 02java.lang.Thread.State: BLOCKED (on object monitor) 03  at java.lang.ClassLoader.loadClass(ClassLoader.java:403) 04  - waiting to lock <0x00000000ab84c0c8> (a java.lang.Object) 05  // Waiting to acquire a LOCK held by Thread “default task-20” 06  at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308) 07  at java.lang.ClassLoader.loadClass(ClassLoader.java:356) // JBoss now delegates to system ClassLoader.. 08  at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:371) 09  at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:119) 10  at java.lang.Class.forName0(Native Method) 11  at java.lang.Class.forName(Class.java:186) 12  at org.jboss.tools.examples.rest.MemberResourceRESTService.SystemCLFailure(MemberResourceRESTService.java:176) 13  at org.jboss.tools.examples.rest.MemberResourceRESTService$Proxy$_$$_WeldClientProxy.SystemCLFailure(Unknown Source) 14  at sun.reflect.GeneratedMethodAccessor15.invoke(Unknown Source) 15  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 16  at java.lang.reflect.Method.invoke(Method.java:601) 17……………………..

    罪魁祸首的线程– default task-20

    01"default task-20" prio=6 tid=0x000000000e3a3000 nid=0x21d8 runnable [0x0000000010e7d000] 02    java.lang.Thread.State: RUNNABLE 03                at java.lang.Throwable.fillInStackTrace(Native Method) 04                at java.lang.Throwable.fillInStackTrace(Throwable.java:782) 05                - locked <0x00000000a09585c8> (a java.lang.ClassNotFoundException) 06                at java.lang.Throwable.<init>(Throwable.java:287) 07                at java.lang.Exception.<init>(Exception.java:84) 08                at java.lang.ReflectiveOperationException.<init>(ReflectiveOperationException.java:75) 09at java.lang.ClassNotFoundException.<init>(ClassNotFoundException.java:82) // ClassNotFoundException!                                      at java.net.URLClassLoader$1.run(URLClassLoader.java:366) 10                at java.net.URLClassLoader$1.run(URLClassLoader.java:355) 11                at java.security.AccessController.doPrivileged(Native Method) 12                at java.net.URLClassLoader.findClass(URLClassLoader.java:354) 13                at java.lang.ClassLoader.loadClass(ClassLoader.java:423) 14                - locked <0x00000000ab84c0e0> (a java.lang.Object) 15                at java.lang.ClassLoader.loadClass(ClassLoader.java:410) 16- locked <0x00000000ab84c0c8> (a java.lang.Object)   // java.lang.ClassLoader: LOCK acquired                                                             at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308) 17                at java.lang.ClassLoader.loadClass(ClassLoader.java:356) 18                at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:371) 19                at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:119) 20                at java.lang.Class.forName0(Native Method) 21                at java.lang.Class.forName(Class.java:186) 22                at org.jboss.tools.examples.rest.MemberResourceRESTService.SystemCLFailure(MemberResourceRESTService.java:176) 23                at org.jboss.tools.examples.rest.MemberResourceRESTService$Proxy$_$$_WeldClientProxy.SystemCLFailure(Unknown Source) 24…………………………………

    现在我们通过一个被标记为 “application”包中的一部分的Java 类为替换我们的类名,并在同样的条件下重新测试。

    1String className =”org.ph.WrongClassName”; 2Class.forName(className);

    正如我们所看到,不需要再应对BLOCKED线程.. 为什么这样呢?咱们一块看看JVM线程dump,更好的理解一下这种行为的变化。

    01"default task-51" prio=6 tid=0x000000000dd33000 nid=0x200c runnable [0x000000001d76d000] 02    java.lang.Thread.State: RUNNABLE 03                at java.io.WinNTFileSystem.getBooleanAttributes(Native Method)    // IO overhead due to JAR file search operation 04                at java.io.File.exists(File.java:772) 05                at org.jboss.vfs.spi.RootFileSystem.exists(RootFileSystem.java:99) 06                at org.jboss.vfs.VirtualFile.exists(VirtualFile.java:192) 07                at org.jboss.as.server.deployment.module.VFSResourceLoader$2.run(VFSResourceLoader.java:127) 08                at org.jboss.as.server.deployment.module.VFSResourceLoader$2.run(VFSResourceLoader.java:124) 09                at java.security.AccessController.doPrivileged(Native Method) 10                at org.jboss.as.server.deployment.module.VFSResourceLoader.getClassSpec(VFSResourceLoader.java:124) 11                at org.jboss.modules.ModuleClassLoader.loadClassLocal(ModuleClassLoader.java:252) 12                at org.jboss.modules.ModuleClassLoader$1.loadClassLocal(ModuleClassLoader.java:76) 13                at org.jboss.modules.Module.loadModuleClass(Module.java:526) 14                at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:189)   // JBoss now fully responsible to load the class 15                at org.jboss.modules.ConcurrentClassLoader.performLoadClassUnchecked(ConcurrentClassLoader.java:444) // Unchecked since using JDK 1.7 e.g. tagged as “safe” JDK 16                at org.jboss.modules.ConcurrentClassLoader.performLoadClassChecked(ConcurrentClassLoader.java:432) 17                at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:374) 18                at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:119) 19                at java.lang.Class.forName0(Native Method) 20                at java.lang.Class.forName(Class.java:186) 21                at org.jboss.tools.examples.rest.MemberResourceRESTService.AppCLFailure(MemberResourceRESTService.java:196) 22                at org.jboss.tools.examples.rest.MemberResourceRESTService$Proxy$_$$_WeldClientProxy.AppCLFailure(Unknown Source) 23                at sun.reflect.GeneratedMethodAccessor60.invoke(Unknown Source) 24……………….

    上述堆栈跟踪信息表明:

    自从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 这样的类加载错误.

    转载自 并发编程网 - ifeve.com
    最新回复(0)