141
阿裏雲
技術社區[雲棲]
ClassNotFoundException: 真的會使你的JVM慢下來嗎?
大多數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”從係統包級別執
1 |
String className =”java.lang.WrongClassName”; |
2 |
Class.forName(className); |
這次模擬是采用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 ]
|
02 |
java.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 )
|
罪魁禍首的線程– 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)
|
09 |
at 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)
|
現在我們通過一個被標記為 “application”包中的一部分的Java 類為替換我們的類名,並在同樣的條件下重新測試。
1 |
String className =”org.ph.WrongClassName”; |
2 |
Class.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)
|
上述堆棧跟蹤信息表明:
- 自從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 這樣的類加載錯誤.
最後更新:2017-05-23 18:02:34