日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

使用ThreadLocal不当可能会导致内存泄露

發布時間:2024/1/17 编程问答 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 使用ThreadLocal不当可能会导致内存泄露 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

使用ThreadLocal不當可能會導致內存泄露

?

基礎篇已經講解了ThreadLocal的原理,本節著重來講解下使用ThreadLocal會導致內存泄露的原因,并講解使用ThreadLocal導致內存泄露的案例。

1 為何會出現內存泄露

基礎篇我們講到了ThreadLocal只是一個工具類,具體存放變量的是在線程的threadLocals變量里面,threadLocals是一個ThreadLocalMap類型的,

?

image.png

如上圖ThreadLocalMap內部是一個Entry數組,Entry繼承自WeakReference,Entry內部的value用來存放通過ThreadLocal的set方法傳遞的值,那么ThreadLocal對象本身存放到哪里了嗎?下面看看Entry的構造函數:

Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}public WeakReference(T referent) {super(referent); }Reference(T referent) {this(referent, null); }Reference(T referent, ReferenceQueue<? super T> queue) {this.referent = referent;this.queue = (queue == null) ? ReferenceQueue.NULL : queue; }

可知k被傳遞到了WeakReference的構造函數里面,也就是說ThreadLocalMap里面的key為ThreadLocal對象的弱引用,具體是referent變量引用了ThreadLocal對象,value為具體調用ThreadLocal的set方法傳遞的值。

當一個線程調用ThreadLocal的set方法設置變量時候,當前線程的ThreadLocalMap里面就會存放一個記錄,這個記錄的key為ThreadLocal的引用,value則為設置的值。如果當前線程一直存在而沒有調用ThreadLocal的remove方法,并且這時候其它地方還是有對ThreadLocal的引用,則當前線程的ThreadLocalMap變量里面會存在ThreadLocal變量的引用和value對象的引用是不會被釋放的,這就會造成內存泄露的。但是考慮如果這個ThreadLocal變量沒有了其他強依賴,而當前線程還存在的情況下,由于線程的ThreadLocalMap里面的key是弱依賴,則當前線程的ThreadLocalMap里面的ThreadLocal變量的弱引用會被在gc的時候回收,但是對應value還是會造成內存泄露,這時候ThreadLocalMap里面就會存在key為null但是value不為null的entry項。其實在ThreadLocal的set和get和remove方法里面有一些時機是會對這些key為null的entry進行清理的,但是這些清理不是必須發生的,下面簡單說下ThreadLocalMap的remove方法的清理過程:

private void remove(ThreadLocal<?> key) {//(1)計算當前ThreadLocal變量所在table數組位置,嘗試使用快速定位方法Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);//(2)這里使用循環是防止快速定位失效后,變量table數組for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {//(3)找到if (e.get() == key) {//(4)找到則調用WeakReference的clear方法清除對ThreadLocal的弱引用e.clear();//(5)清理key為null的元素expungeStaleEntry(i);return;}} } private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;//(6)去掉去value的引用tab[staleSlot].value = null;tab[staleSlot] = null;size--;Entry e;int i;for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();//(7)如果key為null,則去掉對value的引用。if (k == null) {e.value = null;tab[i] = null;size--;} else {int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;}
  • 步驟(4)調用了Entry的clear方法,實際調用的是父類WeakReference的clear方法,作用是去掉對ThreadLocal的弱引用。
  • 步驟(6)是去掉對value的引用,到這里當前線程里面的當前ThreadLocal對象的信息被清理完畢了。
  • 代碼(7)從當前元素的下標開始看table數組里面的其他元素是否有key為null的,有則清理。循環退出的條件是遇到table里面有null的元素。所以這里知道null元素后面的Entry里面key 為null的元素不會被清理。

總結:ThreadLocalMap內部Entry中key使用的是對ThreadLocal對象的弱引用,這為避免內存泄露是一個進步,因為如果是強引用,那么即使其他地方沒有對ThreadLocal對象的引用,ThreadLocalMap中的ThreadLocal對象還是不會被回收,而如果是弱引用則這時候ThreadLocal引用是會被回收掉的,雖然對于的value還是不能被回收,這時候ThreadLocalMap里面就會存在key為null但是value不為null的entry項,雖然ThreadLocalMap提供了set,get,remove方法在一些時機下會對這些Entry項進行清理,但是這是不及時的,也不是每次都會執行的,所以一些情況下還是會發生內存泄露,所以在使用完畢后即使調用remove方法才是解決內存泄露的王道。

2 線程池中使用ThreadLocal導致的內存泄露

下面先看線程池中使用ThreadLocal的例子:

public class ThreadPoolTest {static class LocalVariable {private Long[] a = new Long[1024*1024];}// (1)final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,new LinkedBlockingQueue<>());// (2)final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();public static void main(String[] args) throws InterruptedException {// (3)for (int i = 0; i < 50; ++i) {poolExecutor.execute(new Runnable() {public void run() {// (4)localVariable.set(new LocalVariable());// (5)System.out.println("use local varaible");//localVariable.remove();}});Thread.sleep(1000);}// (6)System.out.println("pool execute over");}
  • 代碼(1)創建了一個核心線程數和最大線程數為5的線程池,這個保證了線程池里面隨時都有5個線程在運行。
  • 代碼(2)創建了一個ThreadLocal的變量,泛型參數為LocalVariable,LocalVariable內部是一個Long數組。
  • 代碼(3)向線程池里面放入50個任務
  • 代碼(4)設置當前線程的localVariable變量,也就是把new的LocalVariable變量放入當前線程的threadLocals變量。
  • 由于沒有調用線程池的shutdown或者shutdownNow方法所以線程池里面的用戶線程不會退出,進而JVM進程也不會退出。

運行當前代碼,使用jconsole監控堆內存變化如下圖:

?

image.png

然后解開localVariable.remove()注釋,然后在運行,觀察堆內存變化如下:

?

image.png

從運行結果一可知,當主線程處于休眠時候進程占用了大概77M內存,運行結果二則占用了大概25M內存,可知運行代碼一時候內存發生了泄露,下面分析下泄露的原因。

運行結果一的代碼,在設置線程的localVariable變量后沒有調用localVariable.remove()
方法,導致線程池里面的5個線程的threadLocals變量里面的new LocalVariable()實例沒有被釋放,雖然線程池里面的任務執行完畢了,但是線程池里面的5個線程會一直存在直到JVM退出。這里需要注意的是由于localVariable被聲明了static,雖然線程的ThreadLocalMap里面是對localVariable的弱引用,localVariable也不會被回收。運行結果二的代碼由于線程在設置localVariable變量后即使調用了localVariable.remove()方法進行了清理,所以不會存在內存泄露。

總結:線程池里面設置了ThreadLocal變量一定要記得及時清理,因為線程池里面的核心線程是一直存在的,如果不清理,那么線程池的核心線程的threadLocals變量一直會持有ThreadLocal變量。

3 Tomcat的Servlet中使用ThreadLocal導致內存泄露

首先看一個Servlet的代碼如下:

public class HelloWorldExample extends HttpServlet {private static final long serialVersionUID = 1L;static class LocalVariable {private Long[] a = new Long[1024 * 1024 * 100];}//(1)final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();@Overridepublic void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {//(2)localVariable.set(new LocalVariable());response.setContentType("text/html");PrintWriter out = response.getWriter();out.println("<html>");out.println("<head>");out.println("<title>" + "title" + "</title>");out.println("</head>");out.println("<body bgcolor=\"white\">");//(3)out.println(this.toString());//(4)out.println(Thread.currentThread().toString());out.println("</body>");out.println("</html>");} }
  • 代碼(1)創建一個localVariable對象,
  • 代碼(2)在servlet的doGet方法內設置localVariable值
  • 代碼(3)打印當前servlet的實例
  • 代碼(4)打印當前線程

修改tomcat的conf下sever.xml配置如下:

<Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="10" minSpareThreads="5"/><Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

這里設置了tomcat的處理線程池最大線程為10個,最小線程為5個,那么這個線程池是干什么用的那?這里回顧下Tomcat的容器結構,如下圖:

image.png

Tomcat中Connector組件負責接受并處理請求,其中Socket acceptor thread 負責接受用戶的訪問請求,然后把接受到的請求交給Worker threads pool線程池進行具體處理,后者就是我們在server.xml里面配置的線程池。Worker threads pool里面的線程則負責把具體請求分發到具體的應用的servlet上進行處理。

有了上述知識,下面啟動tomcat訪問該servlet多次,會發現有可能輸出下面結果

HelloWorldExample@2a10b2d2 Thread[catalina-exec-5,5,main] HelloWorldExample@2a10b2d2 Thread[catalina-exec-1,5,main] HelloWorldExample@2a10b2d2 Thread[catalina-exec-4,5,main]

其中前半部分是打印的servlet實例,這里都一樣說明多次訪問的都是一個servlet實例,后半部分中catalina-exec-5,catalina-exec-1,catalina-exec-4,說明使用了connector中線程池里面的線程5,線程1,線程4來執行serlvet的。
如果在訪問該servlet的同時打開了jconsole觀察堆內存會發現內存會飆升,究其原因是因為工作線程調用servlet的doGet方法時候,工作線程的threadLocals變量里面被添加了new LocalVariable()實例,但是沒有被remove,另外多次訪問該servlet可能用的不是工作線程池里面的同一個線程,這會導致工作線程池里面多個線程都會存在內存泄露。

更糟糕的還在后面,上面的代碼在tomcat6.0的時代,應用reload操作后會導致加載該應用的webappClassLoader釋放不了,這是因為servlet的doGet方法里面創建new LocalVariable()的時候使用的是webappclassloader,所以LocalVariable.class里面持有webappclassloader的引用,由于LocalVariable的實例沒有被釋放,所以LocalVariable.class對象也沒有沒釋放,所以
webappclassloader也沒有被釋放,那么webappclassloader加載的所有類也沒有被釋放。這是因為應用reload的時候connector組件里面的工作線程池里面的線程還是一直存在的,并且線程里面的threadLocals變量并沒有被清理。而在tomcat7.0里面這個問題被修復了,應用在reload時候會清理工作線程池中線程的threadLocals變量,tomcat7.0里面reload后會有如下提示:

十二月 31, 2017 5:44:24 下午 org.apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks 嚴重: The web application [/examples] created a ThreadLocal with key of type [java.lang.ThreadLocal] (value [java.lang.ThreadLocal@63a3e00b]) and a value of type [HelloWorldExample.LocalVariable] (value [HelloWorldExample$LocalVariable@4fd7564b]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.

4 總結

Java提供的ThreadLocal給我們編程提供了方便,但是如果使用不當也會給我們帶來致命的災難,編碼時候要養成良好的習慣,線程中使用完ThreadLocal變量后,要記得及時remove掉。

總結

以上是生活随笔為你收集整理的使用ThreadLocal不当可能会导致内存泄露的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。