0

The ThreadLocal will act as entry key of Thread.threadLocals : ThreadLocal.ThreadLocalMap. It's wrapped by WeakReference. If the ThreadLocal object is GCed (but there's no call ThreadLocal.remove()) and the thread is still alive (such as in a Thread Pool), then the memory will leak.

If Java internally used some mechanism to prevent that, like adding a remove() call in ThreadLocal.finalize(), wouldn't the memory leak problem be solved?

Dharman
  • 30,962
  • 25
  • 85
  • 135
iseki
  • 63
  • 1
  • 12
  • Can you provide the code snippet ? – Eric Feb 05 '20 at 03:32
  • 1
    @EricWang What snippet? Is it useful? I means I don't understand why java designed as it. It cause the problem – iseki Feb 05 '20 at 03:39
  • A TheadLocal object is not garbage collected it it's Thread it still alive. – Scratte Feb 05 '20 at 03:42
  • @Scratte Looks when thread still alive but the key of ThreadLocalMap(ThreadLocal object) is gced. The value will be inaccessable and unGCable. Why java not automatically clear it use some machanism such as `finalize` / `ReferenceQueue` ? – iseki Feb 05 '20 at 03:49
  • I think you'll need to post some code. I don't know what a ThreadLocalMap is. According to [Threadlocal Memory Leak](https://stackoverflow.com/questions/30992479/threadlocal-memory-leak), there is no apparent memory leak. In addition: Any instance can use `remove()` when the strong reference is no longer needed. – Scratte Feb 05 '20 at 03:59
  • @Scratte Yeah,,, it's the problem. If I forget to call `remove()`... – iseki Feb 05 '20 at 04:04
  • @Scratte `Thread::threadLocals` . `ThreadLocal` act as a key of it. If java add a cleaner(such as `remove()`) to ThreadLocal 's `finalize()`, the problem will be avoid. – iseki Feb 05 '20 at 04:09
  • Kindly edit your Question, so we have a clear understand of what you think the key is. – Scratte Feb 05 '20 at 04:12
  • @Scratte thank you for your response. I will try to edit my question. – iseki Feb 05 '20 at 04:13
  • Questions that asks why a company has made a decision is not applicable to StackOverflow, and will very likely be closed/deleted, so I've rephrased your question to comply with standard. If you wish to know on what basis Oracle makes decision, you'll have to ask them. – Scratte Feb 05 '20 at 13:06
  • @Dharman Thank you for your edit. My question is why java can't provide a mechanism avoid this problem. Maybe my idea is unrealistic? – iseki Feb 05 '20 at 16:57

1 Answers1

2

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 ThreadLocals. 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 ThreadLocals 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();

  }
}
Scratte
  • 3,056
  • 6
  • 19
  • 26
  • What if the key value is wrapped by `ThreadLocal` ? If all `ThreadLocal` is inaccessible, can the the GC remove the value together?(Assume the value hasn't other strong reference) – iseki Feb 05 '20 at 17:03
  • @cpdyj The `Entry(ThreadLocal> k, Object v)` constuctor is calling `super(k);`, so yes, I **think** so. If there are no strong references to ThreadLocals, then those ThreadLocals can be garbage collected with their values. I find it somewhat tricky with `Entry extends WeakReference>`, when the `ThreadLocal>` itself being the key to a Map held by the Thread. – Scratte Feb 05 '20 at 19:06
  • But if you want to be absolutely sure, you have to test it. Also there are many post here that discusses `WeakReference` and `ThreadLocal`, and of course one can always spend a long time reading the code of the API. – Scratte Feb 05 '20 at 19:15