JVM记一次堆内存溢出OutOfMemoryError: Java heap space

项目场景:

前段时间现场报了一个内存溢出的OutOfMemoryError: Java heap space的问题,这个问题十分典型,所以记录下来,希望能帮助到看这篇文章的朋友


问题描述:

问题项目: 这个问题出现在省网的BOSS系统, 这是一个BS架构的系统, 前台使用的Spring Rich Client 也叫Spring胖客户端,Spring Richclient 是一个基于 Spring 的企业级富客户端开发框架,使用该框架,可以轻易构建出具有良好可维护性和可扩展性的企业级 Swing 应用。

问题频次: 经过和现场售后人员沟通,发现问题是偶现现象。只有个别操作员反馈有问题而90%的操作员都反馈操作过程中没有异常现象 

问题日志:从服务器上日志发现日志中有明显的报错信息, 日志中显示OutOfMemoryError: Java heap space:

org.springframework.remoting.RemoteAccessException: Could not access HTTP invoker remote service at http://XXX.XX.XX.XXX:8080/boss/invoke;

nested exception is java.lang.OutOfMemoryError: Java heap space


原因分析:

       Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”,从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等

     从日志上分析OutOfMemoryError: Java heap space异常表示堆内存溢出,堆内存溢出最常出现在两种情况

  1. 程序中已经分配的内存由于某种原因不能释放引起的内存泄露导致内存溢出
  2. 占用内存增加但无法申请到足够的内存空间,空间不足导致内存溢出

从现场反馈中得知“90%的操作员都反馈操作过程中没有异常现象” ,怀疑这个问题和操作员的操作有关,某种操作下会引堆内存发生巨大变化。使用 jvisualvm 查看客户端虚拟机信息变化(jvisualvm  是JDK自带的监控成都新可以监控本地的Java虚拟机也可以监控远程服务),观察一段时间后发现堆内存使用出现了明显上升,表明某个操作创建了大量的产品对象占用的对内存的大量空间

 

通过Dump操作发现有很多产品对象,通过这个信息发现当客户端打开产品管理很套餐管理后通过客户端再给客户做受理操作的时候出现堆内存溢出的情况比较多,两个界面分别显示可使用的产品或者套餐信息,当选中某条产品记录或者套餐记录的时候界面也显示其详情。这个时候分析“产品管理”和“套餐产品管理”的程序代码 发现程序中存在设计问题,

开发人员设计的时候一次性查询出了所有产品列表和产品详情返回给客户端,然后界面选择产品后联动显示详情,分析代码和数据历史, 10年前系统只有几百产品,开发人员用这种设计方法提高了界面联动速度,但是现在再次查询数据库发现数据库里的产品信息已经高到5W。一次处理将5W产品的信息和详情返回给客户端并展示在界面上,这个操作导致堆内存被大量占用并已经块到临界点, 一些其他操作处理时无法申请到足够的空间,导致内存溢出报错


解决方案:

本问题遇到的问题属于第二种情况,内存使用量过大,虚拟机扩展内存的时候已经达到最大值,新的操作无法申请到足够的内存导致内存溢出抛出OutOfMemoryError异常, JAVA虚拟机配置中-Xms 表示初始的堆大小,-Xmx表示最大堆大小,当空余空间小于40%的时候JVM就会增加堆内存大小直到-Xmx最大限制,相反当空余空间大于70%的时候JVM会减少堆直到-Xms 的大小,因此当发现堆内存不足抛出 OutOfMemoryError: Java heap space 异常的时候如果服务器内存足够可以增加-Xmx 配置的值来解决问题,小桶不能装下所有的水,换个大桶来装, 这种方法简单粗暴,虽然能解决问题,但我不建议此方法,更好的做法是先分析问题,找到最优的解法,

产品处理的过程中设计的交互有明显问题,开发人员没有考虑到系统数据的增量变化,使用了一次交互全数据查询的方式,显然这个设计方式不合理, 更好的做法是采用2次交互,先查询出产品信息,当选中产品信息后再去查询产品的详情信息,减少对象创建引起不必要的对内存占用


文章总结:

01_01_java-heap-space.png

假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发java.lang.OutOfMemoryError: Java heap space 错误

例如:

1.内存泄露(Memory leak):程序中存在问题,造成内存泄露,内存被一点点占用但又不释放,最后申请内存时,内存不足

2.超出预计的访问量或者数据量:服务器设计都会考虑容量,也就是部署几台机器来满足多少访问量,或者说满足多少数据处理,当业务量突然变更时,超出预计的访问量时候,也会出现溢出问题。此时考虑扩容等方式处理

堆内存溢出常见场景

  • 长生命周期的对象持有短生命周期对象的引用,这是内存泄露最常见的场景,也是代码设计中经常出现的问题,本文中的问题场景就属于这一类
  • 当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中删除当前对象,造成内存泄露。

  • 机器的连接数和关闭时间设置,长时间开启非常耗费资源的连接,也会造成内存泄露。


附录

JVM参数的含义  官网:Java HotSpot VM Options

参数名称

含义

默认值

-Xms

的等级高或者包路径匹配不对

物理内存的1/64(

默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.

-Xmx

最大堆大小

物理内存的1/4(

默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制

-Xmn

年轻代大小(1.4or lator)

注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。

整个堆大小=年轻代大小 + 年老代大小 + 持久代大小.

增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8

-XX:NewSize

设置年轻代大小(for 1.3/1.4)

-XX:MaxNewSize

年轻代最大值(for 1.3/1.4)

-XX:PermSize

设置持久代(perm gen)初始值

物理内存的1/64

-XX:MaxPermSize

设置持久代最大值

物理内存的1/4

-Xss

每个线程的堆栈大小

JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右

一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)

和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"”

-Xss is translated in a VM flag named ThreadStackSize”

一般设置这个值就可以了。

-XX:ThreadStackSize

Thread Stack Size

(0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.]

-XX:NewRatio

年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)

-XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5

Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。

-XX:SurvivorRatio

Eden区与Survivor区的大小比值

设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10

-XX:LargePageSizeInBytes

内存页的大小不可设置过大, 会影响Perm的大小

=128m

-XX:+UseFastAccessorMethods

原始类型的快速优化

-XX:+DisableExplicitGC

关闭System.gc()

这个参数需要严格的测试

-XX:MaxTenuringThreshold

垃圾最大年龄

如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率

该参数只有在串行GC时才有效.

-XX:+AggressiveOpts

加快编译

-XX:+UseBiasedLocking

锁机制的性能改善

-Xnoclassgc

禁用垃圾回收

-XX:SoftRefLRUPolicyMSPerMB

每兆堆空闲空间中SoftReference的存活时间

1s

softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap

-XX:PretenureSizeThreshold

对象超过多大是直接在旧生代分配

0

单位字节 新生代采用Parallel Scavenge GC时无效

另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.

-XX:TLABWasteTargetPercent

TLAB占eden区的百分比

1%

-XX:+CollectGen0First

FullGC时是否先YGC

false

并行收集器相关参数

-XX:+UseParallelGC

Full GC采用parallel MSC

(此项待验证)

选择垃圾收集器为并行收集器.此配置仅对年轻代有效.即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集.(此项待验证)

-XX:+UseParNewGC

设置年轻代为并行收集

可与CMS收集同时使用

JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值

-XX:ParallelGCThreads

并行收集器的线程数

此值最好配置与处理器数目相等 同样适用于CMS

-XX:+UseParallelOldGC

年老代垃圾收集方式为并行收集(Parallel Compacting)

这个是JAVA 6出现的参数选项

-XX:MaxGCPauseMillis

每次年轻代垃圾回收的最长时间(最大暂停时间)

如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值.

-XX:+UseAdaptiveSizePolicy

自动选择年轻代区大小和相应的Survivor区比例

设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开.

-XX:GCTimeRatio

设置垃圾回收时间占程序运行时间的百分比

公式为1/(1+n)

-XX:+ScavengeBeforeFullGC

Full GC前调用YGC

true

Do young generation GC prior to a full GC. (Introduced in 1.4.1.)

CMS相关参数

-XX:+UseConcMarkSweepGC

使用CMS内存收集

测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明.所以,此时年轻代大小最好用-Xmn设置.???

-XX:+AggressiveHeap

试图是使用大量的物理内存

长时间大内存使用的优化,能检查计算资源(内存, 处理器数量)

至少需要256MB内存

大量的CPU/内存, (在1.4.1在4CPU的机器上已经显示有提升)

-XX:CMSFullGCsBeforeCompaction

多少次后进行内存压缩

由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理.

-XX:+CMSParallelRemarkEnabled

降低标记停顿

-XX+UseCMSCompactAtFullCollection

在FULL GC的时候, 对年老代的压缩

CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。

可能会影响性能,但是可以消除碎片

-XX:+UseCMSInitiatingOccupancyOnly

使用手动定义初始化定义开始CMS收集

禁止hostspot自行触发CMS GC

-XX:CMSInitiatingOccupancyFraction=70

使用cms作为垃圾回收

使用70%后开始CMS收集

92

为了保证不出现promotion failed(见下面介绍)错误,该值的设置需要满足以下公式CMSInitiatingOccupancyFraction计算公式

-XX:CMSInitiatingPermOccupancyFraction

设置Perm Gen使用到达多少比率时触发

92

-XX:+CMSIncrementalMode

设置为增量模式

用于单CPU情况

-XX:+CMSClassUnloadingEnabled

辅助信息

-XX:+PrintGC

输出形式:

[GC 118250K->113543K(130112K), 0.0094143 secs]

[Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails

输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]

[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

-XX:+PrintGCTimeStamps

-XX:+PrintGC:PrintGCTimeStamps

可与-XX:+PrintGC -XX:+PrintGCDetails混合使用

输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

-XX:+PrintGCApplicationStoppedTime

打印垃圾回收期间程序暂停的时间.可与上面混合使用

输出形式:Total time for which application threads were stopped: 0.0468229 seconds

-XX:+PrintGCApplicationConcurrentTime

打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用

输出形式:Application time: 0.5291524 seconds

-XX:+PrintHeapAtGC

打印GC前后的详细堆栈信息

-Xloggc:filename

把相关日志信息记录到文件以便分析.

与上面几个配合使用

-XX:+PrintClassHistogram

garbage collects before printing the histogram.

-XX:+PrintTLAB

查看TLAB空间的使用情况

XX:+PrintTenuringDistribution

查看每次minor GC后新的存活周期的阈值

Desired survivor size 1048576 bytes, new threshold 7 (max 15)

new threshold 7即标识新的存活周期的阈值为7。

前一篇:时间管理:重要紧急四象限的思考

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

=PNZ=BeijingL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值