性能文章>【译】Java中内存泄漏的问题类型,原因和解决方案>

【译】Java中内存泄漏的问题类型,原因和解决方案转载

1年前
4454510

20 多年来,Java语言 一直在世界技术市场上占据领先地位。 

它的核心优势之一就是自动内存管理机制,比如GC机制,特别是与没有此功能的 C 和 C++ 语言相比。

当然,凡事有利有弊,内存管理机制背后也有问题,什么问题呢?其中一个就是内存泄漏的问题。

什么是内存泄漏及其原因?

为了更好地理解直接示例的本质,先从 Java 内存结构开始讲起。 

1.内存结构及其清理

what_is_memory_leak_and_its_causes

Java 应用程序数据可以存储在空间块中,例如 «Stack» 或 «Heap»。 

  • 堆栈内存是对“堆”元素和原始值类型的存储引用的存储库;
  • 堆包含堆栈中的变量引用的动态对象。

java_memory

因此,在默认设置下,堆在内存中占用的空间比堆栈要多得多。但是,我们可以通过在增加堆栈值后将堆大小设置为低于堆栈值来手动更改这些设置。

Stack 具有LIFO 原则(最后进入,先离开)。每当引用元素或原始值调用新方法时,都会在堆栈顶部释放一块内存:

与堆栈不同,堆不会自行清理。因此,系统迫切需要垃圾收集器功能。如果没有它的存在,我们只能管理堆的大小:

java_8_management

随着时间的推移,对可能包含堆栈变量的对象的引用成为垃圾收集器的合适目标,具体取决于其类型。并且,当堆内存中出现红色项目时,它们可以由收集器组装。 

内存中的垃圾项示例:

example_of_garbage_items_in_memory

2.内存泄漏

Java内存泄漏是GC在系统中留下未使用的对象时的一种错误。这可能是因为无法删除堆栈中可能引用的一些无用对象。 

泄漏的典型示例:

典型的_example_of_leakage

这种泄漏对利用系统资源的能力及其整体生产力具有负面影响。如果忽略此问题,系统可能会完全耗尽数据存储空间,并以不可逆的错误“Java Out Of Memory Error”结束。

最好使用内存管理工具来优化它。其中最相关的:

  • 惠普的OpenView Operations (OVO);
  • Apache 的JMETER;
  • IBM 的IBM Tivoli;
  • TechRepublic 的JProbe Profiler。

3、Java中内存泄漏的原因

Java 中的内存泄漏可能是由于代码中不可预见的错误导致对云中不需要的对象的引用。这些链接会阻止 GC 功能。因此,无法清理存储库,这些对象没有使用它。

原因_of_memory_leakage_in_java

Java内存泄漏的主要原因:

  • 无限缓存;
  • 一个会话中的文件溢出;
  • 更换操作系统页面过多;
  • 用户数据系统中的错误;
  • 在集合中插入对象而不删除它们; 
  • 不可复制的聆听方式。

有关 Java 中内存泄漏的详细分析信息,您可以在此处找到: 

视频名称

Java 中的内存泄漏及其类型

各种类型的泄漏都是可能的。它们的差异基于它们是如何产生的以及是什么原因造成的。 

最常见的泄漏类型:

1.过度使用静态变量

Java 中静态字段的生命周期通常对应于应用程序会话时间,而不考虑具有 « ClassLoader » 功能的垃圾收集。在命令执行期间分析内存堆时,我们可以观察到控制点 1 和 2 之间的内存增加:

但是在 VisualVM 中的第 3 点停止方法 «populateList()» - 堆存储库仍未处理。但是,如果您不考虑第 2 行中的 «static»,它将更改内存值:

在这种情况下,在使用方法«populateList()»进行操作后,堆内存被收集器清理,因为对对象的所有引用都被停用:

2.参考外部的内部类的可用性

要初始化静态类,您总是需要来自外部类的示例。每个非静态默认类都包含对保存它的类的隐藏引用。如果你使用一个内部类对象——它不会被 GC 收集,即使你关闭了一个外部类对象。 

例如,采用一个包含非静态值并引用多个体积元素的类。在这种情况下,内部类对象的创建有这样的统计指标:

将内部或匿名类的值平庸地更改为静态,内存值将发生根本性的变化。这可能是由于内部类的内容对外部对象的引用,从而阻塞了 GC 的主要功能。

3. 应用内未关闭的资源

每当我们创建新连接或打开线程时,JVM 总是为新线程或连接分配内存空间。此类连接可能具有带有综合数据库的会话对象。

如果我们不关闭这些资源,我们就有可能让存储被阻塞。这会产生垃圾收集器无法检测和删除这些对象的风险。如果您忽略保持稳定和正确关闭资源,它们将填满内存,直至出现错误«Java Out Of Memory Error。»

4. 使用 finalize() 方法

在该方案中,当我们有一个类重新定义了 finalize() 方法时,它的对象可能不会及时收集 GC,而是会排队等待删除。此外,finalize() 中错误覆盖的代码及其与垃圾收集器的速度不匹配可能导致错误 «OutOfMemoryError。»

例如,采用一个具有重新定义的方法 finalize() 的类,并且需要时间。如果你有大量被 GC 收集的元素,就会形成这样的堆分数:

using_finalize_methods

如果你简单地移除一个重新定义的 finalize(),你可以得到这个值:

using_finalize_methods_2

5. 使用内部字符串

在 Java 系统版本 6 中,必须小心使用体积字符串。版本 7 将字符串池的更改从 PermGen 移到了 HeapSpace。如果您调用方法 «intern()» 来读取大字符串 - 它会保存到常量内存 ( PermGen ) 中的字符串池中。此方法存储在 PermGen 中直到会话结束,这会导致应用程序内存不足。

永久内存的一个例子是在读取没有其国际文件的字符串时:

using_interned_strings

6. ThreadLocals 参与度

ThreadLocal – 一种通过关闭其变量的值来创建流安全性的工具。同时,所有线程都有一个指向重复 ThreadLocal 变量的隐藏链接,并保存它们的副本,而不是在所有线程中使用资源。

除了有用之外,此功能还存在错误。误用时会影响泄漏。

ThreadLocal 变量必须在删除包含它们的线程后由 GC 收集。但是,某些系统服务器可能无法正确使用此功能。这可能是因为服务器没有为所有请求创建一个新线程,而是应用了整个线程池。 

服务器中的池重用线程,使垃圾收集器无法访问它们并占用内存。

7. 实现不正确的equals()和hashCode()

在创建新类时,我们经常会遇到 equals() 和 hashCode() 方法的覆盖错误。HashSet 和 HashMap 常用此类方法,如果它们包含覆盖错误,则会导致内存消耗过多。

例如,你可以使用ORM Hibernate,它采用 equals() 和 hashCode() 方法来处理项目并将它们存储在缓存中。如果这些方法没有被覆盖,Hibernate 将不会分析这些项目,并且缓存将填充它们的副本,从而导致内存泄漏。

症状和 Java 内存泄漏排查

1.内存泄漏症状

有几个可疑点可能表明存在泄漏:

  • 持续的和不可预见的系统故障; 
  • 不稳定的应用功能支持;
  • 长时间会话期间发生错误«Java.lang.OutOfMemoryError»;
  • 系统移除连接对象;
  • 显着降低整体系统性能。

2.如何检测Java内存泄漏

为了检测泄漏,需要多种工具和技术,以及它们的组合。有一个受信任方法的列表:

2.1。内存分析器——用于跟踪在存储库中占用空间的文件和项目的工具。他们能够检测泄漏并分析系统中使用的元素的正确分布。此外,估计处理时间。 

用于分析 Java 内存的最常用工具: 

  • EJ-Technologies 的JProfiler;
  • 甲骨文公司的Java VisualVM;
  • YourKit GmbH 的 YourKit。

2.2. 参与堆转储这是一个用于在 Java 内存存储中创建堆的即时和及时快照的工具。需要这些图像来控制使用的对象数量及其在内存中的权重。此外,该工具还跟踪系统创建的元素数量以及可能影响泄漏的因素。

?

 

让我们更深入地了解Java 与 .Net之间的区别,以及它们各自的优缺点。

2.3. 激活详细垃圾收集日志。该工具能够演示对存储库和 GC 中堆配置的更改。它提供了应用程序最准确的功能和性能。并通过识别堆中合适的元素、其替代方法和 JVM 参数来优化收集器的性能。 

例如,使用 JVM 启动方法在 app 中激活详细集合:

«-XX: +UseSerialGC -Xms1024m -Xmx1024m -verbose:gc»

«-verbose:gc» 参数激活收集信息的记录。默认情况下,日志保存到标准输出并为所有 GC 生成行。还可以使用 «-XX: +UseSerialGC» 参数指定顺序 GC,并设置堆大小。

这有助于及时检测泄漏并配置应用程序运行状况指标。

可以在 GitHub 服务上找到 GC 日志激活的示例。

如何修复内存泄漏?

为了解决这个问题,首先要考虑它出现的原因。因此,有必要识别泄漏的类型并根据其种类解决问题。对于每种类型,都有不同的方案来修复 Java 中的内存错误:

1.如何解决内存错误?

1.1。使用静态变量时:

尽量减少系统中静态字段的使用。在这种情况下,您可以使用 «lazy» 而不是紧急加载对象。

1.2. 如果有内部类,参考外部:

如果不需要外部类元素,您可以将内部类转换为静态类。

1.3. 如果应用程序的资源没有关闭:

«finally» 应及时激活以完成资源的使用。 

1.4. 使用 finalize() 方法时:

应将与决赛选手的任何工作减少到零。

1.5。使用内部字符串时

尝试将 Java 应用程序升级到最新版本。这可以通过在第 6 个版本之后将字符串池移动到堆的空闲位置来实现。并且为了避免错误« Out Of Memory Error »,在使用批量字符串时,您可以扩展«PermGen»大小。

1.6. 使用 ThreadLocals 时

在不需要时稳定地清理 ThreadLocal 变量。具有 remove() 属性的 ThreadLocal – 删除所有当前线程的变量值。它必须在 «finally» 块中关闭以确保它被停用。

1.7. 当实现不正确的 equals() 和 hashCode()

创建新元素时,最好覆盖 equals() 和 hashCode() 路径。

2.其他消除内存泄漏的方法

当对内存泄漏的发生原因没有清楚的了解时,您还可以应用其他方法来对抗内存泄漏。 

2.1。使用基准测试:

要详细分析 Java 中的代码性能,您可以将其测试与基准测试结合使用。通过这种方式,通过比较它们的有效性,可以很容易地评估执行任务的不同方式的生产力。这优先考虑最佳实践,这将避免不必要的内存消耗。

2.2. 参考对象应用程序:

在 Java 中应用特殊引用对象而不是直接引用应用程序中的元素的选项。使用包«java.lang.ref»中包含的此类链接允许垃圾收集器简单地删除多余的对象。

2.3. 代码审查:

平庸,但在某些情况下同样有效的方法——检查代码。有时它有助于消除内存泄漏。

2.4. 启用分析:

通过使用上述方法,您可以发现可用于存储应用程序资源的有用存储区域。

2.5. 详细垃圾收集:

前面提到的另一种方式。包括这种模式,我们可以密切监控GC的工作。

遵守此类存储空间规则及其合理使用有助于消除造成 Java 备份编程和 Java 备份语言激增的情况。这是整个系统正常运行的基础。

3. 删除工具出错:

作为使用 Java 的专家报告,在门户网站 Habr上– 引用:

“找到并修复泄漏后,再次执行所有步骤,分析内存,并向 内存分析器报告以确保修复有所帮助,这并不是多余的。”

为了清楚地了解 Java 中的内存错误,可以在此处找到更多信息:

 

视频名称

 

 

结论

因此,分析这个问题——Java 中的内存泄漏时,它就像影响系统的一种疾病,它会感染系统并恶化其整体性能。如果不进行治疗,可能会导致无法挽回的后果。

如果出现很久之后才发现该问题,那就不太容易解决。没有什么“灵丹妙药”可以一次性修复泄漏,即使涉及到 Java 编程方面的高素质专家的工作也是如此。 

所以平时的监控,分析,定位就变得更加重要,如果你有好的案例,欢迎分享。

作者:apassov Meiir, Java DEV

点赞收藏
willberthos

keep foolish!

请先登录,查看5条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

浅析AbstractQueuedSynchronizer

浅析AbstractQueuedSynchronizer

10
5