4年前 (2020-11-25)  jvm |   抢沙发  547 
文章评分 0 次,平均分 0.0

Java应用程序内存泄漏outofmemory简介

Java的核心优点之一是JVM,它是一种现成的内存管理。本质上,我们可以创建对象,Java垃圾收集器将负责为我们分配和释放内存。

然而,在Java应用程序中仍然会发生outofmemory内存泄漏。

在本文中,我们将描述最常见的java内存泄漏,了解其原因,并介绍一些检测/避免它们的技术。在本文中,我们还将使用java yourkit 探查器来分析运行时内存的状态,进而知道outofmemory怎么解决。

什么是Java内存泄漏?

内存泄漏的标准定义是当应用程序不再使用对象,但垃圾回收器无法将它们从工作内存中删除时发生的情况,因为它们仍在被引用。结果,应用程序消耗越来越多的资源,最终导致致命的 OutOfMemoryError

为了更好地理解这个概念,这里有一个简单的图:

java内存泄露

如我们所见,我们有两种类型的对象-引用未引用;垃圾收集器可以移除未引用的对象。引用的对象不会被收集,即使它们实际上不再被应用程序使用。

检测内存泄漏可能很困难。许多工具执行静态分析以确定潜在的泄漏,但这些技术并不完美,因为最重要的方面是运行系统的实际运行时行为。

因此,让我们通过分析一些常见的场景,重点了解一些防止内存泄漏的标准实践。

Java堆泄漏

在最初的部分中,我们将重点讨论经典的内存泄漏场景——在这种情况下,Java对象是连续创建而不被释放的。

了解这些情况的一个有利的技术是通过设置较小的堆大小来更容易地再现内存泄漏。这就是为什么在启动应用程序时,我们可以调整JVM以满足内存需求:

-Xms<size>
-Xmx<size>

这些参数指定初始Java堆大小以及最大堆大小。

保持静态对象的引用

第一个可能导致Java内存泄漏的场景是使用静态字段引用重对象。

让我们看一个简单的例子:

private Random random = new Random();
public static final ArrayList<Double> list = new ArrayList<Double>(1000000);

@Test
public void givenStaticField_whenLotsOfOperations_thenMemoryLeak() throws InterruptedException {
    for (int i = 0; i < 1000000; i++) {
        list.add(random.nextDouble());
    }
    
    System.gc();
    Thread.sleep(10000); // to allow GC do its job
}

我们将ArrayList创建为一个静态字段——在JVM进程的生命周期内,JVM垃圾回收器将永远不会收集该字段,即使在计算完成之后也是如此。我们还调用了线程睡眠(10000)允许GC执行完全收集并尝试回收所有可以回收的内容。

让我们分析JVM的测试并运行它:

java内存泄露

注意,在一开始,所有的内存当然是free的。

然后,在2秒钟内,迭代过程运行并完成——将所有内容加载到列表中(这自然取决于运行测试的机器)。

之后,将触发一个完整的垃圾回收周期,并继续执行测试,以允许此周期时间运行并完成。如您所见,列表不会被回收,内存消耗也不会下降。

现在让我们看一个完全相同的例子,只是这一次,ArrayList没有被静态变量引用。相反,它是一个局部变量,它被创建、使用然后丢弃:

@Test
public void givenNormalField_whenLotsOfOperations_thenGCWorksFine() throws InterruptedException {
    addElementsToTheList();
    System.gc();
    Thread.sleep(10000); // to allow GC do its job
}
    
private void addElementsToTheList(){
    ArrayList<Double> list = new ArrayList<Double>(1000000);
    for (int i = 0; i < 1000000; i++) {
        list.add(random.nextDouble());
    }
}

一旦该方法完成了它的工作,我们将观察到主要的GC收集,大约在下图的第50秒处:

java内存泄露

请注意GC现在如何能够回收JVM使用的一些内存。

outofmemory怎么解决?

既然您了解了这种情况,当然有一些方法可以防止它发生。

首先,我们需要密切关注static的用法;将任何集合或重对象声明为static会将其生命周期与JVM本身的生命周期联系起来,并使整个对象图无法收集。

我们还需要了解一般的集合——这是一种常见的方法,无意中将引用保留的时间超过了我们需要的时间。

在长字符串上调用String.intern()

第二组经常导致内存泄漏的场景涉及字符串操作,特别是 String.intern() API。

让我们看一个简单的例子:

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

在这里,我们只需尝试将一个大型文本文件加载到运行内存中,然后使用.intern()返回规范格式。

intern API会将str字符串放在JVM内存池中(在那里它无法被收集),这将再次导致GC无法释放足够的内存:

java内存泄露

我们可以清楚地看到,在前15秒JVM是稳定的,然后我们加载文件,JVM执行垃圾收集(第20秒)。

最后str.intern()被调用,这会导致内存泄漏-这是一条稳定的线,表示堆内存使用率很高,永远不会释放。

outofmemory怎么解决?

请记住,interned String对象存储在PermGen空间中–如果我们的应用程序要对大型字符串执行大量操作,我们可能需要增加永久生成的大小:

-XX:MaxPermSize=<size>

第二种解决方案是使用Java 8,其中PermGen空间被元空间取代,在字符串上使用intern时不会导致任何OutOfMemoryError

java内存泄露

最后,还有几个避免在字符串上使用.intern()API的选项。

未关闭的流Streams

忘记关闭流是一个非常常见的场景,当然,也是大多数开发人员都能想到的。在Java7中,自动关闭的功能部分地消除了这个问题 try with resource子句中引入了所有类型的流。

为什么部分?因为try with resources语法是可选的:

@Test(expected = OutOfMemoryError.class)
public void givenURL_whenUnclosedStream_thenOutOfMemory()
  throws IOException, URISyntaxException {
    String str = "";
    URLConnection conn 
      = new URL("http://norvig.com/big.txt").openConnection();
    BufferedReader br = new BufferedReader(
      new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
    
    while (br.readLine() != null) {
        str += br.readLine();
    } 
    
    //
}

让我们看看从URL加载大文件时应用程序的内存:

java内存泄露

如我们所见,堆的使用随着时间的推移逐渐增加,这是由于不关闭流而导致的内存泄漏的直接影响。

让我们更深入地研究一下这个场景,因为它不像其他场景那么清晰。从技术上讲,未关闭的流将导致两种类型的泄漏:

  • 低级资源泄漏
  • 内存泄漏

低级资源泄漏只是操作系统级资源的泄漏,例如文件描述符、打开的连接等。这些资源也可能泄漏,就像内存一样。

当然,JVM也使用内存来跟踪这些底层资源,这也是导致jvm内存泄漏的原因。

outofmemory怎么解决?

我们始终需要记住手动关闭流,或者使用Java 8中引入的自动关闭功能:

try (BufferedReader br = new BufferedReader(
  new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
    // further implementation
} catch (IOException e) {
    e.printStackTrace();
}

在这种情况下,BufferedReader将在try语句的末尾自动关闭,而不需要在显式finally块中关闭它。

未关闭的连接

这个场景与前一个场景非常相似,主要区别在于处理未关闭的连接(例如到数据库、到FTP服务器等)。同样,不正确的实现会造成很大的危害,导致内存问题。

让我们看一个简单的例子:

@Test(expected = OutOfMemoryError.class)
public void givenConnection_whenUnclosed_thenOutOfMemory()
  throws IOException, URISyntaxException {
    
    URL url = new URL("ftp://speedtest.tele2.net");
    URLConnection urlc = url.openConnection();
    InputStream is = urlc.getInputStream();
    String str = "";
    
    //
}

URLConnection保持打开状态,结果是内存泄漏:

java内存泄露

请注意,垃圾回收器如何无法执行任何操作来释放未使用但被引用的内存。这种情况在第一分钟后就很明显了——GC操作的数量迅速减少,导致堆内存使用增加,从而导致OutOfMemoryError

outofmemory怎么解决?

这里的答案很简单——我们需要始终注意记得关闭连接。

将没有 hashCode() 和 equals() 的对象添加到哈希集中

可能导致内存泄漏的一个简单但非常常见的示例是将哈希集与缺少hashCode()equals()实现的对象一起使用。

特别是,当我们开始向一个集合中添加重复的对象时,它只会增长,而不是忽略它应该忽略的重复。我们也无法删除这些对象,一旦添加。

让我们创建一个没有equals或hashCode的简单类:

public class Key {
    public String key;
    
    public Key(String key) {
        Key.key = key;
    }
}

现在,让我们看看这个场景:

@Test(expected = OutOfMemoryError.class)
public void givenMap_whenNoEqualsNoHashCodeMethods_thenOutOfMemory()
  throws IOException, URISyntaxException {
    Map<Object, Object> map = System.getProperties();
    while (true) {
        map.put(new Key("key"), "value");
    }
}

这个简单的实现将在运行时导致以下场景:

java内存泄露

请注意垃圾回收器是如何在1:40左右停止回收内存的,并注意内存泄漏;之后GC收集的数量几乎下降了四倍

outofmemoryerror怎么解决?

在这些情况下,解决方案很简单–提供hashCode()equals()实现非常重要。

这里值得一提的一个工具是project lombok,它通过注解提供了很多默认实现,例如@EqualsAndHashCode

如何在应用程序中查找泄漏源

诊断内存泄漏是一个漫长的过程,需要大量的实践经验、调试技巧和应用程序的详细知识。

让我们看看除了标准评测之外,还有哪些技术可以帮助您。

详细的垃圾收集

识别内存泄漏的最快方法之一是启用详细的垃圾回收。

通过添加-verbose:gc参数对于应用程序的JVM配置,我们启用了对GC的非常详细的跟踪。摘要报告显示在默认的错误输出文件中,这将有助于您了解内存的管理方式。

进行分析

第二种技术是我们在本文中一直在使用的技术,这就是评测。最流行的剖析器是visualvm,这是一个很好的地方,可以让您从命令行JDK工具转向轻量级评测。

在本文中,我们使用了另一个分析器YourKit,它与visualvm相比有一些额外的、更高级的特性。

检查代码

最后,这更像是一种通用的良好实践,而不是处理内存泄漏的特定技术。

简单地说,彻底检查代码,定期进行代码检查,并充分利用静态分析工具来帮助您理解代码和系统。

结论

在本篇文章中,我们实际了解了JVM上内存泄漏是如何发生的。了解这些情况是如何发生的是处理这些情况的第一步。

然后,拥有技术和工具来真正了解在运行时发生了什么,当泄漏发生时,也是至关重要的。静态分析和仔细的代码重点审查只能做这么多,而且——归根结底,是运行时向您展示代码中无法立即识别的更复杂的泄漏。

最后,泄漏是出了名的很难发现和重现的,因为很多泄漏只发生在高负荷下,这通常发生在生产中。这是您需要超越代码级分析的地方,并在两个主要方面进行工作—复制和早期检测。

再现内存泄漏的最佳和最可靠的方法是在一套良好的性能测试的帮助下,尽可能地模拟生产环境的使用模式。

早期检测是可靠的性能管理解决方案,甚至是早期检测解决方案可以产生重大影响的地方,因为这是了解生产中应用程序运行时的唯一方法。

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/883.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册