【JUC】二十五、ThreadLocal内存泄漏问题(强软弱虚四种引用)

文章目录

  • 1、引用之强软弱虚
  • 2、强引用
  • 3、软引用
  • 4、弱引用
  • 5、虚引用
  • 6、ThreadLocal回顾
  • 7、ThreadLocal使用弱引用的原因
  • 8、清除脏Entry
  • 9、最佳实践

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露(累积可能导致OOM)。

1、引用之强软弱虚

在这里插入图片描述

  • Reference:强引用
  • SoftReference:软引用
  • WeakReference:弱引用
  • PhantomReference:虚引用

Java 允许使用 finalize方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作(遗言时机)。

//since Java9,已过期
public class MyObject {@Overrideprotected void finalize() throws Throwable {//finalize用于在对象被不可撤销的丢弃之前执行的操作System.out.println("----invoke finalize method ~");}
}

2、强引用

当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。

强引用是最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象

//eg:
Student student = new Student();
  • 在Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用

  • 当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收

  • 因此强引用是造成Java内存泄漏的主要原因之一

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。

public class ReferenceDemo {public static void main(String[] args) {MyObject myObject = new MyObject();System.out.println("gc before: " + myObject);myObject = null;//手动触发一次GCSystem.gc();System.out.println("gc after: " + myObject);}
}

在这里插入图片描述

调用finalize方法是另一线程,这里的打印顺序不用关注。

3、软引用

软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。

总之就是相对强引用而言,稍微松一点,GC触发时:

  • 当系统内存充足时,它不会被回收
  • 当系统内存不足时,它会被回收

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收。用SoftReference把自定义对象包装一下,对应的引用就变成了软引用。

SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
System.out.println("gc before: " + softReference.get());
//手动触发一次GC
System.gc();
System.out.println("gc after: " + softReference.get());

在这里插入图片描述

修改Demo类的JVM内存限制,创造一个内存不足的情况:

在这里插入图片描述

创建一个20M的数组,超过了上面的最大内存,模拟内存不足,对象被回收:
在这里插入图片描述

在这里插入图片描述

4、弱引用

对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。

WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
System.out.println("gc before: " + weakReference.get());
System.gc();
System.out.println("gc after: " + weakReference.get());

在这里插入图片描述

软引用和弱引用的适用场景举例:

假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取则会严重影响性能,如果一次性全部加载到内存中又可能造成内存溢出,此时使用软引用可以解决这个问题。

设计思路是:

用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>().

5、虚引用

1)虚引用必须和引用队列ReferenceQueue联合使用

  • 虚引用需要java.lang.ref.PhantomReference类来实现
  • 虚,即形同虚设
  • 虚引用不会决定对象的生命周期
  • 如果一个对象仅持有虚引用,则它和没任何引用一样,随时都可能被垃圾回收器回收
  • 不能单独使用,也不能通过它访问对象
  • 虚引用必须和引用队列ReferenceQueue联合使用,如果虚引用对象被干掉了,就装到队列里

2)PhantomReference虚引用的get方法总是返回null

  • 虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅是提供了一种确保对象被 finalize以后,做某些事情的通知机制
  • PhantomReference的get方法总是返回null,因此无法访问对应的引用对象

3)处理监控通知使用

  • 设置虚引用关联对象的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作

构造方法:

//传入要包装的对象和引用队列
PhantomReference(T referent, ReferenCeQueue<? super T> queue)

继续设置JVM最大内存10M:

public class ReferenceDemo {public static void main(String[] args) {MyObject myObject = new MyObject();ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>();PhantomReference<MyObject> phantomReference = new PhantomReference<>(myObject, referenceQueue);List<byte[]> list = new ArrayList<>();new Thread(() -> {while (true){list.add(new byte[1024 * 1024]); //1M//歇500ms,写1M进Listtry {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}//验证下每次get都是nullSystem.out.println(phantomReference.get() + " list add OK.");}},"t1").start();new Thread(() -> {while (true){Reference<? extends MyObject> reference = referenceQueue.poll();if(reference != null){System.out.println("有虚引用对象被回收,加入了队列");//break;}}},"t2").start();}}

开一个线程去占用内存,另开一个线程去查看队列,可以看到中途虚引用对应的对象被回收时,会加入到队列中。

在这里插入图片描述

在这里插入图片描述

6、ThreadLocal回顾

在这里插入图片描述

ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里的ThreadLocalMap这个内部类,每个Thread对象维护着ThreadLocalMap的引用,ThreadLocalMap则用Entry来进行存储。

  • 调用ThreadLocal的set方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLoca对象,值Value是传递进来的对象
  • 调用ThreadLocal的get方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象

ThreadLocal本身并不存储值(ThreadLocal是一个壳),它只是自己作为一个key来让线程从ThreadLocalMap获取value。正因为这个原理,所以ThreadLocal能够实现线程间的"数据隔离",获取当前线程的局部变量值,不受其他线程影响

7、ThreadLocal使用弱引用的原因

public void function01(){//新建一个ThreadLocal对象,t1是强引用指向这个对象ThreadLocal<String> t1 = new ThreadLocal<>();//实际是创建了一个Entry对象,根据Entry源码知:Entry对象里的key(即ThreadLocal)是弱引用指向这个对象//当一个ThreadLocal实例对象只被Entry类实例(或者其它弱引用实例)引用时,它就会被GC回收t1.set("code9527");t1.get();
}

在这里插入图片描述

当function1方法执行完毕后,栈帧销毁,强引用 t1 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象。此时:

  • 若这个key引用是强引用,就会导致key指向的ThreadLocal对象,以及v指向的对象不能被gc回收,造成内存泄漏

  • 若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为nul的雷,在下面再展开)。

使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null,而此后我们调用get、set、remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存。

8、清除脏Entry

当我们为threadLocal变量赋值,实际上就是当前Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(比如前面例子的t1=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条路能够引用到它, 这个ThreadLocal势必会被回收。

这样一来,ThreadLocalMap中就会出现key为nul的Entry,就没有办法访问这些key为nul的Entry的value,如果当前线程再迟迟不结束的话(线程池,线程在不断复用),这些key为null的Entry的value就会一直存在一条强引用:某个线程池中线程T1的引用Thread Ref ⇒ Thread ⇒ ThreadLocalMap ⇒ Entry ⇒ value ,因此永远无法回收,最后造成内存泄漏。

当然,如果当前thread运行结束,threadLocal、threadLocalMap、Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。

关于以上key为null的脏Entry的清除 ===> expungeStaleEntry方法

其中,get、set、remove等方法源码中,都有调用expungeStaleEntry方法,如get --> 调getEntry方法 --> getEntryAfterMiss方法:

在这里插入图片描述

虽然弱引用,保证了key指向的ThredLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为nul时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露,我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。

总结:

  • 弱引用保证ThreadLocal对象被及时回收,key为null的Entry会累积
  • get、set时检查所有键为null的Entry对象并删除

9、最佳实践

  • 【建议】创建ThreadLocal对象采用静态方法ThreadLocal.withInitial(() -> 初始值)
  • 【建议】把ThreadLocal修饰为static(若某个属性所有对象都相同,则用静态变量,存方法区,如国籍,这样只在方法区保存一份,可避免不必要的内存空间浪费,反之,则是实例变量)
  • 【强制】用完手动remove

在这里插入图片描述

最后,对ThreadLocal的总结:

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
  • 都会通过expungeStaleEntry、cleanSomeSlots、replaceStaleEntry这三个方法回收键为 null 的 Entry对象的值 以及 Entry 对象本身,从而防止内存泄漏,属于安全加固的方法
  • 群雄逐鹿起纷争,人各一份天下安

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

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

相关文章

InnoDB在SQL查询中的关键功能和优化策略

文章目录 前言存储引擎介绍存储引擎是干嘛的InnoDB的体系结构 InnoDB的查询操作InnoDB的查询原理引入 Buffer Pool引入数据页Buffer Pool 的结构数据页的加载Buffer Pool 的管理Buffer Pool 的优化 总结 前言 通过上篇文章《MySQL的体系结构与SQL的执行流程》了解了SQL语句的执…

【LSM tree 】Log-structured merge-tree 一种分层、有序、面向磁盘的数据结构

文章目录 前言基本原理读写流程写流程读流程 写放大、读放大和空间放大优化 前言 LSM Tree 全称是Log-structured merge-tree, 是一种分层&#xff0c;有序&#xff0c;面向磁盘的数据结构。其核心原理是磁盘批量顺序写比随机写性能高很多&#xff0c;可以通过围绕这一原理进行…

scala表达式

1.8 表达式&#xff08;重点&#xff09; # 语句(statement)&#xff1a;一段可执行的代码# 表达式(expression)&#xff1a;一段可以被求值的代码&#xff0c;在Scala中一切都是表达式 - 表达式一般是一个语句块&#xff0c;可包含一条或者多条语句&#xff0c;多条语句使用“…

Android BluetoothAdapter 使用(二)

Android BluetoothAdapter 使用(二) 本篇文章主要讲下蓝牙设备的配对. 1: 蓝牙设备列表展示 下 面是蓝牙设备adapter的代码: package com.test.bluetooth;import android.bluetooth.BluetoothDevice; import android.content.Context; import android.view.LayoutInflater;…

Linux中的堡垒机搭建以及使用

JumpServer搭建 安装应用包 curl -sSL https://resource.fit2cloud.com/jumpserver/jumpserver/releases/latest/download/quick_start.sh | bash 一路回车即可安装完毕&#xff08;可根据需求更改&#xff09; JumpServer的 配置文件路径 /opt/jumpserver/config/config.tx…

【智能家居】九、停车场车牌识别功能点(回调、解耦)

一、翔云 人工智能开放平台&#xff08;车牌识别&#xff09; 二、cJSON 库 三、实现代码 四、回调函数 五、人脸识别和车牌识别获取数据的区别 六、异步网络请求和同步网络请求的区别 七、解耦 一、翔云 人工智能开放平台&#xff08;车牌识别&#xff09; 翔云 人工智能开放…

.NET 反射优化的经验分享

比如针对 GetCustomAttributes 通过反射获取属性的优化,以下例子 // dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0public class Tests{public object[] GetCustomAttributes() => typeof(C).GetCustomAttributes(typeof(MyAttribute…

坑爹的奥数(枚举法)

枚举法是一种解决问题的基本方法&#xff0c;它通过列举问题的所有可能情况来找到问题的解。这种方法适用于问题的解空间相对较小&#xff0c;可以通过穷举所有可能的解来找到最优解或满足特定条件的解。 以下是枚举法的一般步骤&#xff1a; 定义问题&#xff1a; 确定问题的…

Cypress安装与使用教程(2)—— 软测大玩家

&#x1f60f;作者简介&#xff1a;博主是一位测试管理者&#xff0c;同时也是一名对外企业兼职讲师。 &#x1f4e1;主页地址&#xff1a;【Austin_zhai】 &#x1f646;目的与景愿&#xff1a;旨在于能帮助更多的测试行业人员提升软硬技能&#xff0c;分享行业相关最新信息。…

数据库字段名和sql关键字冲突报错解决方法

1、修改实体类字段映射。注解里加反引号 2、sql字段上加反引号 3、问题解决

ue5材质预览界面ue 变黑

发现在5.2和5.1上都有这个bug 原因是开了ray tracing引起的&#xff0c;这个bug真是长时间存在&#xff0c;类似的bug还包括草地上奇怪的影子和地形上的影子等等 解决方法也很简单&#xff0c;就是关闭光追&#xff08;不是…… 就是关闭预览&#xff0c;在材质界面preview sc…

C# WPF上位机开发(会员充值软件)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 在软件开发中&#xff0c;有一种很重要的控件&#xff0c;那就是表格。大家可以想象下&#xff0c;办公软件里面是不是就有一个专门做表格的软件&a…

路由器的转换原理--ENSP实验

目录 一、路由器的工作原理 二、路由表的形成 1、直连路由 2、非直连路由 2.1静态路由 2.2动态路由 三、静态路由和默认路由 1、静态路由 1.1静态路由的缺点 1.2路由的配置--结合ensp实验 2、默认路由--特殊的静态路由 2.1概念 2.2格式 2.3默认路由的配置--ens…

本地部署语音转文字(whisper,SpeechRecognition)

本地部署语音转文字 1.whisper1.首先安装Chocolatey2.安装3.使用 2.SpeechRecognition1.环境2.中文包3.格式转化4.运行 3.效果 1.whisper 1.首先安装Chocolatey https://github.com/openai/whisper 以管理员身份运行PowerShell Set-ExecutionPolicy Bypass -Scope Process -…

LeetCode刷题--- 二叉树剪枝

个人主页&#xff1a;元清加油_【C】,【C语言】,【数据结构与算法】-CSDN博客 个人专栏&#xff1a;http://t.csdnimg.cn/ZxuNL http://t.csdnimg.cn/c9twt 前言&#xff1a;这个专栏主要讲述递归递归、搜索与回溯算法&#xff0c;所以下面题目主要也是这些算法做的 我讲述…

NFTScan | 12.04~12.10 NFT 市场热点汇总

欢迎来到由 NFT 基础设施 NFTScan 出品的 NFT 生态热点事件每周汇总。 周期&#xff1a;2023.12.04~ 2023.12.10 NFT Hot News 01/ NFTScan 与 MintCore 联合推出适用于 NFT 的 Layer2 网络 Mint 12 月 5 日&#xff0c;根据官方消息&#xff0c;NFT 基础设施服务商 NFTScan …

NFC物联网解决方案应用实例:基于NFC的通用物流链防伪溯源

NFC物联网系统解决方案已在某局进行推广应用&#xff0c;给出了某省内出口蔬菜水果检验检疫监管的物联网解决方案。 依据相关法规&#xff0c;出口蔬菜必须在质检总局注册种植基地进行种植&#xff0c;出口前按批次向产地检验检疫部门进行申报&#xff0c;按时在集中监管区统一…

Python+selenium自动化生成测试报告

前言 批量执行完用例后&#xff0c;生成的测试报告是文本形式的&#xff0c;不够直观&#xff0c;为了更好的展示测试报告&#xff0c;最好是生成HTML格式的。 unittest里面是不能生成html格式报告的&#xff0c;需要导入一个第三方的模块&#xff1a;HTMLTestRunner 一、导…

SpringBoot系列之基于Jedis实现分布式锁

Redis系列之基于Jedis实现分布式锁 1、为什么需要分布式锁 在单机环境&#xff0c;我们使用最多的是juc包里的单机锁&#xff0c;但是随着微服务分布式项目的普及&#xff0c;juc里的锁是不能控制分布锁环境的线程安全的&#xff0c;因为单机锁只能控制同个进程里的线程安全&…

Java实现选择排序及其动图演示

选择排序是一种简单直观的排序算法。它的基本思想是每次从未排序的元素中选出最小&#xff08;或最大&#xff09;的元素&#xff0c;然后将其放到已排序的序列的末尾。具体步骤如下&#xff1a; 首先&#xff0c;找到未排序序列中的最小&#xff08;或最大&#xff09;元素&a…