java内省优化工具类BeanUtils(优化内省并防止内存泄漏
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这个清除方法的调用者实际上就存在两处:
- 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的元数据。
- 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
方法将会最后一个执行,将会发挥最有效缓存清除的作用。