spring bootJAVA基础(未看)

java内省优化工具类BeanUtils(优化内省并防止内存泄漏

2020-01-19  本文已影响0人  小胖学编程

java内省(Introspector)
java内省优化工具类BeanUtils(优化内省并防止内存泄漏)

Spring中专门提供了用于缓存JavaBean的PropertyDescriptor描述信息的类——CachedIntrospectionResults。但它的forClass()(获取对象)访问权限时default,不能被应用代码直接使用。但是可以通过org.springframework.beans.BeanUtils工具类来使用。

@Nullable  
public static PropertyDescriptor getPropertyDescriptor(Class<?> clazz, String propertyName)  
        throws BeansException {  
    //工厂模式,获取到对应的CachedIntrospectionResults 对象
    CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz);  
    return cr.getPropertyDescriptor(propertyName);  
}  

CachedIntrospectionResults这个类使用的是工厂模式,通过forClass()方法获取到不同的CachedIntrospectionResults对象。

实际上使用的是ConcurrentHashMap进行存储,key就是Class对象,而value是CachedIntrospectionResults对象。

1. CachedIntrospectionResults基本结构

CachedIntrospectionResults对象.png

当使用JavaBean的内省时,使用Introspector,jdk会自动缓存内省信息(BeanInfo),这一点是可以理解的,毕竟内省通过反射的代价是高昂的。当ClassLoader关闭时,Introspector的缓存持有BeanInfo的信息,而BeanInfo持有Class的强引用,这会导致ClassLoader和它引用的Class等对象不能被回收。

获取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;  
    }  
    //开始创建出CachedIntrospectionResults对象
    results = new CachedIntrospectionResults(beanClass);  
    ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse;  
    //确保Spring框架的jar包和应用类的jar包使用的是同一个ClassLoader加载的,这样的话,会允许随着Spring容器的生命周期来清除缓存。
    //当然若是多类加载器的应用,判断应用类使用的类加载器是否是安全的。使得类加载器过期时,能即时清除缓存中的值。
    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);  
}  

请注意CachedIntrospectionResults使用的是两个缓存。

/** 
 * 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);  

其实CachedIntrospectionResults对内省的优化一个是缓存PropertyDescriptor对象,第二点就是若类加载器失效时,清除旧缓存。

2. CachedIntrospectionResults防止内存泄漏

清除给定类加载器的内省缓存,删除该类加载器下所有类的内省结果。以及从acceptedClassLoaders列表中移除类加载器(及其子类)。

//源码:org.springframework.beans.CachedIntrospectionResults#clearClassLoader清除缓存和类加载器
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));  
}  

这个方法被谁调用呢?

清除方法调用者.png

这个清除方法的调用者实际上就存在两处:

  1. Spring初始化时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();  
        }  
    }  
}  

在容器初始化时,会重置introspection 缓存,因为可能不再需要单例bean的元数据。

  1. Servlet监听中使用
public class IntrospectorCleanupListener implements ServletContextListener {
    //web 容器初始化时(在filter、servlets初始化之前)执行
    @Override
    public void contextInitialized(ServletContextEvent event) {
        CachedIntrospectionResults.acceptClassLoader(Thread.currentThread().getContextClassLoader());
    }
    //在ServletContext销毁时(filters和servlets销毁执之后)执行
    @Override
    public void contextDestroyed(ServletContextEvent event) {
        CachedIntrospectionResults.clearClassLoader(Thread.currentThread().getContextClassLoader());
        Introspector.flushCaches();
    }
}

IntrospectorCleanupListener实现了ServletContextListener接口。虽然使用Spring本身时不需要使用该监听器,因为Spring自己的内部机制会清空对应的缓存。但是如果Spring配合其他框架使用,而其他框架存在这个问题时,例如Struts 和Quartz,那就需要配置这个监听器,在销毁ServletContext的时候清除对应缓存。

需要注意,即使存在一个Introspector造成内存泄漏也会导致整个应用的类加载器不会被垃圾回收器回收,可能会造成内存泄漏。

配置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优先级应该最高。那么它的contextDestroyed方法将会最后一个执行,将会发挥最有效缓存清除的作用。

推荐阅读

加入ehchace后,系统出现内存泄露问题,详细解决方法

上一篇下一篇

猜你喜欢

热点阅读