ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ThreadLocal系列(二):内存泄漏与remove

ThreadLocal系列(二):内存泄漏与remove ThreadLocal内存泄漏上个文章说到为什么最开始有用到remove方法是为了防范ThreadLocal内存泄漏。从这段代码可以看到map的key和value的引用关系不同GC回收的时候并不会回收value导致key是null但value指的对象却没释放Thread可能长时间运行比如线程池里的value堆积之后会发生内存泄漏。static class Entry extends WeakReferenceThreadLocal? { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal? k, Object v) { super(k); value v; } }其中key弱引用ThreadLocal对象而value强引用实际的对象。为什么要这么设计GC垃圾回收的时候会回收弱引用所指向的对象即referentThreadLocal对象本身被回收而弱引用对象Entry本身不会被回收。而在set和get的时候发现要放入的entry对应的Key是null就把value进行回收也就是说GC回收了Entry弱引用所指向的ThreadLocal对象导致e.get()返回nullvalue还在而在下一次setget的时候才会回收强引用的valueset清理private void set(ThreadLocal? key, Object value) { // We dont use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab table; int len tab.length; int i key.threadLocalHashCode (len-1); for (Entry e tab[i]; e ! null; e tab[i nextIndex(i, len)]) { ThreadLocal? k e.get(); if (k key) { e.value value; return; } if (k null) { //发现key为null准备回收value replaceStaleEntry(key, value, i); return; } } tab[i] new Entry(key, value); int sz size; if (!cleanSomeSlots(i, sz) sz threshold) rehash(); }而在replaceStaleEntry里有这么一段private void replaceStaleEntry(ThreadLocal? key, Object value, int staleSlot) { Entry[] tab table; int len tab.length; Entry e; //省略。。。 // If key not found, put new entry in stale slot tab[staleSlot].value null; tab[staleSlot] new Entry(key, value); //省略。。。 }get清理public T get() { Thread t Thread.currentThread(); ThreadLocalMap map getMap(t); if (map ! null) { //关键getEntry ThreadLocalMap.Entry e map.getEntry(this); if (e ! null) { SuppressWarnings(unchecked) T result (T)e.value; return result; } } return setInitialValue(); }进入getEntryprivate Entry getEntry(ThreadLocal? key) { int i key.threadLocalHashCode (table.length - 1); Entry e table[i]; if (e ! null e.get() key) return e; else return getEntryAfterMiss(key, i, e); // 没命中进入miss }再进入getEntryAfterMissprivate Entry getEntryAfterMiss(ThreadLocal? key, int i, Entry e) { Entry[] tab table; int len tab.length; while (e ! null) { ThreadLocal? k e.get(); if (k key) return e; if (k null) //找到了当key为null的时候会调用这个方法进入 expungeStaleEntry(i); else i nextIndex(i, len); e tab[i]; } return null; }进入expungeStaleEntryprivate int expungeStaleEntry(int staleSlot) { Entry[] tab table; int len tab.length; // expunge entry at staleSlot // 这个staleSlot就是上面传的i会把这个key为null的value清除掉 tab[staleSlot].value null; // value置为null tab[staleSlot] null; // Entry置为null size--; //继续往后找并清理脏entry // Rehash until we encounter null Entry e; int i; for (i nextIndex(staleSlot, len); (e tab[i]) ! null; //往后继续清理直到遇到entry为null才停 i nextIndex(i, len)) { ThreadLocal? k e.get(); if (k null) { // Key又是null脏entry,准备回收value e.value null; tab[i] null; size--; } else { int h k.threadLocalHashCode (len - 1); // 重新计算理想槽位 if (h ! i) { // 理想槽位 ! 当前位置说明之前是hash冲突被挤过来的上个文章说过它用的是开放定址法 tab[i] null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] ! null) h nextIndex(h, len); tab[h] e; } } } return i; }从上面可以看到get的时候不仅会清理自己的还会清理后面的。set类似不同于getset还会扫描前面的entry所以ThreadLocal的setget也对内存泄漏做了防范措施但是这其实是为了防范编程习惯导致的漏洞实际上在使用ThreadLocal的时候就应该养成手动调用remove方法的习惯ThreadLocal里的remove方法public void remove() { ThreadLocalMap m getMap(Thread.currentThread()); if (m ! null) m.remove(this); }进入removeprivate void remove(ThreadLocal? key) { Entry[] tab table; int len tab.length; int i key.threadLocalHashCode (len-1); // 算hash槽位 for (Entry e tab[i]; e ! null; e tab[i nextIndex(i, len)]) { if (e.get() key) { e.clear(); // WeakReference.clear()把key置null expungeStaleEntry(i); //清理当前往后到null return; } } }OOMpublic class TestOOM { public static void main(String[] args) throws InterruptedException { int round 0; while ((true)) { round; //在ThreadLocal里创建大对象50MB ThreadLocalbyte[] threadLocal new ThreadLocal(); threadLocal.set(new byte[1024 * 1024 * 50]); //调用GC回收弱引用所指向的对象 System.gc(); //等GC完成GC是单独的另一个线程先让main睡一会儿确保GC完成 Thread.sleep(100); //打印堆内存 Runtime r Runtime.getRuntime(); long usedMB (r.totalMemory() - r.freeMemory()) / 1024/1024; // MB System.out.println(第 round 轮已用堆: usedMB MB); } } }先设置JVM堆内存大小-Xmx300m -Xms300m -XX:PrintGCDetails运行结果[GC (System.gc()) [PSYoungGen: 57344K-856K(89600K)] 57344K-52064K(294400K), 0.0259066 secs] [Times: user0.01 sys0.00, real0.03 secs] [Full GC (System.gc()) [PSYoungGen: 856K-0K(89600K)] [ParOldGen: 51208K-51883K(204800K)] 52064K-51883K(294400K), [Metaspace: 3237K-3237K(1056768K)], 0.0154506 secs] [Times: user0.11 sys0.08, real0.02 secs] 第1轮已用堆: 61MB [GC (System.gc()) [PSYoungGen: 61995K-992K(89600K)] 113879K-104083K(294400K), 0.0145459 secs] [Times: user0.06 sys0.03, real0.01 secs] [Full GC (System.gc()) [PSYoungGen: 992K-0K(89600K)] [ParOldGen: 103091K-103895K(204800K)] 104083K-103895K(294400K), [Metaspace: 6108K-6108K(1056768K)], 0.0146028 secs] [Times: user0.09 sys0.03, real0.01 secs] 第2轮已用堆: 101MB [GC (System.gc()) [PSYoungGen: 52156K-64K(89600K)] 156051K-155159K(294400K), 0.0152130 secs] [Times: user0.02 sys0.05, real0.02 secs] [Full GC (System.gc()) [PSYoungGen: 64K-0K(89600K)] [ParOldGen: 155095K-155093K(204800K)] 155159K-155093K(294400K), [Metaspace: 6108K-6108K(1056768K)], 0.0294127 secs] [Times: user0.02 sys0.16, real0.03 secs] 第3轮已用堆: 151MB [Full GC (System.gc()) [PSYoungGen: 52349K-0K(89600K)] [ParOldGen: 155093K-103893K(204800K)] 207443K-103893K(294400K), [Metaspace: 6108K-6108K(1056768K)], 0.0083966 secs] [Times: user0.00 sys0.00, real0.01 secs] 第4轮已用堆: 108MB [GC (System.gc()) [PSYoungGen: 59287K-448K(89600K)] 163181K-155549K(294400K), 0.0155315 secs] [Times: user0.00 sys0.01, real0.02 secs] [Full GC (System.gc()) [PSYoungGen: 448K-0K(89600K)] [ParOldGen: 155101K-155389K(204800K)] 155549K-155389K(294400K), [Metaspace: 8034K-8034K(1056768K)], 0.0311568 secs] [Times: user0.08 sys0.03, real0.03 secs] 第5轮已用堆: 151MB [Full GC (System.gc()) [PSYoungGen: 52491K-51200K(89600K)] [ParOldGen: 155389K-155389K(204800K)] 207880K-206589K(294400K), [Metaspace: 8034K-8034K(1056768K)], 0.0155440 secs] [Times: user0.16 sys0.00, real0.02 secs] 第6轮已用堆: 201MB [Full GC (Ergonomics) [PSYoungGen: 52563K-51200K(89600K)] [ParOldGen: 155389K-155389K(204800K)] 207953K-206589K(294400K), [Metaspace: 8034K-8034K(1056768K)], 0.0046614 secs] [Times: user0.00 sys0.00, real0.01 secs] [Full GC (Allocation Failure) [PSYoungGen: 51200K-51200K(89600K)] [ParOldGen: 155389K-155235K(204800K)] 206589K-206435K(294400K), [Metaspace: 8034K-8013K(1056768K)], 0.0282928 secs] [Times: user0.06 sys0.08, real0.03 secs] Heap PSYoungGen total 89600K, used 54775K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000) eden space 76800K, 71% used [0x00000000f9c00000,0x00000000fd17dd80,0x00000000fe700000) from space 12800K, 0% used [0x00000000ff380000,0x00000000ff380000,0x0000000100000000) to space 12800K, 0% used [0x00000000fe700000,0x00000000fe700000,0x00000000ff380000) ParOldGen total 204800K, used 155235K [0x00000000ed400000, 0x00000000f9c00000, 0x00000000f9c00000) object space 204800K, 75% used [0x00000000ed400000,0x00000000f6b98c70,0x00000000f9c00000) Metaspace used 8060K, capacity 8286K, committed 8448K, reserved 1056768K class space used 947K, capacity 1015K, committed 1024K, reserved 1048576K Exception in thread main java.lang.OutOfMemoryError: Java heap space at threadlocal.TestOOM.main(TestOOM.java:17)看到上面第三轮到第四轮已用堆降下去了正如前面说的set的时候也会清理value在System.gc()前调用remove可以防止OOM这里就贴个结果。[GC (System.gc()) [PSYoungGen: 52692K-32K(89600K)] 54691K-2030K(294400K), 0.0003997 secs] [Times: user0.00 sys0.00, real0.00 secs] [Full GC (System.gc()) [PSYoungGen: 32K-0K(89600K)] [ParOldGen: 1998K-1998K(204800K)] 2030K-1998K(294400K), [Metaspace: 8635K-8635K(1056768K)], 0.0044947 secs] [Times: user0.00 sys0.00, real0.00 secs] 第52轮已用堆: 1MB [GC (System.gc()) [PSYoungGen: 52692K-32K(89600K)] 54691K-2030K(294400K), 0.0016404 secs] [Times: user0.00 sys0.00, real0.00 secs] [Full GC (System.gc()) [PSYoungGen: 32K-0K(89600K)] [ParOldGen: 1998K-1998K(204800K)] 2030K-1998K(294400K), [Metaspace: 8636K-8636K(1056768K)], 0.0064242 secs] [Times: user0.00 sys0.00, real0.01 secs] 第53轮已用堆: 1MB 省略。。。除了set,get以及手动调用remove防止内存泄漏还可以从弱引用方向思考如何防止OOM。当GC回收弱引用所指向的对象后对应的弱引用对象会被放入引用队列所以可以通过引用队列追踪并释放value。和ThreadLocal没太大关系只是从另一角度思考防止OOM
返回列表