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]

Reply via email to