公司新来的小可爱,竟然把内存搞崩了!

ThreadLocal使用不规范,师傅两行泪

组内来了一个实习生,看这小伙子春光满面、精神抖擞、头发微少,我心头一喜:绝对是个潜力股。于是我找经理申请亲自来带他,为了帮助小伙子快速成长,我给他分了一个需求,这不需求刚上线几天就出网上问题了????后台监控服务发现内存一直在缓慢上升,初步怀疑是内存泄露。

把实习生的PR都找出来仔细review,果然发现问题了。由于公司内部代码是保密的,这里简单写一个demo还原场景(忽略代码风格问题)。

public class ThreadPoolDemo {private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 10, 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<BigObject> 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方法,线程池里的线程还是会在运行。

乍一看这代码好像没有什么问题,那为什么会导致服务GC后内存还高居不下呢?

代码中给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.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}// 定义一个Entry类,key是一个弱引用的ThreadLocal对象// 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和get方法如何与ThreadLocalMap静态内部类关联上。

ThreadLocal类set方法

public class ThreadLocal<T> {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<T> {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();

ThreadLocal相关类的关系总结

看了上面的分析是不是对Thread,ThreadLocal,ThreadLocalMap,Entry这几个类之间的关系有点晕了,没关系我专门画了一个UML类图来总结(忽略UML标准语法)。

ThreadLocal相关类的关系
  • 每个线程是一个Thread实例,其内部维护一个threadLocals的实例成员,其类型是ThreadLocal.ThreadLocalMap。

  • 通过实例化ThreadLocal实例,我们可以对当前运行的线程设置一些线程私有的变量,通过调用ThreadLocal的set和get方法存取。

  • ThreadLocal本身并不是一个容器,我们存取的value实际上存储在ThreadLocalMap中,ThreadLocal只是作为TheadLocalMap的key。

  • 每个线程实例都对应一个TheadLocalMap实例,我们可以在同一个线程里实例化很多个ThreadLocal来存储很多种类型的值,这些ThreadLocal实例分别作为key,对应各自的value,最终存储在Entry table数组中。

  • 当调用ThreadLocal的set/get进行赋值/取值操作时,首先获取当前线程的ThreadLocalMap实例,然后就像操作一个普通的map一样,进行put和get。

ThreadLocal内存模型原理

经过上面的分析我们对ThreadLocal相关的类设计已经非常清楚了,下面通过一张图更加深入理解一下ThreadLocal的内存存储。

ThreadLocal内存模型

图中左边是栈,右边是堆。线程的一些局部变量和引用使用的内存属于Stack(栈)区,而普通的对象是存储在Heap(堆)区。

  • 线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef。

  • 当ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化。

  • Map实例化之后,也就拿到了该ThreadLocalMap的句柄,那么就可以将当前ThreadLocal对象作为key,进行存取操作。

  • 图中的虚线,表示key对应ThreadLocal实例的引用是个弱引用。

强引用弱引用的概念

ThreadLocalMap的key是一个弱引用类型,源代码如下:

static class ThreadLocalMap {// 定义一个Entry类,key是一个弱引用的ThreadLocal对象// value是任意对象static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 省略其他
}

下面解释一下常见的几种引用概念。

强引用

一直活着:类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。

弱引用

回收就会死亡:被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

软引用

有一次活的机会:软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

虚引用

也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

内存泄露是不是弱引用的锅?

从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得思考:为什么ThreadLocalMap使用弱引用而不是强引用?

翻看官网文档的说法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. 

为了处理非常大和长期的用途,哈希表条目使用weakreference作为键。

分两种情况讨论:

(1)key 使用强引用

引用ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

(2)key 使用弱引

引用ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal最佳实践

通过前面几小节我们分析了ThreadLocal的类设计以及内存模型,同时也重点分析了发生内存泄露的条件和特定场景。最后结合项目中的经验给出建议使用ThreadLocal的场景:

  • 当需要存储线程私有变量的时候。

  • 当需要实现线程安全的变量时。

  • 当需要减少线程资源竞争的时候。

综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

答案就是:每次使用完ThreadLocal,建议调用它的remove()方法,清除数据。

另外需要强调的是并不是所有使用ThreadLocal的地方,都要在最后remove(),因为他们的生命周期可能是需要和项目的生存周期一样长的,所以要进行恰当的选择,以免出现业务逻辑错误!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544860.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

理解Node.js的event loop

为什么80%的码农都做不了架构师&#xff1f;>>> 关于Node.js的第一个基本概念是I/O操作开销是巨大的&#xff1a; 所以&#xff0c;当前变成技术中最大的浪费来自于等待I/O操作的完成。有几种方法可以解决性能的影响&#xff1a; 同步方式&#xff1a;按次序一个…

硬核|定时任务的10种实现方案,满足你的不同需求!

最近有几个读者私信给我&#xff0c;问我他们的业务场景&#xff0c;要用什么样的定时任务。确实&#xff0c;在不用的业务场景下要用不同的定时任务&#xff0c;其实我们的选择还是挺多的。我今天给大家总结10种非常实用的定时任务&#xff0c;总有一种是适合你的。一. linux自…

Semaphore自白:限流器用我就对了!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;大家好&#xff0c;我是 Semaphore&#xff0c;我的中文名字叫“信号量”&#xff0c;我来自 JUC 家族&#xff08;java.uti…

Android Activity和Intent机制学习笔记

文章转自&#xff1a;http://www.cnblogs.com/feisky/archive/2010/01/16/1649081.html Activity Android中&#xff0c;Activity是所有程序的根本&#xff0c;所有程序的流程都运行在Activity之中&#xff0c;Activity具有自己的生命周期&#xff08;见http://www.cnblogs.com…

线程的故事:我的3位母亲成就了优秀的我!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;声明&#xff1a;本故事纯属虚构&#xff0c;如果雷同那就是真事了&#xff01;大家好&#xff0c;我是线程&#xff0c;我的…

7种内存泄露场景和13种解决方案!

前言Java通过垃圾回收机制&#xff0c;可以自动的管理内存&#xff0c;这对开发人员来说是多么美好的事啊。但垃圾回收器并不是万能的&#xff0c;它能够处理大部分场景下的内存清理、内存泄露以及内存优化。但它也并不是万能的。不然&#xff0c;我们在实践的过程中也不会出现…

Android Sqlite

2019独角兽企业重金招聘Python工程师标准>>> 一、http://lansuiyun.iteye.com/blog/1246430 good! 二、增加新表时&#xff0c;需要更新版本号。 三、Android 使用自定义cursorAdapter&#xff1a; http://blog.csdn.net/buaalei/article/details/6064792 good! 四…

小黑小波比.git clone报错解决方案

2019独角兽企业重金招聘Python工程师标准>>> zmzpzmzp1:~/data$ git clone git192.168.199.199:zmw/s910.git 正克隆到 s910... ssh: connect to host 192.168.199.199 port 22: Connection refused fatal: Could not read from remote repository.Please make sure…

一个sql注入直接把我们服务搞挂了

前言最近我在整理安全漏洞相关问题&#xff0c;准备在公司做一次分享。恰好&#xff0c;这段时间团队发现了一个sql注入漏洞&#xff1a;在一个公共的分页功能中&#xff0c;排序字段作为入参&#xff0c;前端页面可以自定义。在分页sql的mybatis mapper.xml中&#xff0c;orde…

reinterpret_cast和static_cast的总结

主要参考&#xff1a;http://blog.csdn.net/querw/article/details/7387594 http://www.cnblogs.com/jerry19880126/archive/2012/08/14/2638192.html http://www.cnblogs.com/ider/archive/2011/07/30/cpp_cast_operator_part3.htmlhttp://bbs.csdn.net/topics/390249118 关键…

Java双刃剑之Unsafe类详解

前一段时间在研究juc源码的时候&#xff0c;发现在很多工具类中都调用了一个Unsafe类中的方法&#xff0c;出于好奇就想要研究一下这个类到底有什么作用&#xff0c;于是先查阅了一些资料&#xff0c;一查不要紧&#xff0c;很多资料中对Unsafe的态度都是这样的画风&#xff1a…

Java知多少(66)输入输出(IO)和流的概述

输入输出&#xff08;I/O&#xff09;是指程序与外部设备或其他计算机进行交互的操作。几乎所有的程序都具有输入与输出操作&#xff0c;如从键盘上读取数据&#xff0c;从本地或网络上的文件读取数据或写入数据等。通过输入和输出操作可以从外界接收信息&#xff0c;或者是把信…

额!Java中用户线程和守护线程区别这么大?

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在 Java 语言中线程分为两类&#xff1a;用户线程和守护线程&#xff0c;而二者之间的区别却鲜有人知&#xff0c;所以本文磊…

EasyUI-右键菜单变灰不可用效果

使用过EasyUI的朋友想必都知道疯狂秀才写的后台界面吧&#xff0c;作为一个初学者我不敢妄自评论它的好坏&#xff0c;不过它确实给我们提供了一个很好框架&#xff0c;只要在它的基础上进行修改&#xff0c;基本上都可以满足我们开发的需要。 知道“疯狂秀才”写的后台界面已经…

ThreadLocal中的3个大坑,内存泄露都是小儿科!

我在参加Code Review的时候不止一次听到有同学说&#xff1a;我写的这个上下文工具没问题&#xff0c;在线上跑了好久了。其实这种想法是有问题的&#xff0c;ThreadLocal写错难&#xff0c;但是用错就很容易&#xff0c;本文将会详细总结ThreadLocal容易用错的三个坑&#xff…

基于.Net的单点登录(SSO)解决方案

为什么80%的码农都做不了架构师&#xff1f;>>> 前些天一位朋友要我帮忙做一单点登录&#xff0c;其实这个概念早已耳熟能详&#xff0c;但实际应用很少&#xff0c;难得最近轻闲&#xff0c;于是决定通过本文来详细描述一个SSO解决方案&#xff0c;希望对 大家有所…

在android中ScrollView嵌套ScrollView解决方案

文章转载自&#xff1a;http://www.jb51.net/article/33054.htm大家好&#xff0c;众所周知&#xff0c;android里两个相同方向的ScrollView是不能嵌套的&#xff0c;那要是有这样的需求怎么办,接下来为您介绍解决方法&#xff0c;感兴趣的朋友可以了解下大家好&#xff0c;众所…

Cucumber 入门一

&#xff08;转自&#xff1a;http://www.cnblogs.com/jarodzz/archive/2012/07/02/2573014.html&#xff09; 第一次看到Cucumber和BDD&#xff08;Behavior Driven Development, 行为驱动开发&#xff09;&#xff0c;是在四年前。那时才開始工作&#xff0c;对软件測试工具相…

Java中这7个方法,一不小心就用错了!

最近我们通过sonar静态代码检测&#xff0c;同时配合人工代码review&#xff0c;发现了项目中很多代码问题。除了常规的bug和安全漏洞之外&#xff0c;还有几处方法用法错误&#xff0c;引起了我极大的兴趣。我为什么会对这几个方法这么感兴趣呢&#xff1f;因为它们极具迷惑性…

这样设置,让你的 IDEA 好看到爆炸

今天这期我们来分享几个美化 IDEA 设置技巧&#xff0c;让你的 IDEA 与众不同。首先我们来看下 IDEA 默认设置&#xff0c;虽然不丑&#xff0c;但就是太单调&#xff0c;千篇一律。默认主题接着&#xff0c;我们来看下美化以后的界面&#xff0c;总体看起来是不是比默认好看了…