ThreadLocal内存溢出问题

转载自:http://liwx2000.iteye.com/blog/1774169

最近碰到一个使用ThreadLocal时因为未调用remove()而险些引起内存溢出的问题,所以看了下ThreadLocal的源码,结合线程池原理做一个简单的分析,确认是否最终会导致内存溢出。


既然是因为没调用remove()方法而险些导致内存溢出,那首先看下remove()方法中做了什么。
Java代码   收藏代码
  1. public void remove() {  
  2.         ThreadLocalMap m = getMap(Thread.currentThread());  
  3.         if (m != null)  
  4.             m.remove(this);  
  5.     }  

从remove()的实现来看就是一个map.remove()的调用。既然不调用map.remove()可能会引起内存溢出的话,就需要看看ThreadLocalMap的实现了。
Java代码   收藏代码
  1.  /** 
  2.   * ThreadLocalMap is a customized hash map suitable only for 
  3.   * maintaining thread local values. No operations are exported 
  4.   * outside of the ThreadLocal class. The class is package private to 
  5.   * allow declaration of fields in class Thread.  To help deal with 
  6.   * very large and long-lived usages, the hash table entries use 
  7.   * WeakReferences for keys. However, since reference queues are not 
  8.   * used, stale entries are guaranteed to be removed only when 
  9.   * the table starts running out of space. 
  10.   */  
  11.  static class ThreadLocalMap {  
  12.   
  13.      /** 
  14.       * The entries in this hash map extend WeakReference, using 
  15.       * its main ref field as the key (which is always a 
  16.       * ThreadLocal object).  Note that null keys (i.e. entry.get() 
  17.       * == null) mean that the key is no longer referenced, so the 
  18.       * entry can be expunged from table.  Such entries are referred to 
  19.       * as "stale entries" in the code that follows. 
  20.       */  
  21.      static class Entry extends WeakReference<ThreadLocal> {  
  22.          /** The value associated with this ThreadLocal. */  
  23.          Object value;  
  24.   
  25.          Entry(ThreadLocal k, Object v) {  
  26.              super(k);  
  27.              value = v;  
  28.          }  
  29.      }  
  30.   
  31.      /** 
  32.       * The initial capacity -- MUST be a power of two. 
  33.       */  
  34.      private static final int INITIAL_CAPACITY = 16;  
  35.   
  36.      /** 
  37.       * The table, resized as necessary. 
  38.       * table.length MUST always be a power of two. 
  39.       */  
  40.      private Entry[] table;  
  41.   
  42.      /** 
  43.       * The number of entries in the table. 
  44.       */  
  45.      private int size = 0;  
  46.   
  47.      /** 
  48.       * The next size value at which to resize. 
  49.       */  
  50.      private int threshold; // Default to 0  
  51.   
  52.      /** 
  53.       * Set the resize threshold to maintain at worst a 2/3 load factor. 
  54.       */  
  55.      private void setThreshold(int len) {  
  56.          threshold = len * 2 / 3;  
  57.      }  
  58.   
  59.      /** 
  60.       * Increment i modulo len. 
  61.       */  
  62.      private static int nextIndex(int i, int len) {  
  63.          return ((i + 1 < len) ? i + 1 : 0);  
  64.      }  
  65.   
  66.      /** 
  67.       * Decrement i modulo len. 
  68.       */  
  69.      private static int prevIndex(int i, int len) {  
  70.          return ((i - 1 >= 0) ? i - 1 : len - 1);  
  71.      }  
  72.   
  73.      /** 
  74.       * Construct a new map initially containing (firstKey, firstValue). 
  75.       * ThreadLocalMaps are constructed lazily, so we only create 
  76.       * one when we have at least one entry to put in it. 
  77.       */  
  78.      ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {  
  79.          table = new Entry[INITIAL_CAPACITY];  
  80.          int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  
  81.          table[i] = new Entry(firstKey, firstValue);  
  82.          size = 1;  
  83.          setThreshold(INITIAL_CAPACITY);  
  84.      }  
  85.   
  86.      /** 
  87.       * Construct a new map including all Inheritable ThreadLocals 
  88.       * from given parent map. Called only by createInheritedMap. 
  89.       * 
  90.       * @param parentMap the map associated with parent thread. 
  91.       */  
  92.      private ThreadLocalMap(ThreadLocalMap parentMap) {  
  93.          Entry[] parentTable = parentMap.table;  
  94.          int len = parentTable.length;  
  95.          setThreshold(len);  
  96.          table = new Entry[len];  
  97.   
  98.          for (int j = 0; j < len; j++) {  
  99.              Entry e = parentTable[j];  
  100.              if (e != null) {  
  101.                  ThreadLocal key = e.get();  
  102.                  if (key != null) {  
  103.                      Object value = key.childValue(e.value);  
  104.                      Entry c = new Entry(key, value);  
  105.                      int h = key.threadLocalHashCode & (len - 1);  
  106.                      while (table[h] != null)  
  107.                          h = nextIndex(h, len);  
  108.                      table[h] = c;  
  109.                      size++;  
  110.                  }  
  111.              }  
  112.          }  
  113.      }  
  114.   
  115.      /** 
  116.       * Get the entry associated with key.  This method 
  117.       * itself handles only the fast path: a direct hit of existing 
  118.       * key. It otherwise relays to getEntryAfterMiss.  This is 
  119.       * designed to maximize performance for direct hits, in part 
  120.       * by making this method readily inlinable. 
  121.       * 
  122.       * @param  key the thread local object 
  123.       * @return the entry associated with key, or null if no such 
  124.       */  
  125.      private Entry getEntry(ThreadLocal key) {  
  126.          int i = key.threadLocalHashCode & (table.length - 1);  
  127.          Entry e = table[i];  
  128.          if (e != null && e.get() == key)  
  129.              return e;  
  130.          else  
  131.              return getEntryAfterMiss(key, i, e);  
  132.      }  
  133.   
  134.      /** 
  135.       * Version of getEntry method for use when key is not found in 
  136.       * its direct hash slot. 
  137.       * 
  138.       * @param  key the thread local object 
  139.       * @param  i the table index for key's hash code 
  140.       * @param  e the entry at table[i] 
  141.       * @return the entry associated with key, or null if no such 
  142.       */  
  143.      private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {  
  144.          Entry[] tab = table;  
  145.          int len = tab.length;  
  146.   
  147.          while (e != null) {  
  148.              ThreadLocal k = e.get();  
  149.              if (k == key)  
  150.                  return e;  
  151.              if (k == null)  
  152.                  expungeStaleEntry(i);  
  153.              else  
  154.                  i = nextIndex(i, len);  
  155.              e = tab[i];  
  156.          }  
  157.          return null;  
  158.      }  
  159.   
  160.      /** 
  161.       * Set the value associated with key. 
  162.       * 
  163.       * @param key the thread local object 
  164.       * @param value the value to be set 
  165.       */  
  166.      private void set(ThreadLocal key, Object value) {  
  167.   
  168.          // We don't use a fast path as with get() because it is at  
  169.          // least as common to use set() to create new entries as  
  170.          // it is to replace existing ones, in which case, a fast  
  171.          // path would fail more often than not.  
  172.   
  173.          Entry[] tab = table;  
  174.          int len = tab.length;  
  175.          int i = key.threadLocalHashCode & (len-1);  
  176.   
  177.          for (Entry e = tab[i];  
  178. e != null;  
  179. e = tab[i = nextIndex(i, len)]) {  
  180.              ThreadLocal k = e.get();  
  181.   
  182.              if (k == key) {  
  183.                  e.value = value;  
  184.                  return;  
  185.              }  
  186.   
  187.              if (k == null) {  
  188.                  replaceStaleEntry(key, value, i);  
  189.                  return;  
  190.              }  
  191.          }  
  192.   
  193.          tab[i] = new Entry(key, value);  
  194.          int sz = ++size;  
  195.          if (!cleanSomeSlots(i, sz) && sz >= threshold)  
  196.              rehash();  
  197.      }  
  198.   
  199.      /** 
  200.       * Remove the entry for key. 
  201.       */  
  202.      private void remove(ThreadLocal key) {  
  203.          Entry[] tab = table;  
  204.          int len = tab.length;  
  205.          int i = key.threadLocalHashCode & (len-1);  
  206.          for (Entry e = tab[i];  
  207. e != null;  
  208. e = tab[i = nextIndex(i, len)]) {  
  209.              if (e.get() == key) {  
  210.                  e.clear();  
  211.                  expungeStaleEntry(i);  
  212.                  return;  
  213.              }  
  214.          }  
  215.      }  
  216.   
  217.      /** 
  218.       * Replace a stale entry encountered during a set operation 
  219.       * with an entry for the specified key.  The value passed in 
  220.       * the value parameter is stored in the entry, whether or not 
  221.       * an entry already exists for the specified key. 
  222.       * 
  223.       * As a side effect, this method expunges all stale entries in the 
  224.       * "run" containing the stale entry.  (A run is a sequence of entries 
  225.       * between two null slots.) 
  226.       * 
  227.       * @param  key the key 
  228.       * @param  value the value to be associated with key 
  229.       * @param  staleSlot index of the first stale entry encountered while 
  230.       *         searching for key. 
  231.       */  
  232.      private void replaceStaleEntry(ThreadLocal key, Object value,  
  233.                                     int staleSlot) {  
  234.          Entry[] tab = table;  
  235.          int len = tab.length;  
  236.          Entry e;  
  237.   
  238.          // Back up to check for prior stale entry in current run.  
  239.          // We clean out whole runs at a time to avoid continual  
  240.          // incremental rehashing due to garbage collector freeing  
  241.          // up refs in bunches (i.e., whenever the collector runs).  
  242.          int slotToExpunge = staleSlot;  
  243.          for (int i = prevIndex(staleSlot, len);  
  244. (e = tab[i]) != null;  
  245.               i = prevIndex(i, len))  
  246.              if (e.get() == null)  
  247.                  slotToExpunge = i;  
  248.   
  249.          // Find either the key or trailing null slot of run, whichever  
  250.          // occurs first  
  251.          for (int i = nextIndex(staleSlot, len);  
  252. (e = tab[i]) != null;  
  253.               i = nextIndex(i, len)) {  
  254.              ThreadLocal k = e.get();  
  255.   
  256.              // If we find key, then we need to swap it  
  257.              // with the stale entry to maintain hash table order.  
  258.              // The newly stale slot, or any other stale slot  
  259.              // encountered above it, can then be sent to expungeStaleEntry  
  260.              // to remove or rehash all of the other entries in run.  
  261.              if (k == key) {  
  262.                  e.value = value;  
  263.   
  264.                  tab[i] = tab[staleSlot];  
  265.                  tab[staleSlot] = e;  
  266.   
  267.                  // Start expunge at preceding stale entry if it exists  
  268.                  if (slotToExpunge == staleSlot)  
  269.                      slotToExpunge = i;  
  270.                  cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);  
  271.                  return;  
  272.              }  
  273.   
  274.              // If we didn't find stale entry on backward scan, the  
  275.              // first stale entry seen while scanning for key is the  
  276.              // first still present in the run.  
  277.              if (k == null && slotToExpunge == staleSlot)  
  278.                  slotToExpunge = i;  
  279.          }  
  280.   
  281.          // If key not found, put new entry in stale slot  
  282.          tab[staleSlot].value = null;     
  283.          tab[staleSlot] = new Entry(key, value);  
  284.   
  285.          // If there are any other stale entries in run, expunge them  
  286.          if (slotToExpunge != staleSlot)  
  287.              cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);  
  288.      }  
  289.   
  290.      /** 
  291.       * Expunge a stale entry by rehashing any possibly colliding entries 
  292.       * lying between staleSlot and the next null slot.  This also expunges 
  293.       * any other stale entries encountered before the trailing null.  See 
  294.       * Knuth, Section 6.4 
  295.       * 
  296.       * @param staleSlot index of slot known to have null key 
  297.       * @return the index of the next null slot after staleSlot 
  298.       * (all between staleSlot and this slot will have been checked 
  299.       * for expunging). 
  300.       */  
  301.      private int expungeStaleEntry(int staleSlot) {  
  302.          Entry[] tab = table;  
  303.          int len = tab.length;  
  304.   
  305.          // expunge entry at staleSlot  
  306.          tab[staleSlot].value = null;     
  307.          tab[staleSlot] = null;  
  308.          size--;  
  309.   
  310.          // Rehash until we encounter null  
  311.          Entry e;  
  312.          int i;  
  313.          for (i = nextIndex(staleSlot, len);  
  314. (e = tab[i]) != null;  
  315.               i = nextIndex(i, len)) {  
  316.              ThreadLocal k = e.get();  
  317.              if (k == null) {  
  318.                  e.value = null;  
  319.                  tab[i] = null;  
  320.                  size--;  
  321.              } else {  
  322.                  int h = k.threadLocalHashCode & (len - 1);  
  323.                  if (h != i) {  
  324.                      tab[i] = null;  
  325.   
  326.                      // Unlike Knuth 6.4 Algorithm R, we must scan until  
  327.                      // null because multiple entries could have been stale.  
  328.                      while (tab[h] != null)  
  329.                          h = nextIndex(h, len);  
  330.                      tab[h] = e;  
  331.                  }  
  332.              }  
  333.          }  
  334.          return i;  
  335.      }  
  336.   
  337.      /** 
  338.       * Heuristically scan some cells looking for stale entries. 
  339.       * This is invoked when either a new element is added, or 
  340.       * another stale one has been expunged. It performs a 
  341.       * logarithmic number of scans, as a balance between no 
  342.       * scanning (fast but retains garbage) and a number of scans 
  343.       * proportional to number of elements, that would find all 
  344.       * garbage but would cause some insertions to take O(n) time. 
  345.       * 
  346.       * @param i a position known NOT to hold a stale entry. The 
  347.       * scan starts at the element after i. 
  348.       * 
  349.       * @param n scan control: <tt>log2(n)</tt> cells are scanned, 
  350.       * unless a stale entry is found, in which case 
  351.       * <tt>log2(table.length)-1</tt> additional cells are scanned. 
  352.       * When called from insertions, this parameter is the number 
  353.       * of elements, but when from replaceStaleEntry, it is the 
  354.       * table length. (Note: all this could be changed to be either 
  355.       * more or less aggressive by weighting n instead of just 
  356.       * using straight log n. But this version is simple, fast, and 
  357.       * seems to work well.) 
  358.       * 
  359.       * @return true if any stale entries have been removed. 
  360.       */  
  361.      private boolean cleanSomeSlots(int i, int n) {  
  362.          boolean removed = false;  
  363.          Entry[] tab = table;  
  364.          int len = tab.length;  
  365.          do {  
  366.              i = nextIndex(i, len);  
  367.              Entry e = tab[i];  
  368.              if (e != null && e.get() == null) {  
  369.                  n = len;  
  370.                  removed = true;  
  371.                  i = expungeStaleEntry(i);  
  372.              }  
  373.          } while ( (n >>>= 1) != 0);  
  374.          return removed;  
  375.      }  
  376.   
  377.      /** 
  378.       * Re-pack and/or re-size the table. First scan the entire 
  379.       * table removing stale entries. If this doesn't sufficiently 
  380.       * shrink the size of the table, double the table size. 
  381.       */  
  382.      private void rehash() {  
  383.          expungeStaleEntries();  
  384.   
  385.          // Use lower threshold for doubling to avoid hysteresis  
  386.          if (size >= threshold - threshold / 4)  
  387.              resize();  
  388.      }  
  389.   
  390.      /** 
  391.       * Double the capacity of the table. 
  392.       */  
  393.      private void resize() {  
  394.          Entry[] oldTab = table;  
  395.          int oldLen = oldTab.length;  
  396.          int newLen = oldLen * 2;  
  397.          Entry[] newTab = new Entry[newLen];  
  398.          int count = 0;  
  399.   
  400.          for (int j = 0; j < oldLen; ++j) {  
  401.              Entry e = oldTab[j];  
  402.              if (e != null) {  
  403.                  ThreadLocal k = e.get();  
  404.                  if (k == null) {  
  405.                      e.value = null// Help the GC  
  406.                  } else {  
  407.                      int h = k.threadLocalHashCode & (newLen - 1);  
  408.                      while (newTab[h] != null)  
  409.                          h = nextIndex(h, newLen);  
  410.                      newTab[h] = e;  
  411.                      count++;  
  412.                  }  
  413.              }  
  414.          }  
  415.   
  416.          setThreshold(newLen);  
  417.          size = count;  
  418.          table = newTab;  
  419.      }  
  420.   
  421.      /** 
  422.       * Expunge all stale entries in the table. 
  423.       */  
  424.      private void expungeStaleEntries() {  
  425.          Entry[] tab = table;  
  426.          int len = tab.length;  
  427.          for (int j = 0; j < len; j++) {  
  428.              Entry e = tab[j];  
  429.              if (e != null && e.get() == null)  
  430.                  expungeStaleEntry(j);  
  431.          }  
  432.      }  
  433.  }  

首先从声明上来看,ThreadLocalMap并不是一个java.util.Map接口的实现,但是从Entry的实现和整个ThreadLocalMap的实现来看却实现了一个Map的功能,并且从具体的方法的实现上来看,整个ThreadLocalMap实现了一个HashMap的功能,对比HashMap的实现就能看出。

但是,值得注意的是ThreadLocalMap并没有put(K key, V value)方法,而是set(ThreadLocal key, Object value),从这里可以看出,ThreadLocalMap并不是想象那样以Thread为key,而是以ThreadLocal为key。

了解了ThreadLocalMap的实现,也知道ThreadLocal.remove()其实就是ThreadLocalMap.remove(),那么再看看ThreadLocal的set(T value)方法,看看value是如何存储的。
Java代码   收藏代码
  1. public void set(T value) {  
  2.         Thread t = Thread.currentThread();  
  3.         ThreadLocalMap map = getMap(t);  
  4.         if (map != null)  
  5.             map.set(this, value);  
  6.         else  
  7.             createMap(t, value);  
  8.     }  
  9.   
  10.     ThreadLocalMap getMap(Thread t) {  
  11.         return t.threadLocals;  
  12.     }  
  13.   
  14.     void createMap(Thread t, T firstValue) {  
  15.         t.threadLocals = new ThreadLocalMap(this, firstValue);  
  16.     }  

可以看到,set(T value)方法为每个Thread对象都创建了一个ThreadLocalMap,并且将value放入ThreadLocalMap中,ThreadLocalMap作为Thread对象的成员变量保存。那么可以用下图来表示ThreadLocal在存储value时的关系。



所以当ThreadLocal作为单例时,每个Thread对应的ThreadLocalMap中只会有一个键值对。那么如果不调用remove()会怎么样呢?

假设一种场景,使用线程池,线程池中有200个线程,并且这些线程都不会释放,ThreadLocal做单例使用。那么最多也就会产生200个ThreadLocalMap,而每个ThreadLocalMap中只有一个键值对,那最多也就是200个键值对存在。

但是线程池并不是固定一个线程数不改变,下面贴一段tomcat的线程池配置
Java代码   收藏代码
  1. <Connector executor="tomcatThreadPool"   
  2.             port="8080" protocol="HTTP/1.1"  
  3.             connectionTimeout="60000"  
  4.             keepAliveTimeout="30000"  
  5.             minProcessors="5"  
  6.             maxProcessors="75"  
  7.             maxKeepAliveRequests="150"  
  8.             redirectPort="8443" URIEncoding="UTF-8" acceptCount="1000" disableUploadTimeout="true"/>  

可以看到线程池其实有线程最小值和最大值的,并且有超时时间,所以当线程空闲时间超时后,线程会被销毁。那么当线程销毁时,线程所持有的ThreadLocalMap也会失去引用,并且由于ThreadLocalMap中的Entry是WeakReference,所以当YGC时,被销毁的Thread所对应的value也会被回收掉,所以即使不调用remove()方法,也不会引起内存溢出。

猜你喜欢

转载自blog.csdn.net/u013182960/article/details/81027774