Java 内存泄露风险

目录

内存泄露的定义

潜在的内存泄露场景

未关闭的资源类

未正确实现 equals() 和 hashCode()

非静态内部类

重写了 finalize() 的类

针对长字符串调用 String.intern()

ThreadLocal 的误用

类的静态变量


虽然 Java 程序员不用像 C、C++ 程序员那样时刻关注内存的使用情况,JVM 会帮我们处理好这些,但并不是说有了 GC 就可以高枕无忧,内存泄露相关的问题一般在测试的时候很难发现,一旦上线流量起来可能马上就是一个诡异的线上故障。

内存泄露的定义

如果 GC 无法回收内存中不再使用的对象,则定义为内存有泄露。

潜在的内存泄露场景

未关闭的资源类

当我们在程序中打开一个新的流或者是新建一个网络连接的时候,JVM 都会为这些资源类分配内存做缓存,常见的资源类有网络连接,数据库连接以及 IO 流。值得注意的是,如果在业务处理中异常,则有可能导致程序不能执行关闭资源类的代码,因此最好按照下面的做法处理资源类。

public void handleResource() {try {// open connection// handle business} catch (Throwable t) {// log stack} finally {// close connection}
}

未正确实现 equals() 和 hashCode()

假如有下面的这个类:

public class Person {public String name;public Person(String name) {this.name = name;}
}

并且如果在程序中有下面的操作:

@Test
public void givenMapWhenEqualsAndHashCodeNotOverriddenThenMemoryLeak() {Map<Person, Integer> map = new HashMap<>();for(int i=0; i<100; i++) {map.put(new Person("jon"), 1);}Assert.assertFalse(map.size() == 1);
}

可以预见,这个单元测试并不能通过,原因是Person类没有实现equals方法,因此使用Object的equals方法,直接比较实体对象的地址,所以 map.size() == 100。如果我们改写 Person 类的代码如下所示:

public class Person {public String name;public Person(String name) {this.name = name;}@Overridepublic boolean equals(Object o) {if (o == this) return true;if (!(o instanceof Person)) {return false;}Person person = (Person) o;return person.name.equals(name);}@Overridepublic int hashCode() {int result = 17;result = 31 * result + name.hashCode();return result;}
}

则上文中的单元测试就可以顺利通过了,需要注意的是这个场景比较隐蔽,一定要在平时的代码中注意。

非静态内部类

要知道,所有的非静态类别类都持有外部类的引用,因此某些情况如果引用内部类可能延长外部类的生命周期,甚至持续到进程结束都不能回收外部类的空间,这类内存溢出一般在 Android 程序中比较多,只要 MyAsyncTask 处于运行状态 MainActivity 的内存就释放不了,很多时候安卓开发者这样做只是为了在内部类中拿到外部类的属性,殊不知,此时内存已经泄露了。

public class MainActivity extends Activity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);new MyAsyncTask().execute();}private class MyAsyncTask extends AsyncTask {@Overrideprotected Object doInBackground(Object[] params) {return doSomeStuff();}private Object doSomeStuff() {//do something to get resultreturn new MyObject();}}
}

重写了 finalize() 的类

如果运行下面的这个例子,则最终程序会因为 OOM 的原因崩溃:

public class Finalizer {@Overrideprotected void finalize() throws Throwable {while (true) {Thread.yield();}}public static void main(String str[]) {while (true) {for (int i = 0; i < 100000; i++) {Finalizer force = new Finalizer();}}}
}

JVM 对重写了 finalize() 的类的处理稍微不同,首先会针对这个类创建一个 java.lang.ref.Finalizer 类,并让 java.lang.ref.Finalizer 持有这个类的引用,在上文中的例子中,因为 Finalizer 类的引用被 java.lang.ref.Finalizer 持有,所以他的实例并不能被 Young GC 清理,反而会转入到老年代。在老年代中,JVM GC 的时候会发现 Finalizer 类只被 java.lang.ref.Finalizer 引用,因此将其标记为可 GC 状态,并放入到 java.lang.ref.Finalizer.ReferenceQueue 这个队列中。等到所有的 Finalizer 类都加到队列之后,JVM会起一个后台线程去清理 java.lang.ref.Finalizer.ReferenceQueue 中的对象,之后这个后台线程就专门负责清理 java.lang.ref.Finalizer.ReferenceQueue 中的对象了。这个设计看起来是没什么问题的,但其实有个坑,那就是负责清理 java.lang.ref.Finalizer.ReferenceQueue 的后台线程优先级是比较低的,并且系统没有提供可以调节这个线程优先级的接口或者配置。因此当我们在使用使用重写 finalize() 方法的对象时,千万不要瞬间产生大量的对象,要时刻谨记,JVM 对此类对象的处理有特殊逻辑。

针对长字符串调用 String.intern()

如果提前在 src/test/resources/large.txt 中写入大量字符串,并且在 Java 1.6 及以下的版本运行下面程序,也将得到一个 OOM。

@Test
public void givenLengthString_whenIntern_thenOutOfMemory()throws IOException, InterruptedException {String str = new Scanner(new File("src/test/resources/large.txt"), "UTF-8").useDelimiter("\\A").next();str.intern();System.gc(); Thread.sleep(15000);
}

原因是在 Java 1.6 及以下,字符串常量池是处于 JVM 的 PermGen 区的,并且在程序运行期间不会 GC,因此产生了 OOM。在 Java 1.7 以及之后字符串常量池转移到了 HeapSpace 此类问题也就无需再关注了。

ThreadLocal 的误用

ThreadLocal 一定要列在 Java 内存泄露的榜首,总能在不知不觉中将内存泄露掉,一个常见的例子是:

@Test
public void testThreadLocalMemoryLeaks() {ThreadLocal<List<Integer>> localCache = new ThreadLocal<>();List<Integer> cacheInstance = new ArrayList<>(10000);localCache.set(cacheInstance);localCache = new ThreadLocal<>();
}

当 localCache 的值被重置之后 cacheInstance 被 ThreadLocalMap 中的 value 引用,无法被 GC,但是其 key 对 ThreadLocal 实例的引用是一个弱引用,本来 ThreadLocal 的实例被 localCache 和 ThreadLocalMap 的 key 同时引用,但是当 localCache 的引用被重置之后,则 ThreadLocal 的实例只有 ThreadLocalMap 的 key 这样一个弱引用了,此时这个实例在 GC 的时候能够被清理。

其实看过 ThreadLocal 源码的同学会知道,ThreadLocal 本身对于 key 为 null 的 Entity 有自清理的过程,但是这个过程是依赖于后续对 ThreadLocal 的继续使用,假如上面的这段代码是处于一个秒杀场景下,会有一个瞬间的流量峰值,这个流量峰值也会将集群的内存打到高位(或者运气不好的话直接将集群内存打满导致故障),后面由于峰值流量已过,对 ThreadLocal 的调用也下降,会使得 ThreadLocal 的自清理能力下降,造成内存泄露。ThreadLocal 的自清理实现是锦上添花,千万不要指望它雪中送碳。

类的静态变量

Tomcat对在网络容器中使用ThreadLocal引起的内存泄露做了一个总结,这里我们列举其中的一个例子。熟悉Tomcat的同学知道,Tomcat 中的 web 应用由 webapp classloader 这个类加载器的,并且 webapp classloader 是破坏双亲委派机制实现的,即所有的web应用先由 webapp classloader 加载,这样的好处就是可以让同一个容器中的 web 应用以及依赖隔离。下面我们看具体的内存泄露的例子:

public class MyCounter {private int count = 0;public void increment() {count++;}public int getCount() {return count;}
}public class MyThreadLocal extends ThreadLocal<MyCounter> {
}public class LeakingServlet extends HttpServlet {private static MyThreadLocal myThreadLocal = new MyThreadLocal();protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {MyCounter counter = myThreadLocal.get();if (counter == null) {counter = new MyCounter();myThreadLocal.set(counter);}response.getWriter().println("The current thread served this servlet " + counter.getCount()+ " times");counter.increment();}
}

需要注意这个例子中的两个非常关键的点:

  • MyCounter 以及 MyThreadLocal 必须放到 web 应用的路径中,确保被 webapp classloader 加载。
  • ThreadLocal 类一定得是 ThreadLocal 的继承类,比如例子中的 MyThreadLocal,因为 ThreadLocal 本来被 common classloader 加载,其生命周期与 tomcat 容器一致。ThreadLocal 的继承类包括比较常见的 NamedThreadLocal,注意不要踩坑。

假如 LeakingServlet 所在的 web 应用启动,MyThreadLocal 类也会被 webapp classloader 加载,如果此时 web 应用下线,而线程的生命周期未结束(比如为 LeakingServlet 提供服务的线程是一个线程池中的线程),那会导致 myThreadLocal 的实例仍然被这个线程引用,而不能被 GC,期初看来这个带来的问题也不大,因为 myThreadLocal 所引用的对象占用的内存空间不太多,问题在于 myThreadLocal 间接持有加载 web 应用的 webapp classloader 的引用(通过 myThreadLocal.getClass().getClassLoader() 可以引用到),而加载 web 应用的 webapp classloader 持有它加载的所有类的引用,这就引起了 classloader 泄露,它泄露的内存就非常可观了。

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

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

相关文章

蚂蚁技术日首次开放,精彩看点分享

每年的 5 月 27 日&#xff0c;是蚂蚁的技术日&#xff0c;用来鼓励蚂蚁技术人保持敬畏和创新之心&#xff0c;到今天&#xff0c;第九届“527 蚂蚁技术日”已发展成为技术周&#xff0c;成为蚂蚁技术人的嘉年华。 2015 年 5 月 27 日&#xff0c;因为光纤被挖断&#xff0c;全…

国产身份域管架构图集合(信创政策AD域替换必看)

几类典型架构 双机架构 单点单机房 集群架构 多点单机房 两地三中心架构 多点多机房 多地分布式架构 多点多机房 全栈信创方案架构&#xff0c;欢迎探讨交流~

React useContext源码分析

React 框架中 useContext Hook 用于数据的传递&#xff0c;组件的数据传递有几种方式&#xff0c;通过 props、状态管理 和 useContext。本文将讲述useContext 在 React 是如何工作的&#xff0c;创建一个简单的 Context 例子并根据源码进行 Debug&#xff1a; 创建 context …

剖析【C++】——类和对象(下篇)——超详解——小白篇

目录 1.再谈构造函数 1.1 构造函数体赋值 1.2 初始化列表 1.3 explicit 关键字 2. Static成员 2.1 概念 2.2 特性 3. 友元 3.1 友元函数 3.2 友元类 3.3总结&#xff1a; 4. 内部类 1.概念 2.特性 示例代码&#xff1a; 代码分析 3.总结 5.再次理解类和对象 …

MySQL各种锁

目录 1. 从粒度上区分锁 1.1 全局锁&#xff08;第一粒度&#xff09; 1.2 表级锁&#xff08;第二粒度&#xff09; 1.3 行锁&#xff08;第三最小粒度&#xff09; 2 从模式上区分锁 2.1 什么是乐观锁 2.2 什么是悲观锁 2.3 意向共享锁和意向排他锁 2.4 临键锁和记录…

autocad背景色、引线文字大小

一、改变背景 在命令行输入op&#xff0c;回车&#xff0c;弹出配置对话框&#xff1a; 二、改变引线文字大小 选中引线&#xff0c;右键选择【特性】&#xff0c;在文字选项卡中设置文字高度&#xff1a;

Linux学习笔记:日志文件的编写

日志文件Log.hpp 日志文件的作用简单的日志文件编写 日志文件的作用 日志文件可以很好的帮我们显示出程序运行的信息,例如,进程pid,运行时间,运行状况等,通过日志记录程序的执行路径、变量值、函数调用等&#xff0c;可以帮助我们快速定位和修复代码中的错误。 简单的日志文件…

磁盘配额的具体操作

磁盘配额&#xff1a; linux的磁盘空间有两个方面&#xff1a;第一个是物理空间&#xff0c;也就是磁盘的容量 第二个inode号耗尽&#xff0c;也无法写入 linux根分区&#xff1a;根分区的空间完全耗尽&#xff0c;服务程序崩溃&#xff0c;系统也无法启动了。 为了防止有人…

JavaWeb 请求响应路径调试

在使用mvc时&#xff0c;或许会遇到请求的页面响应不了&#xff0c;这种情况要对站下径。 站点根目录 启动服务器时&#xff0c;通常要知道哪个是站点根目录。相应在网页端的url的跟站点通常为http://localhost:8080/ &#xff0c;前端解析时用的是站点根目录。 <form act…

【嵌入式DIY实例】-OLED显示网络时钟

OLED显示网络时钟 文章目录 OLED显示网络时钟1、硬件准备与接线2、代码实现在上一个ESP8266 NodeMCU文章中,我们用DS3231 RTC芯片和SSD1306 OLED制作了一个简单的实时时钟,时间和日期显示在SSD1306屏幕上,并且可以通过两个按钮进行设置。 在本中,我们将使用ESP 8266 NodeMC…

【Gradle】Gradle的本地安装和使用

目录 1、Gradle 的安装 2、集成 IntelliJ IDEA 3、使用 Gradle Gradle 完全兼容 Maven 和 Ivy 仓库&#xff0c;你可以从中检索依赖也可以发布你的文件到仓库中&#xff0c;Gradle 提供转换器能把 Maven 的构建逻辑转换成 Gradle 的构建脚本。 1、Gradle 的安装 Gradle 的…

Python 将Word、Excel、PDF、PPT文档转为OFD文档

OFD&#xff08;Open Fixed-layout Document &#xff09;是我国自主制定的一种开放版式文件格式标准。OFD文档具有不易被篡改、格式独立、版式固定等特点&#xff0c;目前常用于政府公文、金融、电子发票等领域。 如果想要通过Python将Office文档&#xff08;如Word、Excel或…

Android 车载 Audio 中 有关系统按键无声的问题排查小结

本文简单记录一下&#xff0c;车载中系统按键音的问题排查从 App --> FrameWork --> HAL层 的问题排查。 通过日志分析&#xff1a; AudioStreamOutSink 这个有数据写入到 HAL 中&#xff08;方式一&#xff09; 查看 dump 文件。&#xff08;方式二&#xff09; 先 …

SAP 物料的与压缩库存数据不一致

在测试系统中进行501和561增加库存的时候,系统提示物料的预压缩库存数据不一致的报错。 如下图所示: 首先想到的就是搜SAP的NOTES,找到了相关的两个note:293356 note:2197042 表示:数据库表 ACDOCA 和 ACDOCA_M_EXTRACT 之间的差异。 经过查询发现是由于物料分类账中的…

Docker 图形化界面管理工具 Portainer | 让你更轻松的管理 Docker

本文首发于只抄博客&#xff0c;欢迎点击原文链接了解更多内容。 前言 Portainer 是一个 Docker 图形化管理工具&#xff0c;可以通过 Web UI 轻松的管理容器、镜像、网络、卷。与 Dockge 相比功能更加的完善&#xff0c;同时上手难度也更大一些 Portainer 分为社区版和商业版…

你是否正确地编写了 Git 提交信息?

介绍 在版本控制方面&#xff0c;Git 是一个非常有效的工具。然而&#xff0c;像任何其他工具一样&#xff0c;你必须正确使用它才能充分发挥其作用。你需要考虑不同的方面。本文着重介绍如何按照传统提交规范&#xff08;Conventional Commits specification&#xff09;编写…

线上商城API接口再次升级||电商API接口对接线上商城搭建

功能更新 商城对接【蚂蚁搬客】应用 API接口产品上传及订单状态修改 01 商城对接API应用 ▼ 使用场景 适用于多个电商平台&#xff08;如淘宝、京东、天猫、1688、苏宁、唯品会、当当等&#xff09;的产品搬家&#xff0c;包括产品标题、主图等信息&#xff0c;轻松完成商…

NFTScan 获 Google Cloud 战略支持!

近日&#xff0c;NFT 数据基础设施服务商 NFTScan 获得全球领先云计算服务提供商 Google Cloud 战略支持。未来&#xff0c;双方将在链上数据和区块链领域展开战略合作&#xff0c;高效联动&#xff0c;共同探索区块链技术的更多可能性&#xff0c;为用户和行业带来更多惊喜与成…

束测后台实操文档2-OpenWrt

束测后台实操文档1-PVE、PBS 上面文&#xff0c;把proxmox装好并添加好PBS上的镜像存储空间后&#xff0c;还原已经做好的镜像基本上就可以在已有的镜像下开展工作了。 调试的PVE环境一般两个网口&#xff0c;一个外网wan&#xff0c;一个子网lan&#xff0c;虚拟机一般在lan…

【UnityShader入门精要学习笔记】第十六章 Unity中的渲染优化技术 (上)

本系列为作者学习UnityShader入门精要而作的笔记&#xff0c;内容将包括&#xff1a; 书本中句子照抄 个人批注项目源码一堆新手会犯的错误潜在的太监断更&#xff0c;有始无终 我的GitHub仓库 总之适用于同样开始学习Shader的同学们进行有取舍的参考。 文章目录 移动平台上…