ClassNotFoundException: 真的會使你的JVM慢下來嗎?
本文翻譯自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+中改善性的支援多執行緒定製類載入器(
執行緒鎖競爭– 問題複製
我們按照以下規範建立了一個簡單應有程式,來重現和模擬這個問題
- JAX-RS(REST)Web 服務採用一個假的類名“located”從系統包級別執
String className =”java.lang.WrongClassName”; 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…
"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 這樣的類載入錯誤.