https://bz.apache.org/bugzilla/show_bug.cgi?id=70241
Bug ID: 70241
Summary: EL method invocation calls Class.getMethods() on every
evaluation; resolved methods are not cached, unlike
bean properties
Product: Tomcat 10
Version: 10.1.60
Hardware: PC
OS: Mac OS X 10.1
Status: NEW
Severity: normal
Priority: P2
Component: EL
Assignee: [email protected]
Reporter: [email protected]
Target Milestone: ------
Util.findMethod calls Class.getMethods() on every EL method invocation, and
there is no cache of resolved methods. Class.getMethods() is required by its
contract to return a fresh array of fresh Method objects, since callers may
call setAccessible() on them, so every invocation allocates in proportion to
the method count of the receiving class.
This is asymmetric with property access. BeanELResolver already caches property
descriptors through BeanELResolver$ConcurrentCache, so ${bean.foo} is cheap
while ${bean.foo(x)} is not, for the same bean and the same underlying method.
Verified by inspecting the shipped jars:
Tomcat 9.0.115, el-api.jar, javax.el.Util.findMethod calls Class.getMethods()
(bytecode offset 80)
Tomcat 10.1.5, el-api.jar, jakarta.el.Util.findMethod calls Class.getMethods()
(bytecode offset 55)
In both, the only cache in Util is factoryCache, which holds ExpressionFactory
instances and is unrelated.
Cost of Class.getMethods(), measured with
com.sun.management.ThreadMXBean.getThreadAllocatedBytes over 10000 iterations
on a 213 method class, after warmup:
JDK 11.0.16: 19,616 bytes per call
JDK 17.0.16: 19,616 bytes per call
JDK 18.0.2.1: 19,616 bytes per call
JDK 21.0.10: 19,616 bytes per call
Identical across versions, including 18, where JEP 416 reimplemented core
reflection on method handles; that changed Method.invoke, not getMethods(). The
cost works out at roughly 92 bytes per method on the class, so a 600 method
utility class costs about 55 kB per EL method invocation.
Impact seen in a real application. On a page rendering about 4600 table rows, a
single ${someUtil.someMethod(x)} in the row markup accounted for 251 of 409
allocation samples in a JFR jdk.ObjectAllocationSample recording, all in
Method.copy and Class.copyMethods beneath Util.findMethod and
BeanELResolver.invoke. Replacing that one expression with a bean property took
the request from about 719 MB to about 459 MB allocated. A second expression on
the same page, ${list.size() > 0}, was the same pattern on a smaller receiver.
Possible fix: cache resolved methods keyed on class, method name and parameter
types, in the way BeanELResolver already caches properties. The obvious hazard
is retaining Class references in a static map inside a container and pinning
web application class loaders across reloads; BeanELResolver$ConcurrentCache is
existing precedent in the same codebase for handling that concern.
--
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]