Grails JSON 使用内省的编组导致 Classloader.loadClass() 严重瓶颈
Grails JSON marhsaling using introspection causes severe bottleneck on Classloader.loadClass()
我正在使用 Grails 2.2.4,并且有一个控制器端点可以将域对象列表转换为 JSON。在负载下(少至 5 个并发请求),编组性能非常差。进行线程转储时,线程被阻塞:
java.lang.ClassLoader.loadClass(ClassLoader.java:291)
注册了一个编组器以使用反射和内省编组所有域对象。意识到反射和内省比直接方法调用慢,我仍然看到意想不到的行为,因为 class 加载器每次都是调用者,进而发生阻塞。示例堆栈跟踪如下:
java.lang.Thread.State: BLOCKED (on object monitor)
at java.lang.ClassLoader.loadClass(ClassLoader.java:291)
- waiting to lock <785e31830> (a org.grails.plugins.tomcat.ParentDelegatingClassLoader)
at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
at java.beans.Introspector.instantiate(Introspector.java:1470)
at java.beans.Introspector.findExplicitBeanInfo(Introspector.java:431)
at java.beans.Introspector.<init>(Introspector.java:380)
at java.beans.Introspector.getBeanInfo(Introspector.java:167)
at java.beans.Introspector.getBeanInfo(Introspector.java:230)
at java.beans.Introspector.<init>(Introspector.java:389)
at java.beans.Introspector.getBeanInfo(Introspector.java:167)
at java.beans.Introspector.getBeanInfo(Introspector.java:230)
at java.beans.Introspector.<init>(Introspector.java:389)
at java.beans.Introspector.getBeanInfo(Introspector.java:167)
at java.beans.Introspector.getBeanInfo(Introspector.java:230)
at java.beans.Introspector.<init>(Introspector.java:389)
at java.beans.Introspector.getBeanInfo(Introspector.java:167)
at org.springframework.beans.CachedIntrospectionResults.<init>(CachedIntrospectionResults.java:217)
at org.springframework.beans.CachedIntrospectionResults.forClass(CachedIntrospectionResults.java:149)
at org.springframework.beans.BeanWrapperImpl.getCachedIntrospectionResults(BeanWrapperImpl.java:324)
at org.springframework.beans.BeanWrapperImpl.getPropertyValue(BeanWrapperImpl.java:727)
at org.springframework.beans.BeanWrapperImpl.getPropertyValue(BeanWrapperImpl.java:721)
at org.springframework.beans.PropertyAccessor$getPropertyValue.call(Unknown Source)
at com.ngs.id.RestDomainClassMarshaller.extractValue(RestDomainClassMarshaller.groovy:203)
...
...
加载具有相同参数的相同端点的简单基准测试导致 loadClass 调用。
我的印象是,classes 至少会被 class 加载程序缓存,而不是在每次方法调用时加载以获取 属性 进行编组。
获取属性值的代码如下:
BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(domainObject);
return beanWrapper.getPropertyValue(property.getName());
是否需要配置设置来确保 classes 只加载一次?或者可能是另一种获取 属性 的方法不会每次都导致 class 加载?或者也许是一种更高效的方法来实现这一点?
为每个域编写自定义封送拆收器 class 可以避免反射和自省,但会产生大量重复代码。
感谢任何意见。
经过大量挖掘,这就是我的发现。
使用 BeanUtils.getPropertyDescriptors 和 getValue 将始终尝试找到一个 BeanInfo class 来描述使用 class 加载器的 bean。在这种情况下,我们不为我们的 grails 域 classes 提供 BeanInfo classes,所以这个调用是多余的。我找到了一些信息,您可以在其中提供自定义 BeanInfoFactory 来绕过它并排除您的包,但我找不到如何使用 Grails 配置它。
还搜索 springframework 文档,您可以传递一个配置选项 Introspector.IGNORE_ALL_BEANINFO,它会告诉 CachedIntorspectionResults 从不查找 bean classes。然而,这在 grails 2.2.4 当前的 springframework 3.1.4 版本中不可用。较新的版本似乎有此选项。
因此,如果使用 BeanUtils,您不能绕过 class 加载器上的初始查找。但是,后续加载程序应由 CachedIntrospectionResults 缓存。不幸的是,这不会发生在我们的场景中。在查看查找是否可缓存的测试中似乎存在错误。请参阅下面的详细信息。
解决方法最终是退回到使用纯反射。而不是使用:
beanWrapper.getPropertyValue(property.getName());
使用:
PropertyDescription pd = BeanUtils.getPropertyDescriptor(domainObject.getClass(), property.getName())
pd.readMethod.invoke(domainObject)
pd缓存的地方。
修复此问题后,探查器仍然显示开箱即用的 grails 编组器在 CachedIntorspectionResults 上缺少缓存。这是由于 CachedIntrospectionResults 中的错误缓存实现所致。解决此问题的方法是将正确的 class 加载器添加到 CachedIntrospectionResults 中的 acceptedClassLoaders。
public class EnhanceCachedIntrospectionResultsAcceptedClassLoadersListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent event) {
CachedIntrospectionResults.acceptClassLoader(Thread.currentThread().getContextClassLoader().getParent());
}
public void contextDestroyed(ServletContextEvent event) {
CachedIntrospectionResults.clearClassLoader(Thread.currentThread().getContextClassLoader().getParent());
Introspector.flushCaches();
}
}
请注意,需要将父级添加到接受的 class 加载器列表而不是当前的 class 加载器。不确定这是否特定于 grails,但这解决了问题。我不确定此修复是否有副作用。
总而言之,在使用直接反射并修复 CachedIntrospectionResults 缓存后,我们从原始设置中的 10 requests/sec 增加到 120 requests/sec。
然而,真正让我们大开眼界的是,如果我们对每个域使用 1-1 编组器 class,我们会看到性能比通用编组器再提高 2 倍,在通用编组器中我们测试对象是否是class 等。我们使用通用编组器节省了大量代码,但要获得与编写 1-1 编组器相当的性能,还有很多工作要做。
希望这对遇到此问题的其他人有用...
我正在使用 Grails 2.2.4,并且有一个控制器端点可以将域对象列表转换为 JSON。在负载下(少至 5 个并发请求),编组性能非常差。进行线程转储时,线程被阻塞:
java.lang.ClassLoader.loadClass(ClassLoader.java:291)
注册了一个编组器以使用反射和内省编组所有域对象。意识到反射和内省比直接方法调用慢,我仍然看到意想不到的行为,因为 class 加载器每次都是调用者,进而发生阻塞。示例堆栈跟踪如下:
java.lang.Thread.State: BLOCKED (on object monitor)
at java.lang.ClassLoader.loadClass(ClassLoader.java:291)
- waiting to lock <785e31830> (a org.grails.plugins.tomcat.ParentDelegatingClassLoader)
at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
at java.beans.Introspector.instantiate(Introspector.java:1470)
at java.beans.Introspector.findExplicitBeanInfo(Introspector.java:431)
at java.beans.Introspector.<init>(Introspector.java:380)
at java.beans.Introspector.getBeanInfo(Introspector.java:167)
at java.beans.Introspector.getBeanInfo(Introspector.java:230)
at java.beans.Introspector.<init>(Introspector.java:389)
at java.beans.Introspector.getBeanInfo(Introspector.java:167)
at java.beans.Introspector.getBeanInfo(Introspector.java:230)
at java.beans.Introspector.<init>(Introspector.java:389)
at java.beans.Introspector.getBeanInfo(Introspector.java:167)
at java.beans.Introspector.getBeanInfo(Introspector.java:230)
at java.beans.Introspector.<init>(Introspector.java:389)
at java.beans.Introspector.getBeanInfo(Introspector.java:167)
at org.springframework.beans.CachedIntrospectionResults.<init>(CachedIntrospectionResults.java:217)
at org.springframework.beans.CachedIntrospectionResults.forClass(CachedIntrospectionResults.java:149)
at org.springframework.beans.BeanWrapperImpl.getCachedIntrospectionResults(BeanWrapperImpl.java:324)
at org.springframework.beans.BeanWrapperImpl.getPropertyValue(BeanWrapperImpl.java:727)
at org.springframework.beans.BeanWrapperImpl.getPropertyValue(BeanWrapperImpl.java:721)
at org.springframework.beans.PropertyAccessor$getPropertyValue.call(Unknown Source)
at com.ngs.id.RestDomainClassMarshaller.extractValue(RestDomainClassMarshaller.groovy:203)
...
...
加载具有相同参数的相同端点的简单基准测试导致 loadClass 调用。
我的印象是,classes 至少会被 class 加载程序缓存,而不是在每次方法调用时加载以获取 属性 进行编组。
获取属性值的代码如下:
BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(domainObject);
return beanWrapper.getPropertyValue(property.getName());
是否需要配置设置来确保 classes 只加载一次?或者可能是另一种获取 属性 的方法不会每次都导致 class 加载?或者也许是一种更高效的方法来实现这一点?
为每个域编写自定义封送拆收器 class 可以避免反射和自省,但会产生大量重复代码。
感谢任何意见。
经过大量挖掘,这就是我的发现。
使用 BeanUtils.getPropertyDescriptors 和 getValue 将始终尝试找到一个 BeanInfo class 来描述使用 class 加载器的 bean。在这种情况下,我们不为我们的 grails 域 classes 提供 BeanInfo classes,所以这个调用是多余的。我找到了一些信息,您可以在其中提供自定义 BeanInfoFactory 来绕过它并排除您的包,但我找不到如何使用 Grails 配置它。
还搜索 springframework 文档,您可以传递一个配置选项 Introspector.IGNORE_ALL_BEANINFO,它会告诉 CachedIntorspectionResults 从不查找 bean classes。然而,这在 grails 2.2.4 当前的 springframework 3.1.4 版本中不可用。较新的版本似乎有此选项。
因此,如果使用 BeanUtils,您不能绕过 class 加载器上的初始查找。但是,后续加载程序应由 CachedIntrospectionResults 缓存。不幸的是,这不会发生在我们的场景中。在查看查找是否可缓存的测试中似乎存在错误。请参阅下面的详细信息。
解决方法最终是退回到使用纯反射。而不是使用:
beanWrapper.getPropertyValue(property.getName());
使用:
PropertyDescription pd = BeanUtils.getPropertyDescriptor(domainObject.getClass(), property.getName())
pd.readMethod.invoke(domainObject)
pd缓存的地方。
修复此问题后,探查器仍然显示开箱即用的 grails 编组器在 CachedIntorspectionResults 上缺少缓存。这是由于 CachedIntrospectionResults 中的错误缓存实现所致。解决此问题的方法是将正确的 class 加载器添加到 CachedIntrospectionResults 中的 acceptedClassLoaders。
public class EnhanceCachedIntrospectionResultsAcceptedClassLoadersListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent event) {
CachedIntrospectionResults.acceptClassLoader(Thread.currentThread().getContextClassLoader().getParent());
}
public void contextDestroyed(ServletContextEvent event) {
CachedIntrospectionResults.clearClassLoader(Thread.currentThread().getContextClassLoader().getParent());
Introspector.flushCaches();
}
}
请注意,需要将父级添加到接受的 class 加载器列表而不是当前的 class 加载器。不确定这是否特定于 grails,但这解决了问题。我不确定此修复是否有副作用。
总而言之,在使用直接反射并修复 CachedIntrospectionResults 缓存后,我们从原始设置中的 10 requests/sec 增加到 120 requests/sec。
然而,真正让我们大开眼界的是,如果我们对每个域使用 1-1 编组器 class,我们会看到性能比通用编组器再提高 2 倍,在通用编组器中我们测试对象是否是class 等。我们使用通用编组器节省了大量代码,但要获得与编写 1-1 编组器相当的性能,还有很多工作要做。
希望这对遇到此问题的其他人有用...