美文网首页spring bootJAVA基础(未看)
java内省优化工具类BeanUtils(优化内省并防止内存泄漏

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

作者: 小胖学编程 | 来源:发表于2020-01-19 13:02 被阅读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);  
    
    • 若应用类和Spring容器类使用的是一个类加载器 || 应用类使用的加载器是用户提前缓存的类加载器,那么存入strongClassCache对象中。
    • 若应用类加载器不是用户提前缓存的类加载器,那么存入softClassCache对象中,即不安全的类加载器。

    其实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后,系统出现内存泄露问题,详细解决方法

    相关文章

      网友评论

        本文标题:java内省优化工具类BeanUtils(优化内省并防止内存泄漏

        本文链接:https://www.haomeiwen.com/subject/wthrzctx.html