内存泄露原因找到了,罪魁祸首是Java TheadLocal
文章插图
ThreadLocal使用不规范 , 师傅两行泪组内来了一个实习生 , 看这小伙子春光满面、精神抖擞、头发微少 , 我心头一喜:绝对是个潜力股 。 于是我找经理申请亲自来带他 , 为了帮助小伙子快速成长 , 我给他分了一个需求 , 这不需求刚上线几天就出网上问题了后台监控服务发现内存一直在缓慢上升 , 初步怀疑是内存泄露 。
把实习生的PR都找出来仔细review , 果然发现问题了 。 由于公司内部代码是保密的 , 这里简单写一个demo还原场景(忽略代码风格问题) 。
public class ThreadPoolDemo {private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 100; ++i) {poolExecutor.execute(new Runnable() {@Overridepublic void run() {ThreadLocal threadLocal = new ThreadLocal<>();threadLocal.set(new BigObject());// 其他业务代码}});Thread.sleep(1000);}}static class BigObject {// 100Mprivate byte[] bytes = new byte[100 * 1024 * 1024];}}复制代码代码分析:
- 创建一个核心线程数和最大线程数都为10的线程池 , 保证线程池里一直会有10个线程在运行 。
- 使用for循环向线程池中提交了100个任务 。
- 定义了一个ThreadLocal类型的变量 , Value类型是大对象 。
- 每个任务会向threadLocal变量里塞一个大对象 , 然后执行其他业务逻辑 。
- 由于没有调用线程池的shutdown方法 , 线程池里的线程还是会在运行 。
代码中给threadLocal赋值了一个大的对象 , 但是执行完业务逻辑后没有调用remove方法 , 最后导致线程池中10个线程的threadLocals变量中包含的大对象没有被释放掉 , 出现了内存泄露 。
大家说说这样的实习生还能留不?
ThreadLocal的value值存在哪里?实习生说他以为线程任务结束了threadLocal赋值的对象会被JVM垃圾回收 , 很疑惑为什么会出现内存泄露 。 作为师傅我肯定要给他把原理讲透呀 。
ThreadLocal类提供set/get方法存储和获取value值 , 但实际上ThreadLocal类并不存储value值 , 真正存储是靠ThreadLocalMap这个类 , ThreadLocalMap是ThreadLocal的一个静态内部类 , 它的key是ThreadLocal实例对象 , value是任意Object对象 。
ThreadLocalMap类的定义
static class ThreadLocalMap {// 定义一个table数组 , 存储多个threadLocal对象及其value值private Entry[] table;ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCodetable[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}// 定义一个Entry类 , key是一个弱引用的ThreadLocal对象// value是任意对象static class Entry extends WeakReference> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal> k, Object v) {super(k);value = http://kandian.youth.cn/index/v;}}// 省略其他}复制代码进一步分析ThreadLocal类的代码 , 看set和get方法如何与ThreadLocalMap静态内部类关联上 。ThreadLocal类set方法
public class ThreadLocal { public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}// 省略其他方法}复制代码set的逻辑比较简单 , 就是获取当前线程的ThreadLocalMap , 然后往map里添加KV , K是当前ThreadLocal实例 , V是我们传入的value 。这里需要注意一下 , map的获取是需要从Thread类对象里面取 , 看一下Thread类的定义 。public class Thread implements Runnable {ThreadLocal.ThreadLocalMap threadLocals = null;//省略其他}复制代码Thread类维护了一个ThreadLocalMap的变量引用 。ThreadLocal类get方法get获取当前线程的对应的私有变量 , 是之前set或者通过initialValue的值 , 代码如下:
class ThreadLocal {public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null)return (T)e.value;}return setInitialValue();}}复制代码代码逻辑分析:- 获取当前线程的ThreadLocalMap实例;
- 如果不为空 , 以当前ThreadLocal实例为key获取value;
- 如果ThreadLocalMap为空或者根据当前ThreadLocal实例获取的value为空 , 则执行setInitialValue();
- 控制|正弦电气科创板IPO过会,需说明与前员工设立或控制的经销商交易的原因及合理性
- 换头像|从不换“头像”的人,多半都是这几张原因,你是哪一种?
- 试试|手机内存不够用,咋办?试试关闭微信这两步操作,轻松腾出几个G
- 内容|浅谈内容行业的一些规律和壁垒,聊聊电商平台孵化小红书难点(外部原因)
- 手机|手机发热怎么办?手机发烫的解决办法及原因
- 操作|动用军队“挖”比特币!委内瑞拉秀出神操作,背后原因令人心酸
- 最贵|苹果坚持自研原因曝光,iPhone12最贵部件只能靠购买,比A14贵1倍
- 分析师|真香定律或再被验证,iPhone12将大卖,分析师给出两个原因
- 值得|千元价位,大容量电池,大内存,双模5G,你值得拥有!
- 高通|为什么iphoneXR和iphoneXS信号不稳定?原因正式被确认,望周知!
