OutOfMemoryError系列: Java heap space错误深度解析

本文包括JVM内存管理、错误产生的原因、内存泄漏的代码示例,最后还会介绍怎么解决这些问题,特别会提到一些性能诊断工具,让你快速的知道问题发生的根本原因。

java.lang.OutOfMemoryError:Java heap space

java的应用程序只被允许使用限定好的memory。在java的application启动的时候,这个内存大小就被规定好了。为了让内存管理更加的智慧,java的memory被分为了两个不同的区域。这两个区域分别被叫做heap space和Permanent Generation(很多书中叫“永久代”)。

这两个区域的大小在JVM启动的时候就被设置好了,是通过两个JVM参数-Xmx and -XX:MaxPermSize来设置的,如果你不专门去设置这两个的大小,那么将会使用平台定义好的默认值。

java.lang.OutOfMemoryError:java heap space 这个错误是在什么情况下发生呢?就是当你的application想要往heap那个空间里添加更多的数据的时候,但heap里却没有足够的空置区域的时候就会发生java heap space错误。

请注意,也许还有足够的物理内存,但是当JVM达到heap的大小限制时,就会抛出java.lang.OutOfMemoryError: Java heap space 错误。

怎么引起的?

java.lang.OutOfMemoryError这个错误的产生最常见的原因其实很简单:就是你的应用程序需要更多的 heap 空间。引起这个错误的其他原因就比较复杂了,可能是因为你的编程错误引起的,比如下面两种情况:

  • 使用/数据量的峰值。该应用程序旨在处理一定数量的用户或一定量的数据。当用户数量或数据量突然突增并超过预期阈值时,在尖峰停止操作之前正常运行的操作会触发java.lang.OutOfMemoryError:Java heap space 错误。
  • 内存泄漏(Memory leaks)。特定类型的编程错误将导致您的应用程序不断消耗更多的内存。每次应用程序的那个带有泄漏问题的函数被调用的时候,它就会将一些对象留在Java heap space中。随着时间的推移,那些被泄漏的对象会消耗掉所有可用的Java heap space,并触发这个你已很熟悉的java.lang.OutOfMemoryError:Java heap space 错误。

上代码 简单的例子:

第一个例子很简单 - 下面的Java代码试图分配一个2M整数的数组。 当你编译它并使用12MB的Java堆空间(java -Xmx12m OOM)启动时,它会失败,并返回java.lang.OutOfMemoryError:Java heap space 消息。 使用13MB的Java堆空间,程序就运行正常了。

class OOM {
  static final int SIZE=2*1024*1024;
  public static void main(String[] a) {
    int[] i = new int[SIZE];
   }
}

内存泄露的例子:

第二个更现实的例子是内存泄漏。在Java中,当开发人员创建和使用新对象new Integer(5),他们不必自己分配内存 - 这是由Java虚拟机(JVM)来处理。在应用程序的生命周期中,JVM会定期检查内存中的哪些对象仍在使用,哪些不是。未使用的对象可以被丢弃,内存被回收并再次使用。这个过程称为垃圾收集。 JVM中处理集合的相应模块称为垃圾回收器(GC)。

Java的自动内存管理依赖于GC定期查找未使用的对象并删除它们。简化一点我们可以说,Java中的内存泄漏是一种情况,其中一些对象不再由应用程序使用,但垃圾收集无法识别它。因此,这些未使用的对象将无限期地保留在Java堆空间中。这个堆积将最终触发java.lang.OutOfMemoryError:Java heap space 错误。

构造一个满足内存泄漏定义的Java程序是相当容易的,像下面这样:

class KeylessEntry {
 
   static class Key {
      Integer id;
 
      Key(Integer id) {
         this.id = id;
      }
 
      @Override
      public int hashCode() {
         return id.hashCode();
      }
   }
 
   public static void main(String[] args) {
      Map m = new HashMap();
      while (true)
         for (int i = 0; i < 10000; i++)
            if (!m.containsKey(new Key(i)))
               m.put(new Key(i), "Number:" + i);
   }
}

当你执行上面的代码,你也许希望它一直没问题的运行下去,并且认为这个缓存方案仅仅会增加到10000个key。然而事实是 key会一直被增加,因为Key这个class里边并没有包含equals()的实现,只是override了hashCode()方法。

最后,随着时间的推移,随着泄漏代码的不断使用,“缓存”结果最终消耗了大量的Java堆空间。 当泄漏的内存填满堆区域中的所有可用内存,并且Garbage Collection无法清除它时,会抛出java.lang.OutOfMemoryError:Java heap space 。

要想内存不再泄漏,其实办法很简单-就是加上 equals()方法的实现,像下面这样:

@Override
public boolean equals(Object o) {
   boolean response = false;
   if (o instanceof Key) {
      response = (((Key)o).id).equals(this.id);
   }
   return response;
}

怎么解决呢?

在某些情况下,你分配给JVM的heap数量不足以满足在该JVM上运行的应用程序的需求。 在这种情况下,你只需要分配更多的heap就可以了 - 请参见本章末尾如何实现。

然而,在许多情况下,提供更多的Java堆空间不会解决问题。 例如,如果您的应用程序包含内存泄漏,添加更多堆将只是推迟java.lang.OutOfMemoryError:Java heap space 错误。 此外,增加Java堆空间量也会增加影响应用程序吞吐量或延迟的GC暂停时间。

如果你想解决Java堆空间的根本问题,而不是掩盖症状,你需要找出那些分配了最多的内存的那些代码。 换句话说,你需要回答这些问题:

1.哪些对象占用了heap的大部分?

2.这些对象分布在源码中的哪个地方?

此时,你要花上一些时间来解决这些问题了。这里是一个粗略的过程大纲,将帮助您回答上述问题:

获取安全许可,以便从JVM中把heap中的内容dump出来存储到另外一个地方。 “Dumps”基本上就是堆内容的一个快照,你稍后就是在这些dumps中进行分析。由于这些快照可能包含机密信息,例如密码,银行卡号等,所以你必须要获得安全部门的允许。

选择在一个合适的时间进行dump(转储)操作。如果时机不对,堆垃圾可能包含大量的噪音,甚至可能几乎没有什么有用的信息。另一方面,每个堆的dump(转储)会完全“冻结”JVM,这样会占用过多的JVM,这种情况下很可能会影响正常业务的访问,会出现一些性能问题。

专门找一台机器用作dump(转储)。当你要分析8GB的堆,那你就要一台超过8GB的机器来分析堆内容。然后选择一个转储分析软件(我们建议使用Eclipse MAT,你也可以选择其他转储分析软件)。

检测出堆的最大消费者的GC根的路径。我们在这里单独的一篇文章中介绍了这项活动。

接下来,你需要确定源代码中的哪些位置正在分配潜在危险的大量对象。如果你对应用程序的源代码很了解的话,你将能够在几次搜索中做到这一点。

或者,我们建议你使用Plumbr,这是一个jvm性能检测工具,它可以自动检测出问题的根本原因。 在其他性能问题上,它同样可以捕获所有java.lang.OutOfMemoryErrors,并且可以自动的为你整理出那些非常“内存饥饿”(memory-hungry)的数据结构的信息。

Plumbr会在幕后为你收集必要的数据 - 包括有关堆使用的相关数据(只有对象布局图,没有实际数据),以及一些在“堆转储”(heap dump)中找不到的数据。 如果JVM遇到java.lang.OutOfMemoryError,Plumbr还会为您执行必要的数据处理 。 下面是一个Plumbr的有关java.lang.OutOfMemoryError错误的一个例子(分析结果):

没有任何额外的工具或分析,你就可以看到:

  • 哪些对象消耗的内存最多(271个com.example.map.impl.PartitionContainer实例在248MB的总堆中消耗173MB)
  • 这些对象是在哪个类中被分配的?(大多数在MetricManagerImpl类中分配,行304)
  • 当前引用这些对象的完整引用链(GC引用的完整引用链)

有了这些信息,您就可以找到问题的根本原因了,然后把数据结构修剪到它们适合在您的内存池中的级别。

但是,当您从内存分析或阅读Plumbr报告得出的结论是内存使用是合法的,那么就没必要修改源代码了,这时候你就要设置更大的java heap 空间来保证应用程序的运行了。 请更改JVM启动配置,并添加(或增加值,如果存在)以下内容:

-Xmx1024m

上面的配置将给应用程序1024MB的 Java heap space。 您可以使用g或G表示GB,m或M表示MB,k或K表示KB。 例如,下面的几种方式都表示最大Java heap space为1GB:

    java -Xmx1073741824 com.mycompany.MyClass
    java -Xmx1048576k com.mycompany.MyClass
    java -Xmx1024m com.mycompany.MyClass
    java -Xmx1g com.mycompany.MyClass

原文发布于微信公众号 - ImportSource(importsource)

原文发表时间:2016-10-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏腾讯Bugly的专栏

内存泄露从入门到精通三部曲之排查方法篇

1 最原始的内存泄露测试 重复多次操作关键的可疑的路径,从内存监控工具中观察内存曲线,是否存在不断上升的趋势且不会在程序返回时明显回落。 这种方式可以发现最基本...

42314
来自专栏IT技术精选文摘

从Java视角理解系统结构(三)伪共享

从我的前一篇博文中, 我们知道了CPU缓存及缓存行的概念, 同时用一个例子说明了编写单线程Java代码时应该注意的问题. 下面我们讨论更为复杂, 而且更符合现实...

2167
来自专栏坚毅的PHP

进程、线程、轻量级进程、协程和go中的Goroutine 那些事儿

电话面试被问到go的协程,曾经的军伟也问到过我协程。虽然用python时候在Eurasia和eventlet里了解过协程,但自己对协程的概念也就是轻量级线程,还...

4953
来自专栏IMWeb前端团队

给react加try-catch

最近在一个使用fis构建的react.js项目里遇到个问题,render函数里如果发生了运行时错误,比如说某个对象没有判断就直接去访问其属性,那我所知道的就是,...

5825
来自专栏Java学习网

今天说说烦人的Java内存溢出问题

作为一个开发人员最不想看到的就是BUG,可见性的问题可能还不是最关键的,至少我们可以找到问题,很快解决,一般BUG也不会重复出现;但今天要学习的内存溢出就不一样...

25510
来自专栏谈补锅

汇编语言学习

   7、1Byte = 8bit ;    1KB = 1024B ;  1MB = 1024KB ;   1GB = 1024MB

3943
来自专栏QQ会员技术团队的专栏

从 0 实现一个延迟代理服务

部门会定期进行容灾演习,也期望能够验证到各个服务的\"最差服务能力\"。即验证被调出现较高延迟或者过载的时候,主调的服务能力是否符合预期。要想做这种演习,其核心...

2392
来自专栏大内老A

《WCF技术剖析》博文系列汇总[持续更新中]

近半年以来,一直忙于我的第一本WCF专著《WCF技术剖析(卷1)》的写作,一直无暇管理自己的Blog。在《WCF技术剖析(卷1)》写作期间,对WCF又有了新的感...

1768
来自专栏魏琼东

一步一步教你使用AgileEAS.NET基础类库进行应用开发-基础篇-ORM访问器及其配置

系列回顾          本系列前面有三篇文章介绍和演示了AgileEAS.NET平台ORM组件的开发流程及其常见的使用方式,通过前面的三篇文章,大家都可以正...

2019
来自专栏北京马哥教育

Python缓存神奇库cacheout全解

python的缓存库(cacheout) 链接: 项目: https://github.com/dgilland/cacheout 文档地址: https:/...

4405

扫码关注云+社区

领取腾讯云代金券