失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > 论Java多线程如何引发OOM—多线程开发知识点

论Java多线程如何引发OOM—多线程开发知识点

时间:2022-08-31 14:51:41

相关推荐

论Java多线程如何引发OOM—多线程开发知识点

Java —ThreadLocal 如何引发 OOM

Java 内存泄漏ThreadLocal_OOM回顾ThreadLocal强引用软引用弱引用虚引用

Java 内存泄漏

内存溢出(Out Of Memory):是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件,而由系统配置、数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免。

另一个解释:指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储 int 类型数据的存储空间,但是你却存储 long 类型的数据,那么结果就是内存不够用,此时就会报错 OOM ,即所谓的内存溢出。

ThreadLocal_OOM

测试配置堆内存大小:

代码块:

//类说明:测试ThreadLocal造成的内存泄漏public class HYQ{private static final int A = 500;//创建线程池,固定为5个final atatic ThreadPoolExecutor tpe = new ThreadPoolExcutor(5,,5,1,TimeUnit.MINUTES,new LinkedBlockingQueue<Runnable>());//创建一个用来创建数组的类static class fiveByte{//开一个大小为5m的数组private byte[] a = new byte[1024*1024*5];}//创建 ThreadLocal对象final static ThreadLocal<fiveByte> threadLocalForFB = new ThreadLocal<>();public static class testThread implements Runnable{@Overridepublic void run(){//创建一个数组实例,大小约为25兆fiveByte lV1 = new fiveByte();System.out.println("I just miss u Alizary");}}public static void main(String[] args) throws InterruptesException{Object o = new Object();for(int i = 0; i < A; ++i){testThread thread = new testThread();tpe.execute(thread);}Thread.sleep(100);}//System.out.println("???");}

跑起来看看内存情况:

内存变化大小情况:平均稳定在25兆

再看看加入ThreadLocal对象的情况:

public static class testThread implements Runnable{@Overridepublic void run(){//创建一个数组实例,大小约为25兆fiveByte lV1 = new fiveByte();//加入ThreadLocalthreadLocalForFB.set(lV1);//fiveByte lV2 = new fiveByte();System.out.println("I just miss u Alizary");}}

再来看内存情况:

可以看到启动 ThreadLocal 后内存占用上了200

这时候当我们手动释放内存:

public static class testThread implements Runnable{@Overridepublic void run(){//创建一个数组实例,大小约为25兆fiveByte lV1 = new fiveByte();//加入ThreadLocalthreadLocalForFB.set(lV1);//fiveByte lV2 = new fiveByte();threadLocalForFB.remove();System.out.println("I just miss u Alizary");}}

很明显内存情况回到了只开5个数组的大小,可以说明启动ThreadLocal肯定发生了OOM

回顾ThreadLocal

根据上一篇文章写的,每次声明创建 Thread 就声明一个ThreadLocalMap,而ThreadLocalMap里面的每个Enty[ ] 里的 key 是 ThreadLocal 实例本身,而 value 是真正需要存储的 Object对象,也就证明ThreadLocal 就只是一个对象实例 ,它只是作为一个 key 来让线程从 ThreadLocalMap 的 Enty[ ] 中拿到相对应的 value。仔细观察 ThreadLocalMap。

就可以根据ThreadLocal对象和对应线程对象的栈堆情况来分析,画出他们的引用情况。

从源码得知,只有 map 是使用ThreadLocal 的弱引用作为 Key的(WeakReference):

static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}

补充一下Java 四种引用类型:

强引用

强引用是最普遍的引用,如果一个对象具有强引用,垃圾回收器不会回收该对象,当内存空间不足时,JVM 宁愿抛出 OutOfMemoryError异常;只有当这个对象没有被引用时,才有可能会被回收。

package cn.HYQ;import java.util.ArrayList;import java.util.List;public class StrongReferenceTest {static class BigObject {private Byte[] bytes = new Byte[1024 * 1024];}public static void main(String[] args) {List<BigObject> list = new ArrayList<>();while (true) {BigObject obj = new BigObject();list.add(obj);}}}

BigObject obj = new BigObject()创建的这个对象时就是强引用,上面的main方法最终将抛出OOM异常:

软引用

软引用是用来描述一些有用但并不是必需的对象,适合用来实现缓存(比如浏览器的‘后退’按钮使用的缓存),内存空间充足的时候将数据缓存在内存中,如果空间不足了就将其回收掉。

如果一个对象只具有软引用,则

当内存空间足够,垃圾回收器就不会回收它。

当内存空间不足了,就会回收该对象。JVM会优先回收长时间闲置不用的软引用的对象,对那些刚刚构建的或刚刚使用过的“新”软引用对象会尽可能保留。如果回收完还没有足够的内存,才会抛出内存溢出异常。只要垃圾回收器没有回收它,该对象就可以被程序使用。

package cn.HYQ;import java.lang.ref.SoftReference;public class SoftReferenceTest {static class Person {private String name;private Byte[] bytes = new Byte[1024 * 1024];public Person(String name) {this.name = name;}}public static void main(String[] args) throws InterruptedException {Person person = new Person("张三");SoftReference<Person> softReference = new SoftReference<>(person);person = null; //去掉强引用,new Person("张三")的这个对象就只有软引用了System.gc();Thread.sleep(1000);System.err.println("软引用的对象 ------->" + softReference.get());}}

弱引用

被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时, 无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在 JDK 1.2 之 后,提供了 WeakReference 类来实现弱引用。

public static void main(String[] args) throws InterruptedException {Person person = new Person("张三");ReferenceQueue<Person> queue = new ReferenceQueue<>();WeakReference<Person> weakReference = new WeakReference<Person>(person, queue);person = null;//去掉强引用,new Person("张三")的这个对象就只有软引用了System.gc();Thread.sleep(1000);System.err.println("弱引用的对象 ------->" + weakReference.get());Reference weakPollRef = queue.poll(); //poll()方法是有延迟的if (weakPollRef != null) {System.err.println("WeakReference对象中保存的弱引用对象已经被GC,下一步需要清理该Reference对象");//清理softReference} else {System.err.println("WeakReference对象中保存的软引用对象还没有被GC,或者被GC了但是获得对列中的引用对象出现延迟");}}

虚引用

虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么就和没有任何引用一样,在任何时候都可能被垃圾回收。

Object object = new Object();ReferenceQueue queue = new ReferenceQueue ();PhantomReference pr = new PhantomReference (object, queue);

因此使用了 ThreadLocal 后,引用链如图所示:

虚线表示弱引用

当线程把threadlocal变量指向null后,没有任何强引用指向threadlocal实例,所以threadlocal将会被GC回收。ThreadLocalMap中就出现keynullEntry,这些keynullvalue自然也不能再访问,如果这些线程继续跑得话,这些keynullEntryvalue就会一直存在一条强引用链:

Thread对象 —> Thread —> ThreaLocalMap —> Entry —> value

而这些value永远不会被访问到了,这就会OOM

只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current ThreadMap value将全部被GC回收。最好的做法是不在需要使用ThreadLocal变量后,都调用它的remove()方法,清除数据。

回到代码块中,虽然线程池里面的任务执行完毕了,但是线程池里面的 5 个线程会一直存在直到JVM退出,set了线程的localVariable变量后没有调用localVariable.remove()方法,导致线程池里面的 5 个线程的threadLocals变量里面的new LocalVariable()实例没有被释放。

ThreadLocal的实现,无论是get()set()在某些时候,调用了expungeStaleEntry方法用来清除EntryKeynullValue

get( ):

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)------有机会被调用到用来清除 key 为 null 的 Value 值-----expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;}

set( ):

if (k == key) {e.value = value;tab[i] = tab[staleSlot];tab[staleSlot] = e;// Start expunge at preceding stale entry if it existsif (slotToExpunge == staleSlot)slotToExpunge = i;cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}

但是这是不及时的,意思就是不是每次都执行清除语句,所以一些情况下还是会发生内存泄露。只有remove()方法中显式调用了expungeStaleEntry方法。

总结:从表面上看内存泄漏的根源在于使用了弱引用,但是为什么使用弱引用而不是强引用:

key使用强引用:

引用ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal的对象实例不会被回收,导致Entry内存泄漏。

key使用弱引用:

引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal的对象实例也会被回收。value在下一次ThreadLocalMap调用setgetremove都有机会被回收。

比较两种情况,由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保护机制。

因此,ThreadLocal内存泄漏的原因是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

SoSoSoSo,ThreadLocal 变量用完了请记得 remove ,谢谢

不写了,最后:

参考文章:Java中的四种引用类型:强引用、软引用、弱引用和虚引用

如果觉得《论Java多线程如何引发OOM—多线程开发知识点》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。