According to the documentation of ThreadLocal (https://docs.oracle.com/javase/6/docs/api/java/lang/ThreadLocal.html)
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
But what if this "implicit reference" is the only source of accessibility of this ThreadLocal? Consider the following class:
public class ThreadLocalBigObjectSupplier implements Supplier<BigObject> {
private final ThreadLocal<BigObject> threadLocalBigObject = ThreadLocal.withInitial(() -> new BigObject(this));
@Override
public BigObject get()
{
final BigObject bigObject = threadLocalBigObject.get();
return bigObject;
}}
It is not so important what BigObject is, but what matters is that bigObject instance stores reference to this and, as a result, threadLocalBigObject is accessible via bigObject -> this -> threadLocalBigObject. Let's see what happens if we create a many copies of ThreadLocalBigObjectSupplier
public class MemoryLeakTest {
public static void main(String[] args) {
final ExecutorService executorService = Executors.newSingleThreadExecutor();
while (true)
{
final Supplier<BigObject> supplier = new BigObjectSupplier();
executorService.execute(supplier::get);
}
}}
It turns out that this code fails with OOM quite fast (depending on your BigObject size) because executorService thread stores reference to the bigObject instances and there are no other references. From heap dump on OOM:
Is it the expected behavior of ThreadLocal in such cases? What are the standard ways / best practices to avoid this kind of memory leaks?
Additional details and environment:
public class BigObject {
private final double[] array = new double[123_456];
private final Object ref;
public BigObject(Object ref) {
this.ref = ref;
}}
MacOS BigSur 11.2.2, opendjdk 11.0.11, default GC