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]

Reply via email to