通过热点代码分析哪些热点代码可能会产生这些对象(Method Profiling Sample)。像这种大量创建的对象,抓取 Runnable 代码很大概率被抓取到,并且在事件中占比高。
首先查看 Thread Allocation Statistics 事件,发现基本上所有 servlet 线程(就是处理 Http 请求的线程,我们用的 Undertow,所以线程名称是 XNIO 开头的),分配的对象都很多,这样并不能定位问题:
然后我们来看热点代码统计,点击 Method Profiling Sample 事件,查看堆栈追踪统计,看哪些占比比较高。
发现占比靠前的,貌似都和这个 ResolvableType 有关,进一步定位,双击第一个方法查看调用堆栈统计:
我们发现,调用它的是 BeanUtils.copyProperties。查看其它ResolvableType 有关的调用,都和BeanUtils.copyProperties有关。这个方法是我们项目中经常使用的方法,用于同类型或者不同类型之间的属性复制。这个方法为何会创建这么多 ResolvableType 呢?
通过查看源码,我们发现从 Spring 5.3.x 开始,BeanUtils 开始通过创建 ResolvableType 这个统一类信息封装,进行属性复制:
/** * * <p>As of Spring Framework 5.3, this method honors generic type information */ private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException { }里面的源码,每次都针对源对象和目标对象的类型的每个属性方法创建了新的 ResolvableType,并且没有做缓存。这导致一次复制,会创建出来大量的 ResolvableType.我们来做个试验:
public class Test { public static void main(String[] args) { TestBean testBean1 = new TestBean("1", "2", "3", "4", "5", "6", "7", "8", "1", "2", "3", "4", "5", "6", "7", "8"); TestBean testBean2 = new TestBean(); for (int i = 0; i > -1; i++) { BeanUtils.copyProperties(testBean1, testBean2); System.out.println(i); } } }分别使用 spring-beans 5.2.16.RELEASE 和 spring-beans 5.3.9 这两个依赖去执行这个代码,JVM 参数使用 -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xmx512m.这些参数的意思是,使用 EpsilonGC,也就是在堆内存满的时候,不执行 GC,直接抛出 OutofMemory 异常并程序结束,并且最大堆内存是 512m。这样,程序其实就是看:在内存耗尽之前,不同版本的 BeanUtils.copyProperties 分别能执行多少次。
试验结果是:spring-beans 5.2.16.RELEASE 是 444489 次,spring-beans 5.3.9 是 27456 次。这是相当大的差距啊。
于是,针对这个问题,我向 spring-framework github 提了个 Issue.
然后,对于项目中经常使用 BeanUtils.copyProperties 的地方,替换成使用 BeanCopier,并且封装了一个简单类:
public class BeanUtils { private static final Cache<String, BeanCopier> CACHE = Caffeine.newBuilder().build(); public static void copyProperties(Object source, Object target) { Class<?> sourceClass = source.getClass(); Class<?> targetClass = target.getClass(); BeanCopier beanCopier = CACHE.get(sourceClass.getName() + " to " + targetClass.getName(), k -> { return BeanCopier.create(sourceClass, targetClass, false); }); beanCopier.copy(source, target, null); } }