在一篇文章中了解 Java 中的内存泄漏
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<Integer> 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<Entry> 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<Entry> 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 !
下一篇: 了解堆与栈之间区别的文章
推荐阅读
-
微信 "扫一扫 "物联网,全面揭秘 "扫一扫 "背后的扫盲技术!-1.1 扫一扫感知物体是做什么的? 1.1 微信扫一扫是做什么的? 扫一扫识物是指以图片或视频(商品图片:鞋/包/美妆/服饰/家电/玩具/图书/食品/珠宝/家具/其他商品)为输入媒介,挖掘微信内容生态中的有价值信息(电商+百科+资讯,如图1所示),并展示给用户。这里的电商基本涵盖了微信小程序覆盖上亿SKU的全量优质电商,可以支持用户货比N家并直接下单购买,百科和资讯则聚合了微信内的头部自媒体如搜狗、搜搜、百度等,向用户展示和分享拍摄商品相关的内容资讯。 图 1 扫一扫识别功能示意图 欢迎大家更新iOS新版微信→扫一扫→识货,亲自体验,也欢迎大家通过识货界面的反馈按钮向我们提交反馈意见。 扫一扫识物实景图展示 1.2 扫一扫识物有哪些使用场景? 扫一扫识物的目的是为用户访问微信内部生态内容开辟一个新窗口,以用户扫图片为输入形式,为用户提供微信生态内容中的百科、资讯、电商等作为展示页面。除了用户熟悉的扫一扫操作外,我们还将进一步拓展长按操作,让用户更方便地进行扫一扫操作。"扫一扫知事 "的落地场景主要涵盖三大部分: a. 科普知识: a.科普知识。用户通过扫一扫,可以在微信生态圈中获取该对象的百科、资讯等常识或趣闻,帮助用户更好地了解该对象; b.购物场景。同样的搜索功能支持用户看到喜欢的商品立即检索到微信小程序电商中的同款商品,支持用户即扫即购; c.广告场景。扫一扫识别物体可以辅助公众号文章、视频更好地理解其中蕴含的图片信息,从而更好地投放匹配广告,提高点击率。 1.3 Sweep Sense 为 Sweep 家族带来了哪些新技术? 对于扫一扫来说,大家耳熟能详的应该就是扫一扫二维码、扫一扫小程序码、扫一扫条形码、扫一扫翻译了。无论是各种形式的编码还是文字字符,都可以看作是图片的一种特定编码形式,而物的识别则是对自然场景图片的识别,这对于扫一扫家族来说是一个质的飞跃,我们希望从物的识别入手,进一步拓展扫一扫对自然场景图片的理解能力,比如扫酒、扫车、扫植物、扫人脸等服务,如下图3所示。 图 3 Sweep 家族
-
为什么UTF-8 和 GBK 会相互转换,为什么会一团糟?-锟斤拷 "是指在字节和字符的转换(编码和解码)过程中使用了不同的编码,找出编码和解码的编码,修改后使用同一种编码。 ===================== 补充 ========================== 在上面的文章中,其实一直回避了一个问题,那就是既然保存中的所有字符都需要转换成二进制,那么 java 是使用什么编码来保存字符的呢?这个问题我们其实可以不必深究,因为它对我们来说是透明的,我们只需假定 java 使用了某种可以表示所有字符的编码。由于这种透明性,我们可以假设 java 直接保存字符本身,就像上面所说的那样。 在 java 虚拟机中使用的是 unicode 字符集。
-
在一篇文章中掌握 godoc 的使用和规范
-
在 WebEndpointProperties 类中阅读 Java 的文章(附演示) - 1.基础知识
-
在一篇文章中,您将了解到滤波器的线性相位、全通滤波器、群延迟
-
在一篇文章中深入了解 PS 层概念
-
[姿势估计] 实践记录:使用 Dlib 和 mediapipe 进行人脸姿势估计 - 本文重点介绍方法 2):方法 1:基于深度学习的方法:。 基于深度学习的方法:基于深度学习的方法利用深度学习模型,如卷积神经网络(CNN)或递归神经网络(RNN),直接从人脸图像中学习姿势估计。这些方法能够学习更复杂的特征表征,并在大规模数据集上取得优异的性能。方法二:基于二维校准信息估计三维姿态信息(计算机视觉 PnP 问题)。 特征点定位:人脸姿态估计的第一步是通过特征点定位来检测和定位人脸的关键点,如眼睛、鼻子和嘴巴。这些关键点提供了人脸的局部结构信息,可用于后续的姿势估计。 旋转表示:常见的旋转表示方法包括欧拉角和旋转矩阵。欧拉角通过三个旋转角度(通常是俯仰、偏航和滚动)描述头部的旋转姿态。旋转矩阵是一个 3x3 矩阵,表示头部从一个坐标系到另一个坐标系的变换。 三维模型重建:根据特征点的定位结果,三维人脸模型可用于姿势估计。通过将人脸的二维图像映射到三维模型上,可以估算出人脸的旋转和平移信息。这就需要建立人脸的三维模型,然后通过优化方法将模型与特征点对齐,从而获得姿势估计结果。 特征点定位 特征点定位是用于检测人脸关键部位的五官基础部分,还有其他更多的特征点表示方法,大家可以参考我上一篇文章中介绍的特征点检测方案实践:人脸校正二次定位操作来解决人脸校正的问题,客户在检测关键点的代码上略有修改,坐标转换部分客户见上图 def get_face_info(image). img_copy = image.copy image.flags.writeable = False image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results = face_detection.process(image) # 在图像上绘制人脸检测注释。 image.flags.writeable = True image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) box_info, facial = None, None if results.detections: for detection in results. for detection in results.detections: mp_drawing.Drawing.detection = 无 mp_drawing.draw_detection(image, detection) 面部 = detection.location_data.relative_keypoints 返回面部 在上述代码中,返回的数据是五官(6 个关键点的坐标),这是用 mediapipe 库实现的,下面我们可以尝试用另一个库:dlib 来实现。 使用 dlib 使用 Dlib 库在 Python 中实现人脸关键点检测的步骤如下: 确保已安装 Dlib 库,可使用以下命令: pip install dlib 导入必要的库: 加载 Dlib 的人脸检测器和关键点检测器模型: 读取图像并将其灰度化: 使用人脸检测器检测图像中的人脸: 对检测到的人脸进行遍历,并使用关键点检测器检测人脸关键点: 显示绘制了关键点的图像: 以下代码将参数 landmarks_part 添加到要返回的关键点坐标中。
-
在一篇文章中|轨迹预测二十年发展的全面回顾!(基于物理/机器学习/深度学习/强化学习)(顶部) - 基于机器学习的经典方法
-
在一篇文章中阅读 Apache Flink 的历史
-
在一篇文章中阅读更多有关 K3s 或 RKE5 的信息!