不容忽视的ClassNotFoundException

相信很多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都能够正确的处理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,690评论 0 11
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,169评论 11 349
  • (一)Java部分 1、列举出JAVA中6个比较常用的包【天威诚信面试题】 【参考答案】 java.lang;ja...
    独云阅读 7,062评论 0 62
  • 唐代诗人王昌龄的边塞诗: 第一首《出塞》:秦时明月汉时关 ,万里长征人未还。 但使龙城飞将在 ,不教胡马度阴山。 ...
    大气浩然阅读 1,430评论 0 1