文章目录

  • 1 问题
  • 2 内存泄露
  • 3 弱引用
  • 4 问题分析
    • 4.1 key为强引用
    • 4.2 key为弱引用
    • 4.3 内存泄漏的真正原因
    • 4.4 为什么Entry 的key使用弱引用
  • 5 hash冲突的解决
    • 5.1 hash计算
    • 5.2 set()方法中的hash冲突解决
    • 5.3 remove()中的hash冲突

1 问题

如果我们在使用ThreadLocal的过程中发现有内存泄漏的情况,是不是这个内存泄漏跟Entry中使用弱引用的key有关系?下面我们先来复习下内存泄漏和弱引用相关的知识,在分析。

2 内存泄露

  • Memory Overflow:内存溢出,内存不足(不够用了)。内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件,而由系统配置、数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免
  • Memory Leak:内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

3 弱引用

Java中的引用类型有4种类型:强、软、弱、虚。当前问题主要涉及强引用和弱引用。

  • 强引用:就是我们最常见的普通对象引用,只要还有强引用执行一个对象,表示该对象还“活着”,不会被GC回收。
  • 弱引用(WeakReference):垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间如何,都会回收它的内存。

4 问题分析

4.1 key为强引用

我们来分析下如果ThreadLocalMap的Entry的key是强引用的情况,情况会如何呢?此时ThreadLocal的内存堆栈图如下4.1-1所示:

  • 假设此时业务代码使用完ThreadLocal,ThreadLocal Ref被回收,如下图4.1-2所示

  • 因为ThreadLocal Entry中的key强引用了ThreadLocal导致ThreadLocal对象无法被回收

  • 在没有手动删除这个Entry以及当前线程运行的情况下,始终有强引用链,CurrentThread Ref->CurrentThread->ThreadLocalMap->Entry(entry中包括key引用和value),导致Entry内存泄露。

ThreadLocalMap Entry中的key如果使用强引用,无法完全避免内存泄漏。

4.2 key为弱引用

演示下key为弱引用的情况,如下图4.2-1所示:

  • 假设在业务代码使用完ThreadLocal ref之后,ThreadLocal ref被回收
  • 由于ThreadLocalMap的entry只持有ThreadLocal 的弱引用,没有其他强引用指向ThreadLocal的实例,所以ThreadLocal实例被回收,entry的key为null
  • 但是在没有手动删除Entry和当前线程在运行的情况下,依然存在这强引用链CurrentRef->CurrentThread->ThreadLocalMap entry->value,而这个value不会在被访问到,导致value内存泄露
  • ThreadLocalMap entry的key使用了弱引用,也有可能造成内存泄漏

4.3 内存泄漏的真正原因

以上两种情况,内存泄漏的发生跟ThreadLocalMap key是否使用弱引用没有必然联系,那么真正原因是什么呢?

  1. 没有手动删除entry
  2. 当前线程依然在运行

第一点如果在使用完ThreadLocal并调用其remove方法删除对应的Entry,可以避免内存泄漏。

第二点ThreadLocalMap 是Thread的一个属性,被当前线程引用,所以它的生命周期同Thread一样。如果在使用完ThreadLocal同时,线程结束运行,ThreadLocalMap也会被GC回收,根源上避免了内存泄露。

综上,THreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期同Thread一样长,在使用完ThreadLocal后如果没有手动删除对应的Entry就会导致内存泄漏。

4.4 为什么Entry 的key使用弱引用

根据上面的分析,Entry的key无论是使用强弱引用,都可能导致ThreadLocal的内存泄漏。避免内存泄露的方法:

  1. 使用完ThreadLocal后调用remove方法,删除对应的Entry
  2. 使用完ThreadLocal时,Thread随之运行结束。

显然相对于第一种方式,第二种方式更不好控制,特别是在使用线程池的情况下,线程结束执行是不会被销毁的。

而我们只要在使用完ThreadLocal后,调用remove方法,无论Entry的key是强引用还是弱引用,都不会出现ThreadLocal内存泄露的情况,为什么我们还要使用弱引用呢?

根据我们之前对ThreadLocal源码的分析,ThreadLocalMap的set、getEntry方法中,都会对key为null的entry进行标记回收(非实时),即吧key为null对应entry的vualue置为null。这意味着,在当前线程依然运行的前提下,就算没有忘记调用remove方法,弱引用比强引用可以多一层保障:在ThreadLocal引用被回收之后,对应Entry中的key的弱引用ThreadLocal被回收,对应的value在下一次ThreadLocalMap调用set、get或者remove任一方法的时候会被清除,从而避免内存泄漏。

虽然key为弱引用为避免内存泄漏提供一层保障,但不是实时的,需要在下次调用ThreadLocalMap的相应的方法的时候才会被清除。所以在ThreadLocal使用完成后,强烈建议调用remove方法。

5 hash冲突的解决

5.1 hash计算

首先我们来看下ThreadLocalMap是如果计算hash的,构造方法如下:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];// 计算hashint i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);
}
  • 我们在看下firstKey.threadLocalHashCode,源码如下:
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;

这里通过定义一个AtomicInteger类型,每次获取当前值加上HASH_INCREMENT,这个值跟斐波那契数列(黄金分割数)有关,主要目的使哈希码均匀的分布在2n2^n2n的数组中,尽量避免hash冲突。

  • & (INITIAL_CAPACITY - 1)

计算hash的时候采用hashcode&(size-1)的算法,这相当于hashcode%size 的一个更高效的实现。因此采用该算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash冲突的次数减少。

关于hash、散列以及黄金分割数更深入的知识,本人目前没学习,不做讨论啊

03弱引用内存泄露和hash冲突-ThreadLocal详解-并发编程(Java)相关推荐

  1. 两个例子详解并发编程的可见性问题和有序性问题,通过volatile保证可见性和有序性以及volatile的底层原理——缓存一致性协议MESI和内存屏障禁止指令重排

    1. 并发编程的可见性问题 2. 并发编程的有序性问题 3. 使用volatile关键字解决可见性问题 4. 可见性问题的本质--缓存不一致 因为cpu执行速度很快,但是内存执行速度相对于CPU很慢, ...

  2. python内存的回收机制_python的内存管理和垃圾回收机制详解

    简单来说python的内存管理机制有三种 1)引用计数 2)垃圾回收 3)内存池 接下来我们来详细讲解这三种管理机制 1,引用计数: 引用计数是一种非常高效的内存管理手段,当一个pyhton对象被引用 ...

  3. 解决kali中john无法破解hash(小宇特详解)

    解决kali中john无法破解hash(小宇特详解) 问题描述: john使用w=/usr/share/wordlists/rockyou.txt来进行破解无法进行破解. 报错: /usr/share ...

  4. 【git】git冲突解决详解

    git冲突解决详解 方法1:pull下来然后修改冲突文件 方法2: cherry-pick然后checkout git merge时,不免产生各种各样的冲突,可是我们连如何选择版本都晕半天,这里进行详 ...

  5. SVN 树冲突解决详解

    SVN 树冲突解决详解 https://blog.51cto.com/u_15047490/4217803?abTest=51cto

  6. linux内存管理机制以及free命令详解

    linux内存管理机制以及free命令详解 一.linux内存管理机制 1.物理内存和虚拟内存 直接从物理内存读写数据要比从硬盘读写数据要快的多,因此,我们希望所有数据的读取和写入都在内存完成,而内存 ...

  7. git 主干修改合并到分支_idea+git合并分支解决冲突及详解步骤

    Git分支详解参考: 分支管理组成 1.1.master主干 在版本管理中,代码库应该仅有一个主干.此主干是和当前生产保持一致的,是可用的.稳定的可直接发布的版本,不能再主干上进行任何开发操作.git ...

  8. threadlocal内存泄露_深入理解 ThreadLocal

    前言 上篇文章 https://juejin.im/post/5d712cedf265da03ea5a9ecf 中提到了获取线程的 Looper 是通过 ThreadLocal 来实现的: publi ...

  9. 内存泄露的原因找到了,罪魁祸首居然是 Java TheadLocal

    作者 | 雷架 来源 | 爱笑的架构师(ID:DancingOnYourCode) ThreadLocal使用不规范,师傅两行泪 组内来了一个实习生,看这小伙子春光满面.精神抖擞.头发微少,我心头一喜 ...

最新文章

  1. CV 面试问题详解宝典—目标检测篇
  2. 运行launch文件报错Roslaunch got a ‘No such file or directory‘ error while attempting to run:
  3. 分析师分析业务维度,(个人制作分析思维导图Xmind)
  4. Python档案袋( 面向对象 )
  5. './mysql-bin.index' not found (Errcode: 13) 的解决方法
  6. java 时间类 joda_Java日期类Joda-time的使用及性能对比
  7. Django积木块11 —— 缓存
  8. 蓝桥杯官网试题 欧拉的鸡蛋
  9. DataStore的基础用法
  10. Renderdoc截帧
  11. 计算机应用基础补考申请书,院级教改课题申请书计算机应用基础教学.doc
  12. 对于多组数据输入输出的基础题目
  13. Unity接入高德地图
  14. deepin 下使用节能模式不降低屏幕亮度
  15. 清零术——Stay hungry,Stay foolish
  16. 【2022最新Java面试宝典】—— Java虚拟机(JVM)面试题(51道含答案)
  17. gdut-与蓝神一起戳气球 hnust-硬币翻转 - 博弈论
  18. 思科系统公司(Cisco Systems, Inc.)
  19. ui配色方案_开放色彩–针对设计师的UI优化配色方案
  20. “国际软件自由日”头脑风暴成果

热门文章

  1. 苹果与华为领衔 全球科技巨头进军AI手机领域
  2. 每日爬虫:爬百度千千音乐
  3. 河南高校计算机好的是排名,河南高校的计算机科学与技术专业排名如何?
  4. 抖音爆款小游戏《我飞刀玩得贼6》性能优化案例分享
  5. 虚拟服务器能直接识别U盘吗,虚拟云服务器u盘
  6. R语言灰色关联分析法
  7. PHP 登录TPlink路由器
  8. 电脑重装Win10如何选择32位和64位的系统
  9. 【webrtc】 CongestionControlHandler 的 RTC_DCHECK_RUN_ON(sequenced_checker_);
  10. 【中间件技术】第二部分 CORBA规范与中间件(4) 编写对象接口