閱讀141 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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中,當類加載實例級別進行同步時變得更加嚴重。

classNotFoundException
classNotFoundim2
因為這個原因,像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 上獲取鎖。

minotor

分析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)
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)
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)
24 …………………………………

現在我們通過一個被標記為 “application”包中的一部分的Java 類為替換我們的類名,並在同樣的條件下重新測試。

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

test
正如我們所看到,不需要再應對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 這樣的類加載錯誤.

最後更新:2017-05-23 18:02:34

  上一篇:go  Java字節碼淺析(三)
  下一篇:go  【閑說】性能測試