Introspector作用及影響
在分析IntrospectorCleanupListener之前,先了解一下Introspector。Introspector是JDK中java.beans包下的類,它為目標JavaBean提供了一種瞭解原類方法、屬性和事件的標準方法。通俗的說,就是可以通過Introspector構建一個BeanInfo物件,而這個BeanInfo物件中包含了目標類中的屬性、方法和事件的描述資訊,然後可以使用這個BeanInfo物件對目標物件進行相關操作。
下面看一個簡單的示例會很容易明白。為了簡單,Student類中只有一個name屬性。
結果輸出:Student{name='張三'}
複製程式碼
通過檢視Introspector.getBeanInfo方法的原始碼會發現,Introspector在構建一個BeanInfo物件的時候,會將構建的BeanInfo物件和原類快取到一個Map中,原始碼如下。
通過上的程式碼可以得出,Introspector間接持有了BeanInfo的強引用。如果使用Introspector操作了很多類,那麼Introspector將間接持有這些BeanInfo的強引用。在發生垃圾收集的時候,檢測到這些BeanInfo存在引用鏈,則這些類和對應的類載入器將不會被垃圾收集器回收,進而導致記憶體洩漏。所以,為了解決這個問題,在使用Introspector操作完成後,呼叫Introspector類的flushCaches方法清除快取。
通過上面的程式碼會發現,清除的時候是清空了整個快取,因為沒有很好的辦法來確定每個快取是屬於哪個應用的,所以清除的時候會清除所有應用的快取。
IntrospectorCleanupListener解析
上面分析了Introspector的作用和影響,那IntrospectorCleanupListener和Introspector有什麼關係呢?
IntrospectorCleanupListener是spring-web jar中的類,原始碼如下。
IntrospectorCleanupListener實現了ServletContextListener介面,也就是說,在web容器初始化(準確的說是在filters或servlets初始化之前)的時候會執行contextInitialized方法,在ServletContext銷燬(準確的說是在filters和servlets銷燬之後)的時候會執行contextDestroyed方法。從圖中contextDestroyed方法,可以看到在銷燬ServletContext的時候呼叫了Introspector.flushCaches方法,清空了對應快取。IntrospectorCleanupListener中為什麼要這麼做?難道是Spring使用Introspector操作後沒有清空對應快取?檢視IntrospectorCleanupListener類的原始碼,會發現有這樣一段標註。
大意是說,在使用Spring本身的時候並不需要使用此監聽器,因為Spring自己的內部機制會立即清空對應的快取。雖然,Spring本身不存在這樣的問題,但是如果和其它框架結合使用,而其它框架有這個問題,如Struts、Quartz等,那就需要配置這個監聽器,在銷燬ServletContext的時候清空對應快取。
有一點需要注意的是,像這樣一個簡單的Introspector記憶體洩漏將會導致整個應用的類載入器不會被垃圾收集器回收,如果有記憶體洩漏的問題,可以考慮此因素。
配置IntrospectorCleanupListener
在以往的工作經歷中,多次看到在web.xml中將IntrospectorCleanupListener配置成非第一個listener。
其實,看過原始碼的都知道,官方的表述是必須將此監聽器配置成web.xml中的第一個listener,才能在合適的時間發揮最有效的作用。
原因其實很簡單,在Servlet3.0規範之前,監聽器的呼叫是隨機的,而從Servlet3.0開始,監聽器的呼叫順序是根據其在web.xml中配置的順序,並且實現ServletContextListener的監聽器,contextInitialized方法呼叫順序是按照在web.xml中配置的順序正序依次執行,而contextDestroyed方法的呼叫順序是按照在web.xml中配置的順序逆序依次執行。所以,如果IntrospectorCleanupListener被配置成了第一個listener,那麼它的contextDestroyed方法將最後一個執行,將發揮最有效的清除作用;而如果不是,那麼可能會殘留未被清除的快取。