概念

首先解释下内存溢出和内存泄露的概念。内存溢出个别指的是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源码、架构、算法和面试。