一文搞懂 Java 中的内存泄漏(Memory Leak)|热消息
2023-02-15 03:08:58 来源:腾讯云
Hello folks,在今天的这篇文章中,我将讨论 Java 虛擬機生态体系中的一个至为关键內容—— Memory Leak(内存泄漏)。
(相关资料图)
从事 Java 开发的技术人员应该都知道:Java 的核心优势之一是基于其内置的垃圾收集器(或简称 GC)的帮助下能够进行内存自动管理。GC 隐式地负责分配和释放内存,从而使得其能够处理大多数内存泄漏问题。
诚然,在某种意义上而言,GC 能够有效地处理大部分的内存问题,但它并不是一种保证万无一失的内存泄漏解决方案。的确,GC 生性非常聪明,但它并非完美无缺,因为内存泄漏仍然可能悄悄地发生,仍然可能存在应用程序生成大量多余对象的情况,然后耗尽关键内存资源,从而导致整个应用程序失败,业务故障。
因此,Memory Leak (内存泄漏)是 Java 虛擬機體系中的一个真正的疑难问题。
在解析 Memory Leak(内存泄漏)之前,我們先來澄清一下相關概念。Memory Leak 與 OutOfMemoryError(內存溢出):内存泄漏可以视为一种問題, OutOfMemoryError 則视为一种症状。因此,并非所有 OutOfMemoryErrors 都意味着内存泄漏,并且并非所有内存泄漏都表现为 OutOfMemoryErrors。
何为 Java 中的 Memory Leak ?
Memory Leak ,即“内存泄漏”,通常是指一个或多个对象不再被使用,但同时又无法被持续工作的垃圾收集器清除的情况。
我们可以将内存中的对象分为两大类:
1、引用对象是可以从我们的应用程序代码访问并且正在或将要使用的对象。
2、未引用的对象是应用程序代码无法访问的对象。
垃圾收集器最终会从堆中移除未引用的对象,为新对象腾出空间,但它不会移除被引用的对象,因为它们被认为很重要。这样的对象会使 Java 堆内存越来越大,并推动垃圾回收做更多的工作。这将导致所构建的应用程序通过抛出 OutOfMemory 异常而变慢甚至最终崩溃。
通常而言,内存泄漏是不好的,在實際的業務場景中,无论是基于业务表現还是用户体验,因为它会阻塞内存资源并随着时间的推移導致系统性能下降。如果不加以及時处理,应用程序最终将耗尽其资源,最终以致命的 Java.lang.OutOfMemoryError 异常终止退出。
在 Java 内存模型设计中,有两种不同类型的对象驻留在堆内存中,“引用的”和“未引用的”。引用对象是那些在应用程序中仍然具有活动引用的对象,而未引用对象没有任何活动引用。
垃圾收集器定期清除未引用的对象,但它默认情况下不会收集仍在引用的对象。这是可能发生内存泄漏的地方,具體如下所示:
Memory Leak 症状
在實際的場景中,有一些較為明顯的症状可以让我们怀疑所构建的 Java 应用程序正在遭受内存泄漏之困扰。以下为最常见的场景:
1、应用程序运行时出现 Java OutOfMemory 错误。
2、应用程序运行时间较长时性能下降,并且不会在应用程序启动后立即出现。
3、应用程序运行的时间越长,垃圾收集次数就越多。
4、连接用完。
Why Memory Leak ?
这是一个很残酷的现实,Java 中的内存泄漏通常可能是由于代码中无法预料的错误而发生的,这些错误会保留对不需要的对象的引用,除此之外,这些链接会阻止 GC 功能操作。
在某些特定的場景下,即使指定了 System.gc() 方法也是如此。当内存不足或可用内存不足以支撐程序所需时,垃圾收集器很可能会启动。如果垃圾收集器没有释放足够的内存资源,那麼,應用程序将會使用操作系统的内存。
与 C++ 和其他编程语言中的内存泄漏相比,Java 内存泄漏通常没有那么严重。根据 IBM developerWorks Jim Patrick 的说法,在考虑内存泄漏时需要考虑两个方面:
1、泄漏的大小
2、程序的生命周期
如果 JVM 有足够的内存来运行所構建的應用程序,那么小型 Java 应用程序中的内存泄漏并不重要。另一方面,如果我們的 Java 应用程序持续运行,内存泄漏将是一个嚴肅的问题,畢竟,无限期运行的软件最终会耗尽内存,從而導致業務故障。
当應用程序使用大量内存的临时对象时,也会发生内存泄漏。如果不取消引用这些耗费大量内存的对象,程序将很快耗尽可访问的内存。
不过,幸运的是,在实际的经验总结中有几种类型的 Java 内存泄漏是众所周知的,通过在编写 Java 代码时给予一定程度的关注,我们可以确保它们不会出现在我们的代码中。
Memory Leak 实践场景
1、静态字段持有对象
可能导致潜在内存泄漏的第一种情况是大量使用静态变量。 Java 内存泄漏的最简单、直接的示例之一便是通过未清除的静态字段引用的对象。例如,一个静态字段包含一组我们永远不会清除或丢弃的对象。
以下為演示此类行为的一个简单的代码示例:
public class StaticReferenceLeak { public static List NUMBERS = new ArrayList<>(); public void addBatch() { for (int i = 0; i < 100000; i++) { NUMBERS.add(i); } } public static void main(String[] args) throws Exception { for (int i = 0; i < 1000000; i++) { (new StaticReferenceLeak()).addBatch(); System.gc(); Thread.sleep(10000); } }}
addBatch 方法将 100000 个整数添加到名为 NUMBERS 的集合中。当然,如果我们需要这些数据,这完全没问题。但在这种情况下,我们永远不会删除它。即使我们在 main 方法中创建了StaticReferenceLeak 对象并且没有持有对它的引用,我们也很容易看出垃圾收集器无法清理内存。相反,它不断增长:
如果我们看不到 StaticReferenceLeak 类的实现细节,我们会期望对象使用的内存被释放,但事实并非如此,因为 NUMBERS 集合是静态的。如果它不是静态的就没有问题,所以在使用静态变量时要格外小心。
解决方案:
为避免并可能防止此类 Java 内存泄漏,因此,应该尽量减少静态变量的使用。如果必须拥有它们,请格外谨慎,当然,在不再需要时从静态集合中删除数据。
2、未关闭的资源
访问位于远程服务器上的资源、打开文件并处理它们等等并不少见。此类代码需要在我们的代码中打开流、连接或文件。但我们必须记住,我们不仅要负责打开资源,还要负责关闭资源。否则,我们的代码可能会泄漏内存,最终导致 OutOfMemory 错误。
为了说明这个问题,让我们看一下下面的例子:
public class UnclosedResources { public static void main(String[] args) throws Exception { for (int i = 0; i < 1000000; i++) { URL url = new URL("http://www.google.com"); URLConnection conn = url.openConnection(); InputStream is = conn.getInputStream(); // rest of the code goes here } }}
上述循环的每次运行都会导致打开和引用 URLConnection 实例,从而导致资源(内存)缓慢耗尽。
解决方案:
(1)始终使用 finally 块来关闭资源
(2)关闭资源的代码(即使在 finally 块中)本身不应有任何异常
(3)使用 Java 7+ 时,我们可以使用 try -with-resources 块
3、使用 ThreadLocals
ThreadLocal 是 Java 世界中的一个结构体,可以让我们将处理范围隔离到当前线程,从而在某些情况下实现线程安全。我们可以保留有关当前用户的信息、绑定到用户的执行上下文或任何需要在线程之间进行隔离的信息。
ThreadLocal(在 Introduction to ThreadLocal in Java tutorial 中有详细讨论)是一种构造,它使我们能够将状态隔离到特定线程,从而使我们能够实现线程安全。
使用此构造时, 每个线程都将持有对其 ThreadLocal 变量副本的隐式引用,并将维护自己的副本,而不是在多个线程之间共享资源,只要线程处于活动状态。
尽管有很多优点,但使用 ThreadLocal 变量是有争议的,因为如果使用不当,它们会因引入内存泄漏而臭名昭著。Joshua Bloch 曾经评论过线程局部使用:
“Sloppy use of thread pools in combination with sloppy use of thread locals can cause unintended object retention, as has been noted in many places. But placing the blame on thread locals is unwarranted.”
当你开始从更广阔的角度思考时,问题就出现了。现代应用程序服务器或 Servlet 容器使用线程池来控制可以并发运行的线程数,从而一遍又一遍地重用相同的线程。在这种情况下,线程会被重用并且不会被垃圾回收,因为对线程的引用一直保存在池本身中。
这不是 ThreadLocal 本身的问题,但总的来说,这是现代技术堆栈内部发生的复杂情况。我们应该预料到并记住分配给 ThreadLocal 的值将被保留,因此需要清理,否则内存将在 ThreadLocal 内部使用。
解决方案:
(1)、当我们不再使用 ThreadLocals 时,清理它们是一种很好的做法。ThreadLocals 提供了 remove()方法,该方法删除当前线程为此变量的值。
(2)、不要使用 ThreadLocal.set(null) 来清除值。它实际上并没有清除该值,而是会查找与当前线程关联的 Map,并将键值对分别设置为当前线程和 Null。
(3)、最好将 ThreadLocal 视为我们需要在 finally 块中关闭的资源,即使在出现异常的情况下也是如此:
try { threadLocal.set(System.nanoTime()); //... further processing}finally { threadLocal.remove();}
4、 引用外部类的内部类
在我看来,这是一个非常有趣的案例——内部私有类保留对其父类的引用的案例。具體如下场景所示:
public class OuterClass { // some large arrays of values private InnerClass inner; public void create() { inner = new InnerClass(); // do something with inner and keep it } class InnerClass { // some logic of the inner class }}
假设 OuterClass 包含对大量占用大量内存的对象的引用,即使不再使用它也不会被垃圾收集。那是因为 InnerClass 对象将隐式引用 OuterClass ,这使得它不符合垃圾收集的条件。
解决方案:
这是关于内部类的要求,是否应该访问外部类中的数据。如果不是,将内部类变为静态将解决该问题。当然,我们还可以首先考虑内部私有类是否真的需要,也许可以使用不同的架构模式。
5、 使用不正确 equals() 和 hashCode() 的实现
Java 内存泄漏的另一个常见示例便是使用具有未正确实现(或根本不存在)的自定义 equals() 和 hashCode() 方法的对象,以及使用哈希检查重复项的集合。这种集合的一个典型代表便是 HashSet。
为了说明这个问题,让我们看一下下如下的例子:
public class HashAndEqualsNotImplemented { public static void main(String[] args) { Set set = new HashSet<>(); for (int i = 0; i < 1000; i++) { set.add(new Entry("test")); } System.out.println(set.size()); }}class Entry { public String entry; public Entry(String entry) { this.entry = entry; }}
在我们深入解释之前,问自己一个简单的问题:代码将使用 System.out.println(set.size()) 调用打印的数字是多少?如果答案是 1000,那么将是是正确的。那是因为我们没有正确实现 equals 方法。这意味着添加到 HashSet 的 Entry 对象的每个实例都会被添加,而不管从我们的角度来看它是否是重复的。这可能会导致 OutOfMemory 异常。
如果我们用正确的实现来改变我们的代码,代码将导致打印 1 作为我们的 HashSet 的大小。我們以如下場景進行簡單舉例說明,下面是 JetBrains IntelliJ 实现的 equals() 和 hashCode() 方法的代码:
public class HashAndEqualsNotImplemented { public static void main(String[] args) { Set set = new HashSet<>(); for (int i = 0; i < 1000; i++) { set.add(new Entry("test")); } System.out.println(set.size()); }}class Entry { public String entry; public Entry(String entry) { this.entry = entry; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Entry entry1 = (Entry) o; return Objects.equals(entry, entry1.entry); } @Override public int hashCode() { return Objects.hash(entry); }}
解决方案:
根据以往的经验,在创建类时应正确实现 equals() 和 hashCode() 方法。大多数现代 IDE 将帮助实现我们进行优化。
6、使用 finalize() 方法
使用终结器是潜在内存泄漏问题的另一个来源。每当重写类的 finalize() 方法时,该类的对象不会立即被垃圾回收。取而代之的是,GC 将它们排队等待最终确定,这发生在稍后的时间点。
此外,如果在 finalize() 方法中编写的代码不是最优的,并且如果终结器队列跟不上 Java 垃圾收集器,那么迟早我们的应用程序注定会遇到 OutOfMemoryError。
解决方案:
很簡單,禁用此方法。
當然,除了如上所述的場景之外,也存在其他的場景,畢竟,基於不同的環境、不同的場景,便會展示不同的現象。
通俗地说,我们可以将内存泄漏视为一种疾病,它通过阻塞重要的内存资源来降低应用程序的性能。和所有其他疾病一样,如果不治愈,随着时间的推移,它可能会导致致命的应用程序崩溃。
Memory Leak,作為一種症狀,有的時候的確很难解决,通常需要對 Java 语言以及操作系統相關知識體系有很深的理解與掌握。畢竟,在处理内存泄漏时,没有一种万能的解决方案,因为泄漏可能通过各种不同的事件、場景发生。
然而,在實際的項目開發活動中,如果我们能夠采用最佳实践并定期执行严格的代码評審和分析,那麼,我们可以将应用程序中内存泄漏的风险降至最低,從而減少損失。
Adiós !
为你推荐
-
一文搞懂 Java 中的内存泄漏(Memory Leak)|热消息
-
前沿资讯!《一诺千金》阅读理解及答案
-
当前简讯:黑色战区机动任务什么时候出 公测上线时间预告
-
微博比较好听的网名大全 世界微速讯
-
太可怕了!又一起新能源车自燃事件,到底是什么原因引起的呢? 资讯
-
男士过生日送什么花_全球热推荐
-
新年祝福文艺句子 全球即时看
-
冠军奖金70万!WTT官宣大满贯赛积分奖金政策,球员赚钱大好时机-每日视点
-
焦点热议:老板看懂财务报表培训_老板如何看懂财务报表
-
每日速看!简短毕业论文致谢词
-
欧文成本赛季末节之王!基德盛赞:他知道末节该如何站出来-世界时讯
-
上海唯一!长宁这项工作获评全国优秀!
-
美国俄亥俄州列车脱轨事故持续发酵 有毒化学物质引担忧-环球快播报
-
天天快消息!广告费和业务宣传费税前扣除标准 广告费的税前扣除比例标准视各行业的性质各是多少
-
cibn_说一说cibn的简介
-
小龙虾的危害
-
刚认识的女生送什么礼物合适
-
环球报道:花刺代理是什么
-
四部门部署保障春季学校食品安全-新视野
-
天天观焦点:2月14日,我想和你有个约会~
推荐内容
- 一文搞懂 Java 中的内存泄漏(Memory Leak)|热消息
- 前沿资讯!《一诺千金》阅读理解及答案
- 当前简讯:黑色战区机动任务什么时候出 公测上线
- 微博比较好听的网名大全 世界微速讯
- 太可怕了!又一起新能源车自燃事件,到底是什么原因
- 男士过生日送什么花_全球热推荐
- 新年祝福文艺句子 全球即时看
- 冠军奖金70万!WTT官宣大满贯赛积分奖金政策,球
- 焦点热议:老板看懂财务报表培训_老板如何看懂财务
- 每日速看!简短毕业论文致谢词
- 欧文成本赛季末节之王!基德盛赞:他知道末节该如
- 上海唯一!长宁这项工作获评全国优秀!
- 美国俄亥俄州列车脱轨事故持续发酵 有毒化学物质
- 天天快消息!广告费和业务宣传费税前扣除标准
- cibn_说一说cibn的简介
- 小龙虾的危害
- 刚认识的女生送什么礼物合适
- 环球报道:花刺代理是什么
- 四部门部署保障春季学校食品安全-新视野
- 天天观焦点:2月14日,我想和你有个约会~
- 灰鸭绒和白鸭绒的区别是什么
- 补贴即将截止!扬州人买房注意……|当前观察
- 环球快资讯:二级工程建造师报考条件_工程建造师
- 北京:新冠疫情态势总体保持平稳 监测预警体系运
- 敏感性分析法_关于敏感性分析法简介
- office 1907 无法注册字体-win10安装office错误
- 世界资讯:给女生买礼物
- 当前要闻:房产证抵押银行贷款流程有哪些_房产证
- 当前焦点!缩写句子10个_关于缩写的句子范例
- 原料涨价、需求承压,梅花生物业绩增长持续性难料
- 石灰投放机_石灰投加装置
- 管虎梁静被曝离婚!丁昱彤否认小三怒骂梁静伤害自
- 电脑键盘锁住了怎么解锁笔记本_电脑键盘锁住了怎
- 每日动态!儿童荨麻疹快速消退方法_宝宝荨麻疹图片
- 2个月婴儿坐飞机后耳膜破裂_2岁宝宝坐飞机耳膜破
- 关于做好冰雪融化期安全防范的提示
- 发型是女人的第二种脸,选对发型年轻十岁,选错发
- 【环球新视野】群聊查看别人送的礼物
- 世界通讯!19岁的女孩生日礼物
- 全球热讯:黑五嗨购节和双十二哪个力度大?黑五的
油气
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
经济
-
中新网杭州10月18日电 (王题题 胡燕婕)云天收夏色,浅秋正渐浓。10月18日,浙江杭州市西湖游船有限公司推出的惠民多站点“西湖环湖游
-
中国税务机关处罚一名艺人经纪人 中新社北京10月18日电 (记者 赵建华)上海市税务局第一稽查局前期在艺人郑爽偷逃税案件检查过程中
-
中新网兰州10月18日电 (闫姣 艾庆龙 吉翔)“红山白土头,黄河向西流。”不少人疑问,天下黄河向东流,为何甘肃永靖县这段黄河却向西
-
中新网北京10月18日电 《清华城市健康设施指数》18日在北京发布。报告成果显示,城市健康设施指数领先城市以中心城市和东部沿海城市
-
中新网安徽黄山10月18日电 (刘浩 黄启宝 汪娜)10月17日至18日,安徽省黄山市当地民警先后救助国家一级保护动物白锦长尾稚和野生梅花