java內(nèi)省優(yōu)化工具類(lèi)BeanUtils(優(yōu)化內(nèi)省并防止內(nèi)存泄漏)

java內(nèi)省(Introspector)
java內(nèi)省優(yōu)化工具類(lèi)BeanUtils(優(yōu)化內(nèi)省并防止內(nèi)存泄漏)

Spring中專(zhuān)門(mén)提供了用于緩存JavaBean的PropertyDescriptor描述信息的類(lèi)——CachedIntrospectionResults。但它的forClass()(獲取對(duì)象)訪問(wèn)權(quán)限時(shí)default,不能被應(yīng)用代碼直接使用。但是可以通過(guò)org.springframework.beans.BeanUtils工具類(lèi)來(lái)使用。

@Nullable  
public static PropertyDescriptor getPropertyDescriptor(Class<?> clazz, String propertyName)  
        throws BeansException {  
    //工廠模式,獲取到對(duì)應(yīng)的CachedIntrospectionResults 對(duì)象
    CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz);  
    return cr.getPropertyDescriptor(propertyName);  
}  

CachedIntrospectionResults這個(gè)類(lèi)使用的是工廠模式,通過(guò)forClass()方法獲取到不同的CachedIntrospectionResults對(duì)象。

實(shí)際上使用的是ConcurrentHashMap進(jìn)行存儲(chǔ),key就是Class對(duì)象,而value是CachedIntrospectionResults對(duì)象。

1. CachedIntrospectionResults基本結(jié)構(gòu)

CachedIntrospectionResults對(duì)象.png

當(dāng)使用JavaBean的內(nèi)省時(shí),使用Introspector,jdk會(huì)自動(dòng)緩存內(nèi)省信息(BeanInfo),這一點(diǎn)是可以理解的,畢竟內(nèi)省通過(guò)反射的代價(jià)是高昂的。當(dāng)ClassLoader關(guān)閉時(shí),Introspector的緩存持有BeanInfo的信息,而B(niǎo)eanInfo持有Class的強(qiáng)引用,這會(huì)導(dǎo)致ClassLoader和它引用的Class等對(duì)象不能被回收。

獲取CachedIntrospectionResults的工廠方法:

static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {  
    //緩存中取
    CachedIntrospectionResults results = strongClassCache.get(beanClass);  
    if (results != null) {  
        return results;  
    }  
    results = softClassCache.get(beanClass);  
    if (results != null) {  
        return results;  
    }  
    //開(kāi)始創(chuàng)建出CachedIntrospectionResults對(duì)象
    results = new CachedIntrospectionResults(beanClass);  
    ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse;  
    //確保Spring框架的jar包和應(yīng)用類(lèi)的jar包使用的是同一個(gè)ClassLoader加載的,這樣的話,會(huì)允許隨著Spring容器的生命周期來(lái)清除緩存。
    //當(dāng)然若是多類(lèi)加載器的應(yīng)用,判斷應(yīng)用類(lèi)使用的類(lèi)加載器是否是安全的。使得類(lèi)加載器過(guò)期時(shí),能即時(shí)清除緩存中的值。
    if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) ||  
            isClassLoaderAccepted(beanClass.getClassLoader())) {  
        classCacheToUse = strongClassCache;  
    }  
    else {  
        if (logger.isDebugEnabled()) {  
            logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe");  
        }  
        classCacheToUse = softClassCache;  
    }  
  
    CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results);  
    return (existing != null ? existing : results);  
}  

請(qǐng)注意CachedIntrospectionResults使用的是兩個(gè)緩存。

/** 
 * Map keyed by Class containing CachedIntrospectionResults, strongly held. 
 * This variant is being used for cache-safe bean classes. 
 */  
static final ConcurrentMap<Class<?>, CachedIntrospectionResults> strongClassCache =  
        new ConcurrentHashMap<>(64);  
  
/** 
 * Map keyed by Class containing CachedIntrospectionResults, softly held. 
 * This variant is being used for non-cache-safe bean classes. 
 */  
static final ConcurrentMap<Class<?>, CachedIntrospectionResults> softClassCache =  
        new ConcurrentReferenceHashMap<>(64);  
  • 若應(yīng)用類(lèi)和Spring容器類(lèi)使用的是一個(gè)類(lèi)加載器 || 應(yīng)用類(lèi)使用的加載器是用戶(hù)提前緩存的類(lèi)加載器,那么存入strongClassCache對(duì)象中。
  • 若應(yīng)用類(lèi)加載器不是用戶(hù)提前緩存的類(lèi)加載器,那么存入softClassCache對(duì)象中,即不安全的類(lèi)加載器。

其實(shí)CachedIntrospectionResults對(duì)內(nèi)省的優(yōu)化一個(gè)是緩存PropertyDescriptor對(duì)象,第二點(diǎn)就是若類(lèi)加載器失效時(shí),清除舊緩存。

2. CachedIntrospectionResults防止內(nèi)存泄漏

清除給定類(lèi)加載器的內(nèi)省緩存,刪除該類(lèi)加載器下所有類(lèi)的內(nèi)省結(jié)果。以及從acceptedClassLoaders列表中移除類(lèi)加載器(及其子類(lèi))。

//源碼:org.springframework.beans.CachedIntrospectionResults#clearClassLoader清除緩存和類(lèi)加載器
public static void clearClassLoader(@Nullable ClassLoader classLoader) {  
    acceptedClassLoaders.removeIf(registeredLoader ->  
            isUnderneathClassLoader(registeredLoader, classLoader));  
    strongClassCache.keySet().removeIf(beanClass ->  
            isUnderneathClassLoader(beanClass.getClassLoader(), classLoader));  
    softClassCache.keySet().removeIf(beanClass ->  
            isUnderneathClassLoader(beanClass.getClassLoader(), classLoader));  
}  

這個(gè)方法被誰(shuí)調(diào)用呢?

清除方法調(diào)用者.png

這個(gè)清除方法的調(diào)用者實(shí)際上就存在兩處:

  1. Spring初始化時(shí)org.springframework.context.support.AbstractApplicationContext#refresh
@Override  
public void refresh() throws BeansException, IllegalStateException {  
    synchronized (this.startupShutdownMonitor) {  
       ...
  
        try {  
          //初始化Spring容器
          ...
        }  
  
        catch (BeansException ex) {  
         ...
        }  
  
        finally {  
            // Reset common introspection caches in Spring's core, since we  
            // might not ever need metadata for singleton beans anymore...  
            resetCommonCaches();  
        }  
    }  
}  

在容器初始化時(shí),會(huì)重置introspection 緩存,因?yàn)榭赡懿辉傩枰獑卫齜ean的元數(shù)據(jù)。

  1. Servlet監(jiān)聽(tīng)中使用
public class IntrospectorCleanupListener implements ServletContextListener {
    //web 容器初始化時(shí)(在filter、servlets初始化之前)執(zhí)行
    @Override
    public void contextInitialized(ServletContextEvent event) {
        CachedIntrospectionResults.acceptClassLoader(Thread.currentThread().getContextClassLoader());
    }
    //在ServletContext銷(xiāo)毀時(shí)(filters和servlets銷(xiāo)毀執(zhí)之后)執(zhí)行
    @Override
    public void contextDestroyed(ServletContextEvent event) {
        CachedIntrospectionResults.clearClassLoader(Thread.currentThread().getContextClassLoader());
        Introspector.flushCaches();
    }
}

IntrospectorCleanupListener實(shí)現(xiàn)了ServletContextListener接口。雖然使用Spring本身時(shí)不需要使用該監(jiān)聽(tīng)器,因?yàn)镾pring自己的內(nèi)部機(jī)制會(huì)清空對(duì)應(yīng)的緩存。但是如果Spring配合其他框架使用,而其他框架存在這個(gè)問(wèn)題時(shí),例如Struts 和Quartz,那就需要配置這個(gè)監(jiān)聽(tīng)器,在銷(xiāo)毀ServletContext的時(shí)候清除對(duì)應(yīng)緩存。

需要注意,即使存在一個(gè)Introspector造成內(nèi)存泄漏也會(huì)導(dǎo)致整個(gè)應(yīng)用的類(lèi)加載器不會(huì)被垃圾回收器回收,可能會(huì)造成內(nèi)存泄漏。

配置IntrospectorCleanupListener

@Configuration
public class ServletListenerInfo {
    @Bean
    public ServletListenerRegistrationBean<IntrospectorCleanupListener> IntrospectorCleanupListener() {
        ServletListenerRegistrationBean<IntrospectorCleanupListener> li = new ServletListenerRegistrationBean<>();
        li.setOrder(0);
        li.setListener(new IntrospectorCleanupListener());
        return li;
    }
}

需要注意的是:IntrospectorCleanupListener優(yōu)先級(jí)應(yīng)該最高。那么它的contextDestroyed方法將會(huì)最后一個(gè)執(zhí)行,將會(huì)發(fā)揮最有效緩存清除的作用。

推薦閱讀

加入ehchace后,系統(tǒng)出現(xiàn)內(nèi)存泄露問(wèn)題,詳細(xì)解決方法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容