概念
首先解释下内存溢出和内存泄露的概念。内存溢出个别指的是 out of memory,也就是咱们常常说的 OOM,常产生在堆,办法区和办法栈。内存泄露指的是一段程序在申请内存空间后,无奈开释曾经申请的内存空间,导致该内存地址不可达,后续程序里这块内存空间永远被占用。就如同商场的物品柜设计了 10 个抽屉,每个人应用后都会归还给下一个用户应用,如果有某个人始终占用不退还,别的用户就只能应用剩下的 9 个抽屉,这样的人多了当前,最初大家一个抽屉也无奈应用了。所以内存泄露跟内存溢出是存在分割的,一次内存泄露不会有太大的影响,内存泄露沉积当前就会导致内存溢出。
此处应该有图,大家脑补一下画面。
需要背景
一个 to C 的个人账户零碎,数据量大略有 2000 多 W;某天产品忽然来说要做一个 to B 商户账户零碎,马上一个星期左右后的大促流动就要用,产品跟大老板牛逼曾经吹出去了,说咱们的零碎曾经具备了这个能力,可能立马无缝反对。当初火急火燎的来找咱们,问是不是间接把咱们这套 to C 的账户零碎的能力提供进来就能够了。
需要剖析
首先在这么短的工夫内要开发测试上线,新做一个 to B 的账户零碎必定是不事实的,咱们程序员能力再强也不能流水线生产零碎啊。只能依赖以后账户零碎的能力先去撑持这个业务。这两套账户体系底层的外围畛域模型能够形象统一,都包含记账凭证,记账主体,记账流水。不同的中央在于不同的业务应用场景下进出帐的规定不一样,所以规定层的畛域模型须要定制化的配置开发。同时目前零碎曾经有 2000 多 W 账户,日减少流水也是 10W 级别的,自身数据量曾经很大,而且业务上两套数据会相互影响,须要在业务上做垂直分表,将两套数据隔离。
施行过程
畛域模型和数据存储计划定下来后,马上就开始施行了,过后为了更优雅的实现,对目前 to C 的业务影响最小,咱们特意在 interface 入口处对立拦挡后在当前工作线程的 threadlocal 加上一个判断标记,用来辨认是 to C 的业务申请还是 to B 的业务申请,而后业务规定层通过不同的 filter 进行过滤,最初在数据库 jdbc 层通过不同的标记对不同的表做相应的 DML 解决。计划施行的很快,两三天就开发完提测,可在测试过程中发现了两个问题,第一个问题是数据有时候会平白无故的错乱,原本是写到 to B 表构造的数据会写到了 to C 表构造里。而且压测时发现内存监控曲线在缓缓的增长,产生了内存泄露。
问题剖析
仔细分析源代码后发现问题呈现在 threadlocal。每个线程中都有一个 ThreadLocalMap 数据结构,当执行 set 办法时,其值是保留在以后线程的 threadLocals 变量中,当执行 get 办法中,是从以后线程的 threadLocals 变量获取。所以在线程 1 中 set 的值,对线程 2 来说是摸不到的,而且在线程 2 中从新 set 的话,也不会影响到线程 1 中的值,保障了线程之间不会互相烦扰
public void set(T value) {Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
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();}
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}
//Entry 为 ThreadLocalMap 动态外部类,对 ThreadLocal 的若援用
// 同时让 ThreadLocal 和储值造成 key-value 的关系
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {super(k);
value = v;
}
}
内存泄露剖析
通过代码能够晓得,当一个线程调用 ThreadLocal 的 set 办法设置变量时候,以后线程的 ThreadLocalMap 外面就会寄存一个 Entry 对象,这个记录的 key 为 ThreadLocal 的援用,value 则为设置的值。如果以后线程始终存在而没有调用 ThreadLocal 的 remove 办法,并且这时候其它中央还是有对 ThreadLocal 的援用,则以后线程的 ThreadLocalMap 变量外面会存在 ThreadLocal 变量的援用和 value 对象的援用是不会被开释的,这就会造成内存泄露的。然而思考如果这个 ThreadLocal 变量没有了其余强依赖,而以后线程还存在的状况下,因为线程的 ThreadLocalMap 外面的 key 是弱依赖,则以后线程的 ThreadLocalMap 外面的 ThreadLocal 变量的弱援用会被在 gc 的时候回收,然而对应 value 还是会造成内存泄露。
数据写错剖析
java 过程是通过内部 web 容器如 tomcat 启动的,web 容器启动时会启动一个线程池,创立一些外围初始线程,这些线程解决完一个工作后会持续解决另外一个工作。这个时候如果外围线程刚解决完一个 to B 的工作,解决完后线程没有隐没持续解决一个 to C 的工作,这个时候 threadLocal 里的变量标记还是间接的标记,就会导致在 jdbc 层持续被路由到 to B 的表构造里,导致数据谬误。
修复解决
理解分明 threadLoca 的存储构造后,在 interface 入口处做一个 AOP 切片,通过盘绕告诉,在办法正式解决前在当前工作线程的 threadlocal set 一个判断标记,办法解决实现后手动 remve 掉这个标记,保障每次解决后 内存正确被开释。
感激关注
能够关注微信公众号「 回滚吧代码 」,第一工夫浏览,文章继续更新;专一 Java 源码、架构、算法和面试。