The memory leak can not be solved using finalize()
, since the method is deprecated, and should not be used.
If you're not using it as an instance variable or setting your
instanceThreadLocal = null; // this will not directly remove the reference in the Map
instanceThreadLocal.set(null);
instanceThreadLocal.remove();
then the Entry
(extends WeakReference
in the ThreadLocal
implementation) becomes eligable for garbage collection, when it goes out of scope. That means that there are no guarantees. A local reference is equivalent to the first line above.
The javaDoc part of static class ThreadLocalMap
in the ThreadLocal
implementation of java version "11.0.3" 2019-04-16 LTS
by Josh Bloch and Doug Lea states:
...However, since reference queues are not used, stale entries are
guaranteed to be removed only when the table starts running out of
space.
..so the only power you have on the situation is removing the reference yourself. My guess is that the decision to not use a reference queue could be that it's easy to remove the value, and therefore no different than having any other Map
and forgetting to remove unwanted objects.
I did some more checking into the ThreadLocal
s. My recommendation is still to use threadLocal.set()
or threadLocal.remove()
.
When the first ThreadLocal
is created, a ThreadLocalMap
(static nested class of ThreadLocal
) is allocated to the currentThread
. When a map has been set, then it will be used for all ThreadLocal
s of that Thread. In the below snippet from ThreadLocal.java
t
is Thread.currentThread()
:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Since ThreadLocalMap
is a static class it doesn't hold a reference to it's enclosing ThreadLocal
class, so setting a ThreadLocal
to null
should clear the memory after a while. I created a small test though, just to see if the weakReference
would clear up memory on running the Garbage Collector
. The program allocates data into three different ThreadLocals
. I found that the memory is not cleared, unless the ThreadLocal
value is changed via set()
or remove()
. threadLocal = null
had no effect. The below initial numbers are miliseconds from program start:
Result when threadListX.set(new ArrayList()) or threadListX.remove():
0000: Initally 16:44:15 Used: 9067744 B
0000: Garbage collection 16:44:15 Used: 3492656 B
1100: Threads created 16:44:16 Used: 3595992 B
1100: Garbage collection 16:44:16 Used: 3492168 B
3100: Memory of allocated data 16:44:18 Used: 110198712 B
3100: Garbage collection 16:44:18 Used: 83217256 B
5100: Memory of deallocated data 16:44:20 Used: 83599352 B
5100: Garbage collection 16:44:20 Used: 3493856 B
7100: Threads are gone 16:44:23 Used: 3520144 B
7100: Garbage collection 16:44:23 Used: 3493088 B
Result when threadListX = null:
0000: Initally 16:43:13 Used: 12645112 B
0000: Garbage collection 16:43:13 Used: 3502904 B
1100: Threads created 16:43:14 Used: 3581920 B
1100: Garbage collection 16:43:14 Used: 3492192 B
3100: Memory of allocated data 16:43:16 Used: 115017296 B
3100: Garbage collection 16:43:16 Used: 83217280 B
5100: Memory of deallocated data 16:43:18 Used: 83384640 B
5100: Garbage collection 16:43:19 Used: 83217328 B
7100: Threads are gone 16:43:21 Used: 83384688 B
7100: Garbage collection 16:43:21 Used: 3493112 B
Result with any choice & allocateData(30000000). The memory allocation is too much and the timing is too short to get an accurate result:
0000: Initally 16:41:15 Used: 9175024 B
0000: Garbage collection 16:41:15 Used: 3501704 B
1100: Threads created 16:41:16 Used: 3604744 B
1100: Garbage collection 16:41:16 Used: 3492000 B
3100: Memory of allocated data 16:41:18 Used: 127104568 B
3100: Garbage collection 16:41:18 Used: 93591800 B
5100: Memory of deallocated data 16:41:20 Used: 490867424 B
5100: Garbage collection 16:41:21 Used: 379206200 B
7100: Threads are gone 16:41:23 Used: 1235243488 B
7100: Garbage collection 16:41:24 Used: 1038918304 B
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Thread-0"
Program:
import java.time.LocalDateTime;
import java.util.List;
import java.util.ArrayList;
public class StackOverflowTest {
public static void main(String[] args){
StackOverflowTest test = new StackOverflowTest();
/*
Timeline. Numbers are in miliseconds:
0000: Initial memory
0100: Start threads <-- no output of memory
1100: Threads created
2100: Threads allocate data <-- no output of memory
3100: Memory of allocated data
4100: Threads release data <-- no output of memory
5100: Memory of deallocated data
6100: Threads die <-- no output of memory
7100: Threads are gone
*/
try {
printUsedMemoryAndGC("0000", "Initally");
Thread.sleep(100);
for (int i = 0; i < 1; i++) { // set to just one Thread
new Thread(new RunnableTest()).start();
}
Thread.sleep(1000);
printUsedMemoryAndGC("1100", "Threads created");
Thread.sleep(2000);
printUsedMemoryAndGC("3100", "Memory of allocated data");
Thread.sleep(2000);
printUsedMemoryAndGC("5100", "Memory of deallocated data");
Thread.sleep(2000);
printUsedMemoryAndGC("7100", "Threads are gone");
} catch (InterruptedException ex) {
// we don't care about this
}
}
public static void printUsedMemoryAndGC(String time, String message) {
Runtime rt = Runtime.getRuntime();
long total;
long free;
total = rt.totalMemory();
free = rt.freeMemory();
System.out.printf("%-4s: %-30s %3$tH:%3$tM:%3$tS Used: %4$10s B\n",
time, message, LocalDateTime.now(), total-free);
System.gc();
total = rt.totalMemory();
free = rt.freeMemory();
System.out.printf("%-4s: %-30s %3$tH:%3$tM:%3$tS Used: %4$10s B\n",
time, "Garbage collection", LocalDateTime.now(), total-free);
}
}
class RunnableTest implements Runnable{
private ThreadLocal<List<Integer>> threadList1 =
new ThreadLocal<List<Integer>>() {
@Override
protected List<Integer> initialValue() {
return new ArrayList<Integer>();
}
};
private ThreadLocal<List<Long>> threadList2 =
new ThreadLocal<List<Long>>() {
@Override
protected List<Long> initialValue() {
return new ArrayList<Long>();
}
};
private ThreadLocal<List<Double>> threadList3 =
new ThreadLocal<List<Double>>() {
@Override
protected List<Double> initialValue() {
return new ArrayList<Double>();
}
};
public void run(){
try {
Thread.sleep(2000);
allocateData(1000000);
// allocateData(30000000); // This is too much for my system.
Thread.sleep(2000);
clearData();
Thread.sleep(2000); // don't just stop yet.
} catch (InterruptedException ex) {
// we don't care about this
}
}
public void allocateData(int max) {
List<Integer> list1 = threadList1.get();
List<Long> list2 = threadList2.get();
List<Double> list3 = threadList3.get();
for (int i = 0; i < max; i++) {
list1.add(Integer.valueOf(i));
list2.add(Long.valueOf(i));
list3.add(Double.valueOf(i));
}
}
public void clearData() {
// threadList1 = null; // No effect.
// threadList2 = null;
// threadList3 = null;
threadList1.set(new ArrayList<Integer>()); // resetting the value
threadList2.set(new ArrayList<Long>());
threadList3.set(new ArrayList<Double>());
// threadList1.remove(); // removing the value
// threadList2.remove();
// threadList3.remove();
}
}