住建部網(wǎng)站2015年城市建設統(tǒng)計seo優(yōu)化的優(yōu)點
在前面的文章中,我們分析了 tomcat 類加載器的相關源碼,也了解了 tomcat 支持類的熱加載,意味著 tomcat 要涉及類的重復卸裝/裝載過程,這個過程是很敏感的,一旦處理不當,可能會引起內(nèi)存泄露
卸載類
我們知道,class 信息存放在元數(shù)據(jù)區(qū)(1.7是 Perm 區(qū)),這一塊的內(nèi)存相比堆而言,只占據(jù)非常小的空間,但是如果處理不當,還是有可能會導致內(nèi)存溢出。這讓我回想起幾年前的一個故障,線上環(huán)境啟用了 tomcat 的自動 reload 功能,出現(xiàn)過?java.lang.OutOfMemoryError: PermGen space
?問題,排查的結(jié)果是因為 tomcat 在自動重載應用的時候,沒有正常卸載類,導致 Perm 區(qū)內(nèi)存沒能被釋放而發(fā)生溢出。tomcat 會盡量避免這類問題的發(fā)生,但是不能百分之百保證不會出現(xiàn),所以還是建議不要隨意開啟?reloadable
?功能
卸載類的條件很苛刻,必須同時滿足以下3點:
1、 該類所有的實例已經(jīng)被回收
2、 加載該類的?ClassLoder
?已經(jīng)被回收
3、 該類對應的?java.lang.Class
?對象沒有任何地方被引用
針對第1點,保證所有的實例被回收,這點不難,tomcat 在 Context 組件中實例化這些對象,持有直接或間接的引用,所以在熱部署的時候,只要回收 Context 組件即可保證實例對象被回收。
在前面的文章中我們分析了 tomcat 類加載器,tomcat 使用?ParallelWebappClassLoader
?加載 Class,在熱部署的時候自然也會回收該類加載器。但是要注意的是,ParallelWebappClassLoader
?會作為線程上下文的類加載器,因此要避免該類加載器對象在其他地方被引用。其實,這個問題是最隱晦的,jdk 中有些類會持有線程上下文的類加載器,作為一個優(yōu)秀的開源產(chǎn)品,tomcat 為我們解決了很多諸如此類的問題
此外,還要保證類對應的?java.lang.Class
?對象沒有任何地方引用,只要 Class 對象作用域限制在?Context
?組件的作用范圍便不會發(fā)生泄露,tomcat 也是這么做了,使用?Context
?實現(xiàn)了隔離機制
熱加載問題
熱加載會面臨很多問題,有很多坑,需要非常豐富的經(jīng)驗。下面針對 tomcat 中涉及的類加載器泄露、對象泄露、文件鎖等這幾類常見的問題加以分析討論。如果您對熱加載感興趣的話,可以研究下阿里開源的?jarlinks
文件鎖
在 Windows 系統(tǒng)下使用?URLConnection
?讀取本地 jar 包的資源時,它會將資源緩存起來,會導致該 jar 包資源被鎖。如果這個時候使用 war 包進行重新部署,需要解壓 war 包再把原來目錄下面的 jar 包刪除,由于 jar 包資源被鎖,導致刪除失敗,重新部署自然也會失敗。我們先來看一段代碼,這段代碼會拋出異常,java.nio.file.FileSystemException: E:\spring-boot-2.0.1.RELEASE.jar: 另一個程序正在使用此文件,進程無法訪問
,說明該 jar 包被鎖了
String path = "E://spring-boot-2.0.1.RELEASE.jar";
File file = new File( path );
URL url = file.toURI().toURL();URLConnection uConn = url.openConnection();
uConn.getLastModified(); // 讀取jar包信息
為了解決文件鎖的問題,tomcat 禁用了?URLConnection
?的緩存,是在?JreMemoryLeakPreventionListener
?中完成的,關鍵代碼如下所示:
// dummy.jar 不存在也沒有關系
URL url = new URL("jar:file://dummy.jar!/");
URLConnection uConn = url.openConnection();
可能有些童鞋會有疑問,tomcat 只是針對該?URLConnection
?對象禁用了緩存,而其它的?URLConnection
?資源緩存未必被禁用啊。答案是肯定的,因為?URLConnection
?的?defaultUseCaches
?屬性是靜態(tài)變量
類加載器泄露
其中一種 JRE 內(nèi)存泄露是因為上下文類加載器導致的內(nèi)存泄露。某些 JRE 庫以單例的形式存在,它的生命周期很長甚至會貫穿于整個 java 程序,它們會使用上下文類加載器加載類,并且保留了類加載器的引用,所以會導致被引用的類加載器無法被回收,而 tomcat 重加載 webapp 是創(chuàng)建一個新的類加載器來實現(xiàn)的,舊的類加載器無法被 gc 回收,致使其加載的 Class 也無法被回收,導致內(nèi)存泄露。
DriverManager
?就是典型的例子,它利用 jdk 提供的 SPI 機制加載?java.sql.Driver
?驅(qū)動,而 jdk 提供的 SPI 機制便是使用上下文類加載器加載 Class 的,如果這類 jdbc 驅(qū)動由?ParallelWebappClassLoader
?類加載器加載的話,就會導致該?ClassLoder
?無法被回收,自然會出現(xiàn)內(nèi)存泄露
我們來看看 tomcat 是怎么解決的?tomcat 是利用?LifecycleListener
?處理?before_init
?事件,將上下文類加載器置為系統(tǒng)類加載器,并且完成驅(qū)動的加載過程,最后,為了不影響其它的類加載,再將上下文類加載器重置為?ParallelWebappClassLoader
另外一種 JRE 內(nèi)存泄露是因為當前線程會啟動另外一個線程,這個時候新線程會引用當前線程的上下文類加載器,如果新線程無止盡地運行,那么上下文類加載器就會一直被引用,而無法被回收,導致內(nèi)存泄露。sun.awt.AppContext.getAppContext()
?便是典型的例子,它會在內(nèi)部開啟一個?AWT-AppKit
?線程,直到圖形化環(huán)境準備就緒,例如?ImageIO.getCacheDirectory()
、java.awt.Toolkit.getDefaultToolkit()
針對這種情況,解決思路也是一樣的,只需要將當前上下文類加載器指定為系統(tǒng)類加載器即可,關鍵代碼如下所示:
JreMemoryLeakPreventionListener.java@Override
public void lifecycleEvent(LifecycleEvent event) {if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) {ClassLoader loader = Thread.currentThread().getContextClassLoader();try {// 當線程上下文類加載器指定為系統(tǒng)類加載器Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());if (driverManagerProtection) {DriverManager.getDrivers();}// 避免開啟的子線程持有 ParallelWebappClassLoader 引用if (appContextProtection && !JreCompat.isJre8Available()) {ImageIO.getCacheDirectory();}if (awtThreadProtection && !JreCompat.isJre9Available()) {java.awt.Toolkit.getDefaultToolkit();}// 避免持有 ParallelWebappClassLoader 引用if (tokenPollerProtection && !JreCompat.isJre9Available()) {java.security.Security.getProviders();}// 忽略若干代碼......} finally {// 再重置為 ParallelWebappClassLoader,避免影響其它的類的加載Thread.currentThread().setContextClassLoader(loader);}}
ThreadLocal 對象泄露
還有一種內(nèi)存泄露是由于?ThreadLocal
?引起的,假如我們在?ThreadLocal
?中保存了對象A,而且對象A由?ParallelWebappClassLoader
?加載,那么就可以看成線程引用了對象A。由于 tomcat 中處理請求的是線程池,意味著該線程會存活很長一段時間。webapp 熱加載時,會重新實例化一個?ParallelWebappClassLoader
?對象,如果線程未銷毀,那么舊的?ParallelWebappClassLoader
?也無法被回收,導致內(nèi)存泄露。
解決?ThreadLocal
?內(nèi)存泄露最好的辦法,自然是把線程池中的所有的線程銷毀并重新創(chuàng)建。這個過程分為兩步,第一步是將任務隊列堵住,不讓新的任務進來,第二步是將線程池中所有線程停止。
tomcat 解決該 ThreadLocal 對象泄露問題,也是借助了?Lifecycle
?完成的,具體的實現(xiàn)類是?ThreadLocalLeakPreventionListener
,它會處理?Lifecycle.AFTER_STOP_EVENT
?事件,并且銷毀線程池內(nèi)的空閑線程,關鍵代碼如下所示: