作者:草莓公主滴窝窝 | 来源:互联网 | 2023-02-02 19:55
内存泄漏和内存溢出:
内存泄漏是指程序在申请内存后,无法释放已申请的内存空间就会造成内存泄漏。一次内存泄漏似乎不会造成很大影响。但内存泄漏累积的效果就是内存溢出。
场景1:是否会造成内存溢出:
内存泄漏和内存溢出:
内存泄漏是指程序在申请内存后,无法释放已申请的内存空间就会造成内存泄漏。一次内存泄漏似乎不会造成很大影响。但内存泄漏累积的效果就是内存溢出。
场景1:是否会造成内存溢出:
场景描述:线程池中只有一个线程。每一次线程启动,均会初始化一个threadLocal
对象。
该场景便是使用ThreadLocal的反面教材,即使用ThreadLocal但未使用remove()方法清除。
public class ThreadLocalDemo {
private static Logger logger= LoggerFactory.getLogger(WeakReferenceDemo.class);
private ThreadLocal threadLocal = new ThreadLocal();
//开启一个线程池
public static void testThreadLocalByPool(){
ExecutorService executorService = Executors.newSingleThreadExecutor();
while (true) {
executorService.execute(() -> {
//每new出一个对象,那么便new出一个ThreadLocal
ThreadLocalDemo demo = new ThreadLocalDemo();
demo.testThreadLocal();
});
}
}
public void testThreadLocal() {
//ThreadLocal是方法级别的
threadLocal.set("aaa");
String s = threadLocal.get();
logger.info("获取ThreadLocal的内容:"+s);
}
//测试ThreadLocal不会发生内存溢出
public static void main(String[] args) {
testThreadLocalByPool();
}
}
结论:该代码最终也不会发生内存溢出。
实际上ThreadLocal仅仅会造成内存泄漏,若是存在大量线程的情况(蚂蚁咬死象),可能会造成内存溢出。
ThreadLocal的源码分析
ThreadLocal含义为线程本地变量。即每个线程中均存在一个ThreadLocal对象。
1.1 ThreadLocalMap的set方法
- 使用ThreadLocal的set方法存入value,实际上是获取当前线程的ThreadLocalMap对象,将threadLocal对象作为一个key存入到map中。
//java.lang.ThreadLocal#set
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
//将threadLocal作为key,和value存入到ThreadLocalMap中
map.set(this, value);
else
createMap(t, value);
}
- 现在的思路就是调用map的set方法。
Map底层就是一个数组,HashMap使用数组+链表的结构,其实是使用哈希桶去解决哈希冲突问题。而ThreadLocal自己实现Map结构,采用线性探测法去解决哈希冲突,所以单纯的使用数组便可以实现Map结构。
map中元素是依靠hashCode
去计算在数组的位置的。但是总会有一些元素它们并不相等,但是他们的hashCode
相同,即在数组中的位置相同。
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
数组中的对象为Entry对象,Entry对象有两个属性,一个是弱引用持有的key,一个是强引用持有的value。
当key只被弱引用持有,并且发送了GC,key就会被回收,所以在Entry[]中会存在一些失效节点。ThreadLocalMap考虑到了这种情况,每次set的过程中,都会清除失效节点。这种补救措施便可以使得用户未显式调用reomve方法时,线程中的失效entry在set操作时也会被清理掉。
- 颜色相同的节点表示HashCode相同;
- 里面的值若是相同,代表两个节点的key完全相同;
private void set(ThreadLocal> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//通过key的HashCode计算在map中的下标位置;
int i = key.threadLocalHashCode & (len-1);
//情况1:下标位置存在元素。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
//若key相等,则去覆盖
if (k == key) {
e.value = value;
return;
}
//若发现key为null,但是entry存在的节点,即已失效的节点
if (k == null) {
//替换失效的节点。
replaceStaleEntry(key, value, i);
return;
}
}
//若位置不存在元素,则生成entry对象,放入到map中。
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void replaceStaleEntry(ThreadLocal> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//往前执行,寻找失效节点的范围
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
//若是在往后寻找的过程中,遇到key相等的节点,则与覆盖该节点并与失效节点交换
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 开始清除失效节点
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//若是找到空节点,这将失效节点置空,并将值覆盖到失效节点上。
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
1.2 ThreadLocal的get方法
我们在线程中使用ThreadLocal.set()方法,是为了在线程运行到某处时调用ThreadLocal.get()方法获取到存储的值。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private Entry getEntry(ThreadLocal> key) {
//通过HashCode计算出map中的位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//该位置的条目不为空,并且key相当则返回
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private 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)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
总结
ThreadLocal
类提供的API方法实际上就是操纵当前线程对象中的ThreadLocalMap
属性。ThreadLocalMap底层数据结构就是一个Entry数组对象。Entry有两个属性,key是弱引用持有的ThreadLocal对象,而value为我们存储的值。
在使用set或get方法时,会进行线性探测寻找对应的Entry对象,若发现失效节点,ThreadLocalMap会清除这些节点。但是也可以这样理解,若今后没有在此调用set或get方法,这些value永远不会被清除的。从而造成了内存泄漏。
当内存泄漏积少成多,最终可能会内存溢出。
2.1 ThreadLocalMap的key为什么设置为弱引用
ThreadLocalMap的key就是ThreadLocal对象,它在创建出来时,会被强引用和弱引用同时持有。当线程执行完任务后(伴随方法出栈),ThreadLoca只会被弱引用持有,一旦发生GC,key就会被置为null。而ThreadLocal的set或get操作,在线性探测定位entry时,遇到key==null的节点,会将其看做为失效节点进行回收。
2.2 ThreadLocal为什么是static修饰
优点:使用了static方法,实际上可以避免ThreadLocal对象重复创建;
缺点:使用了static方法,map的key就会被强引用持有,除非显式调用remove()方法,否则key不会被回收。
但是利大于弊。
在Spring的bean中使用ThreadLocal,因为bean大多数是单例,故ThreadLocal有无static修饰效果相同。
2.3 ThreadLocalMap的value为什么不会被回收
该value对象被强引用所持有。所以不会被回收。
2.4 ThreadLocal为什么会造成内存泄漏
TheadLocal是操作当前线程的ThreadLocalMap属性,该map的底层数据结构是Entry数组,Entry的value会强引用着对象。所以该对象不会被回收,造成内存泄漏。
相关阅读
JAVA并发(1)—java对象布局
JAVA并发(2)—PV机制与monitor(管程)机制
JAVA并发(3)—线程运行时发生GC,会回收ThreadLocal弱引用的key吗?
JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!
JAVA并发(5)— 多线程顺序的打印出A,B,C(线程间的协作)
JAVA并发(6)— AQS源码解析(独占锁-加锁过程)
JAVA并发(7)—AQS源码解析(独占锁-解锁过程)
JAVA并发(8)—AQS公平锁为什么会比非公平锁效率低(源码分析)
JAVA并发(9)— 共享锁的获取与释放
JAVA并发(10)—interrupt唤醒挂起线程
JAVA并发(11)—AQS源码Condition阻塞和唤醒
JAVA并发(12)— Lock实现生产者消费者