使用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后會有如下提示:
4 總結
Java提供的ThreadLocal給我們編程提供了方便,但是如果使用不當也會給我們帶來致命的災難,編碼時候要養成良好的習慣,線程中使用完ThreadLocal變量后,要記得及時remove掉。
總結
以上是生活随笔為你收集整理的使用ThreadLocal不当可能会导致内存泄露的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 是时候理解下HTTPS及背后的加密原理了
- 下一篇: Java中泛型Class T, T与Cl