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

Java —ThreadLocal 如何引发 OOM

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

Java 内存泄漏

内存溢出(Out Of Memory):是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件,而由系统配置、数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免。
另一个解释:指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储 int 类型数据的存储空间,但是你却存储 long 类型的数据,那么结果就是内存不够用,此时就会报错 OOM ,即所谓的内存溢出。

ThreadLocal_OOM

测试配置堆内存大小:
论Java多线程如何引发OOM—多线程开发知识点_第1张图片
代码块:

//类说明:测试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{
     
		@Override
		public 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("???");
}

跑起来看看内存情况:
论Java多线程如何引发OOM—多线程开发知识点_第2张图片

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

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

	public static class testThread implements Runnable{
     
		@Override
		public void run(){
     
			//创建一个数组实例,大小约为25兆
			fiveByte lV1 = new fiveByte();

			//加入ThreadLocal
			threadLocalForFB.set(lV1);
    		//fiveByte lV2 = new fiveByte();
			System.out.println("I just miss u Alizary");
		}
	}

再来看内存情况:
论Java多线程如何引发OOM—多线程开发知识点_第3张图片

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

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

	public static class testThread implements Runnable{
     
		@Override
		public void run(){
     
			//创建一个数组实例,大小约为25兆
			fiveByte lV1 = new fiveByte();

			//加入ThreadLocal
			threadLocalForFB.set(lV1);
    		//fiveByte lV2 = new fiveByte();
    		threadLocalForFB.remove();
			System.out.println("I just miss u Alizary");
		}
	}

论Java多线程如何引发OOM—多线程开发知识点_第4张图片
很明显内存情况回到了只开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 后,引用链如图所示:
论Java多线程如何引发OOM—多线程开发知识点_第5张图片

虚线表示弱引用

当线程把 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);
                else
                    i = 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 exists
                    if (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多线程如何引发OOM—多线程开发知识点_第6张图片

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

你可能感兴趣的:(Java进阶,java,jvm,内存泄漏,多线程,垃圾回收)