场景题 说一个内存溢出的场景和处置打算
前言
在 Java 言语中处置线程不安保的疑问通常有几种手腕:
锁的成功打算是在多线程写入全局变量时,经过排队一个一个来写入全局变量,从而就可以防止线程不安保的疑问了。比如当咱们经常使用线程不安保的 SimpleDateFormat 对期间启动格局化时,假设经常使用锁来处置线程不安保的疑问,成功的流程就是这样的:
从上述图片可以看出,经过加锁的模式虽然可以处置线程不安保的疑问,但同时带来了新的疑问,经常使用锁时线程须要排队口头,因此会带来必定的性能开支。
但是,假设经常使用的是 ThreadLocal 的模式,则是给每个线程创立一个 SimpleDateFormat 对象,这样就可以防止排队口头的疑问了,它的成功流程如下图所示:
但是, 在咱们经常使用 ThreadLocal 的环节中,很容易就会出现内存溢出的疑问 ,如上方的这个事例。
什么是内存溢出?
内存溢出(Memory Overflow),指的是在程序运转环节中,放开的内存资源不再被经常使用,但没有被正确监禁,造成占用的内存不时参与,最终耗尽系统的可用内存。当程序尝试调配更多的内存空间时,由于内存无余,会抛出 OutOfMemoryError 意外,造成程序中断或解体的现象就叫做内存溢出。
内存溢出代码演示
在开局演示 ThreadLocal 内存溢出的疑问之前,咱们先经常使用“-Xmx50m”的参数来设置一下 Idea,它示意将程序运转的最大内存设置为 50m,假设程序的运转超越这个值就会出现内存溢出的疑问,设置方法如下:
设置后的最终成果这样的:
性能完 Idea 之后,接上去咱们来成功一下业务代码。在代码中咱们会创立一个大对象,这个对象中会有一个 10m 大的数组,而后咱们将这个大对象存储在 ThreadLocal 中,再经常使用线程池口头大于 5 次参与义务,由于设置了最大运转内存是 50m,所以现实的状况是口头 5 次参与操作之后,就会出现内存溢出的疑问,实现代码如下:
import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;publicclass ThreadLocalOOMExample {/*** 定义一个 10m 大的类*/staticclass MyTask {// 创立一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)privatebyte[] bytes = newbyte[10 * 1024 * 1024];}// 定义 ThreadLocalprivatestatic ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();// 主测试代码public static void main(String[] args) throws InterruptedException {// 创立线程池ThreadPoolExecutor threadPoolExecutor =new ThreadPoolExecutor(5, 5, 60,TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));// 口头 10 次调用for (int i = 0; i < 10; i++) {// 口头义务executeTask(threadPoolExecutor);Thread.sleep(1000);}}/*** 线程池口头义务* @param threadPoolExecutor 线程池*/private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {// 口头义务threadPoolExecutor.execute(new Runnable() {@Overridepublic void run() {System.out.println("创立对象");// 创立对象(10M)MyTask myTask = new MyTask();// 存储 ThreadLocaltaskThreadLocal.set(myTask);// 将对象设置为 null,示意此对象不在经常使用了myTask = null;}});}}
以上程序的口头结果如下:
从上述图片可看出,当程序口头到第 5 次参与对象时就出现内存溢出的疑问了,这是由于设置了最大的运转内存是 50m,每次循环会占用 10m 的内存,加上程序启动会占用必定的内存,因此在口头到第 5 次参与义务时,就会出现内存溢出的疑问。
要素剖析
内存溢出的疑问和处置打算比拟便捷,重点在于“要素剖析”,咱们要经过内存溢出的疑问搞分明,为什么 ThreadLocal 会这样?是什么要素造成了内存溢出?
要搞分明这个疑问(内存溢出的疑问),咱们须要从 ThreadLocal 源码入手,所以咱们首先关上 set 方法的源码(在示例中经常使用到了 set 方法),如下所示:
public void set(T value) {// 失掉线程Thread t = Thread.currentThread();// 依据线程失掉到 ThreadMap 变量ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value); // 将内容存储到 map 中elsecreateMap(t, value); // 创立 map 并将值存储到 map 中}
从上述代码咱们可以看出 Thread、ThreadLocalMap 和 set 方法之间的相关:每个线程 Thread 都领有一个数据存储容器 ThreadLocalMap,当口头 ThreadLocal.set 方法口头时,会将要存储的值放到 ThreadLocalMap 容器中,所以接上去咱们再看一下 ThreadLocalMap 的源码:
staticclass ThreadLocalMap {// 实践存储数据的数组private Entry[] table;// 存数据的方法private void set(ThreadLocal<?> key, Object value) {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();// 假设有对应的 key 间接降级 value 值if (k == key) {e.value = value;return;}// 发现空位拔出 valueif (k == null) {replaceStaleEntry(key, value, i);return;}}// 新建一个 Entry 拔出数组中tab[i] = new Entry(key, value);int sz = ++size;// 判别能否须要启动扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}// ... 疏忽其余源码}
从上述源码咱们可以看出:ThreadMap 中有一个 Entry[] 数组用来存储一切的数据,而 Entry 是一个蕴含 key 和 value 的键值对,其中 key 为 ThreadLocal 自身,而 value 则是要存储在 ThreadLocal 中的值。
依据上方的内容,咱们可以得出 ThreadLocal 相关对象的相关图,如下所示:
也就是说它们之间的援用相关是这样的:Thread -> ThreadLocalMap -> Entry -> Key,Value,因此当咱们经常使用线程池来存储对象时,由于线程池有很长的生命周期,所以线程池会不时持有 value 值,那么渣滓回收器就不可回收 value,所以就会造成内存不时被占用,从而造成内存溢出疑问的出现。
处置打算
ThreadLocal 内存溢出的处置打算很便捷,咱们只有要在经常使用完 ThreadLocal 之后,口头 remove 方法就可以防止内存溢出疑问的出现了,比如以下代码:
import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;publicclass App {/*** 定义一个 10m 大的类*/staticclass MyTask {// 创立一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)privatebyte[] bytes = newbyte[10 * 1024 * 1024];}// 定义 ThreadLocalprivatestatic ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();// 测试代码public static void main(String[] args) throws InterruptedException {// 创立线程池ThreadPoolExecutor threadPoolExecutor =new ThreadPoolExecutor(5, 5, 60,TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));// 口头 n 次调用for (int i = 0; i < 10; i++) {// 口头义务executeTask(threadPoolExecutor);Thread.sleep(1000);}}/*** 线程池口头义务* @param threadPoolExecutor 线程池*/private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {// 口头义务threadPoolExecutor.execute(new Runnable() {@Overridepublic void run() {System.out.println("创立对象");try {// 创立对象(10M)MyTask myTask = new MyTask();// 存储 ThreadLocaltaskThreadLocal.set(myTask);// 其余业务代码...} finally {// 监禁内存taskThreadLocal.remove();}}});}}
以上程序的口头结果如下:
从上述结果可以看出咱们只有要在 finally 中口头 ThreadLocal 的 remove 方法之后就不会在出现内存溢出的疑问了。
remove的秘密
那 remove 方法为什么会有这么大的魔力呢?咱们关上 remove 的源码看一下:
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}
从上述源码中咱们可以看出,当调用了 remove 方法之后,会间接将 Thread 中的 ThreadLocalMap 对象移除掉,这样 Thread 就不再持有 ThreadLocalMap 对象了,所以即使 Thread 不时存活,也不会形成由于(ThreadLocalMap)内存占用而造成的内存溢出疑问了。
小结
本文咱们经常使用代码的模式演示了 ThreadLocal 内存溢出的疑问,严厉来讲内存溢出并不是 ThreadLocal 的疑问,而是由于没有正确经常使用 ThreadLocal 所带来的疑问。想要防止 ThreadLocal 内存溢出的疑问,只有要在经常使用完 ThreadLocal 后调用 remove 方法即可。