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>, Worship Street, London EC2A 2FA, United Kingdom. --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected]<mailto:[email protected]> For additional commands, 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, Worship Street, London EC2A 2FA, United Kingdom. --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
