Hi Gary, Oli, I've filed a Jira ticket for this and opened a pull request with a fix.
Jira: https://issues.apache.org/jira/browse/LANG-1839 PR: https://github.com/apache/commons-lang/pull/1846 I reproduced the leak on current master using Oli's reproducer (VISITED grew to 1,000,000 entries after 1,000 invocations, and an OutOfMemoryError followed with a small heap). Cause: a ReflectionToStringBuilder registers its root object in the constructor, but only toString() unregisters it. A builder that is created and never stringified leaves a stale entry in REGISTRY, so REGISTRY never becomes empty. Since VISITED is only cleared when REGISTRY is empty, it is never cleared and grows on every later toString() with RecursiveToStringStyle. Fix: clear VISITED in a finally block when the outermost ReflectionToStringBuilder.toString() completes, instead of relying on REGISTRY being empty. The PR includes regression tests, and the builder package tests and checkstyle pass locally. I did not change the older, small leak of one REGISTRY entry per orphaned builder, to keep the change minimal. Happy to look at that separately if you think it's worth it. Oli, thank you for the report and the reproducer. Thanks, Raju On Fri, Oct 30, 2026 at 7:31 PM Gary Gregory <[email protected]> wrote: > Hi Oli, > > Anyone can apply for an Apache Jira account. > > Gary > > On Fri, Oct 30, 2026, 05:55 Gillespie, Oli <[email protected]> wrote: > > > Thanks. No I don't have a suggested fix, and I don't think I have an > > account for Apache Commons Jira so I figured I'd at least report it here. > > > > Oli > > > > ________________________________________ > > From: Gary Gregory <[email protected]> > > Sent: 29 October 2026 22:13 > > To: Commons Users List > > Cc: Gillespie, Oli > > Subject: RE: [EXTERNAL] Memory leak in > > ReflectionToStringBuilder/ToStringStyle > > > > CAUTION: This email originated from outside of the organization. Do not > > click links or open attachments unless you can confirm the sender and > know > > the content is safe. > > > > Hello Oli, > > > > I apologize for the bad surprise on the migration. Do you have a > > suggestion? If you have time, feel free to offer it on GitHub as a PR. > > Otherwise, please create a Jira ticket so we can track this. > > > > Thank you, > > Gary > > > > On Thu, Oct 29, 2026, 18:06 Gillespie, Oli via user < > > [email protected]<mailto:[email protected]>> wrote: > > Hi, > > > > After upgrading to 3.21.0, I saw a memory leak in my application related > > to the changes in ToStringStyle. The new 'VISITED'[1] ThreadLocal > > IdentityHashMap shows unbounded growth when used in this (admittedly > > strange) fashion. > > > > Here's a reproducer: > > > > ``` > > /* > > * curl -fLO > > > https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/3.21.0/commons-lang3-3.21.0.jar > > * java -cp commons-lang3-3.21.0.jar VisitedLeak.java // leaks > > * // Invocation 1000: REGISTRY.size=1 VISITED.size=1000000 > > * java -cp commons-lang3-3.21.0.jar VisitedLeak.java clean // control, > no > > leak > > */ > > import org.apache.commons.lang3.builder.*; > > import java.lang.reflect.Field; > > import java.util.*; > > > > public class VisitedLeak { > > static class Item { int id; } > > static class Graph { List<Object> items = new ArrayList<>() {{ for > > (int i = 0; i < 999; i++) add(new Object()); }}; } > > > > static int size(String field) throws Exception { > > Field f = ToStringStyle.class.getDeclaredField(field); > > f.setAccessible(true); > > return ((Map<?, ?>) ((ThreadLocal<?>) f.get(null)).get()).size(); > > } > > > > public static void main(String[] args) throws Exception { > > if (args.length == 0) new ReflectionToStringBuilder(new Graph(), > > new RecursiveToStringStyle()); // start leak > > for (int i = 0; i <= 1000; i++) { > > if (i % 100 == 0) System.out.printf("Invocation %d: > > REGISTRY.size=%d VISITED.size=%d%n", i, size("REGISTRY"), > size("VISITED")); > > new ReflectionToStringBuilder(new Graph(), new > > RecursiveToStringStyle()).toString(); > > } > > } > > } > > ``` > > prints `Invocation 1000: REGISTRY.size=1 VISITED.size=1000000`, > > demonstrating the leak. > > > > In my application this was because of some awkward logging code: > > `log.debug("...", new ReflectionToStringBuilder(...))`. toString() is not > > called if debug logging is not enabled, starting the leak. Easy fix on my > > side, but reporting in case you consider this a bug, or for anyone else > > that might run into it. > > > > Oli > > > > [1] > > > https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/builder/ToStringStyle.java#L607 > > > > > > > > Amazon Development Centre (London) Ltd. Registered in England and Wales > > with registration number 04543232 with its registered office at 1 > > Principal Place > > < > https://www.google.com/maps/search/1+Principal+Place?entry=gmail&source=g> > > < > https://www.google.com/maps/search/1+Principal+Place?entry=gmail&source=g > >, > > Worship Street, London EC2A 2FA, United Kingdom. > > > > > > > > > > --------------------------------------------------------------------- > > To unsubscribe, e-mail: [email protected]<mailto: > > [email protected]> > > For additional c > <https://www.google.com/maps/search/%3E+For+additional+c?entry=gmail&source=g>ommands, > e-mail: [email protected]<mailto: > > [email protected]> > > > > > > > > > > Amazon Development Centre (London) Ltd. Registered in England and Wales > > with registration number 04543232 with its registered office at 1 > > Principal Place > > < > https://www.google.com/maps/search/1+Principal+Place?entry=gmail&source=g > >, > > Worship Street, London EC2A 2FA, United Kingdom. > > > > > > > > >
