ThreadLocal内存溢出问题

2024-08-31 07:58

本文主要是介绍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()方法,也不会引起内存溢出。

这篇关于ThreadLocal内存溢出问题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1123327

相关文章

JAVA Calendar设置上个月时,日期不存在或错误提示问题及解决

《JAVACalendar设置上个月时,日期不存在或错误提示问题及解决》在使用Java的Calendar类设置上个月的日期时,如果遇到不存在的日期(如4月31日),默认会自动调整到下个月的相应日期(... 目录Java Calendar设置上个月时,日期不存在或错误提示java进行日期计算时如果出现不存在的

Mybatis对MySQL if 函数的不支持问题解读

《Mybatis对MySQLif函数的不支持问题解读》接手项目后,为了实现多租户功能,引入了Mybatis-plus,发现之前运行正常的SQL语句报错,原因是Mybatis不支持MySQL的if函... 目录MyBATis对mysql if 函数的不支持问题描述经过查询网上搜索资料找到原因解决方案总结Myb

Nginx错误拦截转发 error_page的问题解决

《Nginx错误拦截转发error_page的问题解决》Nginx通过配置错误页面和请求处理机制,可以在请求失败时展示自定义错误页面,提升用户体验,下面就来介绍一下Nginx错误拦截转发error_... 目录1. 准备自定义错误页面2. 配置 Nginx 错误页面基础配置示例:3. 关键配置说明4. 生效

Springboot3统一返回类设计全过程(从问题到实现)

《Springboot3统一返回类设计全过程(从问题到实现)》文章介绍了如何在SpringBoot3中设计一个统一返回类,以实现前后端接口返回格式的一致性,该类包含状态码、描述信息、业务数据和时间戳,... 目录Spring Boot 3 统一返回类设计:从问题到实现一、核心需求:统一返回类要解决什么问题?

maven异常Invalid bound statement(not found)的问题解决

《maven异常Invalidboundstatement(notfound)的问题解决》本文详细介绍了Maven项目中常见的Invalidboundstatement异常及其解决方案,文中通过... 目录Maven异常:Invalid bound statement (not found) 详解问题描述可

idea粘贴空格时显示NBSP的问题及解决方案

《idea粘贴空格时显示NBSP的问题及解决方案》在IDEA中粘贴代码时出现大量空格占位符NBSP,可以通过取消勾选AdvancedSettings中的相应选项来解决... 目录1、背景介绍2、解决办法3、处理完成总结1、背景介绍python在idehttp://www.chinasem.cna粘贴代码,出

SpringBoot整合Kafka启动失败的常见错误问题总结(推荐)

《SpringBoot整合Kafka启动失败的常见错误问题总结(推荐)》本文总结了SpringBoot项目整合Kafka启动失败的常见错误,包括Kafka服务器连接问题、序列化配置错误、依赖配置问题、... 目录一、Kafka服务器连接问题1. Kafka服务器无法连接2. 开发环境与生产环境网络不通二、序

SpringSecurity中的跨域问题处理方案

《SpringSecurity中的跨域问题处理方案》本文介绍了跨域资源共享(CORS)技术在JavaEE开发中的应用,详细讲解了CORS的工作原理,包括简单请求和非简单请求的处理方式,本文结合实例代码... 目录1.什么是CORS2.简单请求3.非简单请求4.Spring跨域解决方案4.1.@CrossOr

nacos服务无法注册到nacos服务中心问题及解决

《nacos服务无法注册到nacos服务中心问题及解决》本文详细描述了在Linux服务器上使用Tomcat启动Java程序时,服务无法注册到Nacos的排查过程,通过一系列排查步骤,发现问题出在Tom... 目录简介依赖异常情况排查断点调试原因解决NacosRegisterOnWar结果总结简介1、程序在

解决java.util.RandomAccessSubList cannot be cast to java.util.ArrayList错误的问题

《解决java.util.RandomAccessSubListcannotbecasttojava.util.ArrayList错误的问题》当你尝试将RandomAccessSubList... 目录Java.util.RandomAccessSubList cannot be cast to java.