前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java内存管理

Java内存管理

作者头像
Java帮帮
发布2018-03-16 17:40:58
1.6K0
发布2018-03-16 17:40:58
举报

内存溢出

理论学习

问题解决

垃圾回收

问题

理论学习

垃圾回收过程

常用垃圾回收器

工具篇

GC日志

命令行工具

可视化工具

问题解决

内存溢出

首先是比较”常见”的内存溢出,先解决两个小问题热热身:

1. 项目H代码量逐渐丰满,某天启动时果断抛出OutOfMemoryError

2. RD自己的单测一路畅通,QA构造的复杂测试case果断报StackOverflowError

理论学习

简单复习一下,虚拟机运行时管理的内存分为图中这几部分

这里写图片描述

程序计数器:存储当前线程执行字节码指令地址。谢天谢地,这部分不会OOM

Stack栈:分为虚拟机栈和本地方法栈(HotSpot中这两是一体的),存储编译期就确定好的局部变量,包括基本数据类型和引用,还有方法返回地址等。这部分如果栈深度溢出,会抛出StackOverflowError。理论上,无法申请到足够内存时,也会抛出OutOfMemoryError。64位jdk7下默认大小为1024字节。

Heap堆:对象都分配在堆上,这部分是垃圾回收管理的主要区域。

Method Area方法区:在HostSpot中用Perm永久代来实现,存储虚拟机加载的class信息,还有常量和静态变量等。

Direct Memory直接内存:不在JVM中,Nio中的DirectByteBuffer对象通过引用Native堆中内存,避免Java堆和Native堆内存复制的方式来提升性能。如果这部分内存加上Java堆内存大于服务器物理内存限制,也会导致JVM出现OOM。

栈、堆和方法区的逻辑关系如下:

这里写图片描述

问题解决

先看问题1,启动错误日志很明了

java.lang.OutOfMemoryError: PermGen space

1

根据理论学习Perm永久代爆掉了,初步分析原因,P同学使用了强大的CGLib做类增强,动态生成的类略多。修改启动脚本增大PermSize后恢复正常

-XX:PermSize=256M -XX:MaxPermSize=256M

1

再来看问题2,相对来讲,StackOverflowError的问题比较好定位,因为异常日志中记录了详细的方法调用栈信息。原来是某解析方法中递归调用层次太深(递归调用数千次)溢出。QA构造的复杂测试输入条件触发了这一潜在bug。把实现方式从递归修改为迭代,问题解决。

Exception in thread "pool-1-thread-6" java.lang.StackOverflowError

at com.baidu.rigel.hina.dws.ss.engin.QueryEngine.visit(QueryEngine.java:108)

at com.baidu.rigel.hina.dws.ss.engin.QueryEngine.visit(QueryEngine.java:108)

at com.baidu.rigel.hina.dws.ss.engin.QueryEngine.visit(QueryEngine.java:108)

at com.baidu.rigel.hina.dws.ss.engin.QueryEngine.visit(QueryEngine.java:108)

……

垃圾回收

问题

项目H的一些特性吸引了隔壁部门的兄弟,要求使用的需求很强烈。首先面临的问题是隔壁部门主推C++,所以我们使用thrift提供跨语言服务。ThriftServer中转层在QPS20000+的访问量下会OOM。

项目D中使用SolrCloud,在高并发性能测试环节,大部分情况下性能良好,每隔一段时间部分请求出现性能严重下降。

理论学习

垃圾回收过程

以GC Roots为起始点搜索引用链,不可达的对象标记为需要回收。常见的GC Roots有栈中局部变量、方法区中静态变量引用的对象等。

如果对象首次不可达并且其finalize方法被覆写,对象会被放进F-Queue队列中,由JVM中低优先级的finalizer线程来调用对象的finalize方法后再判断是否需要回收。其他情况下,对象会直接被GC过程回收。

最基础的回收算法采用Mark-Sweep标记-清理过程,标记阶段标记需要回收的对象,清理阶段进行统一回收。缺点是标记清理之后内存存在碎片,不利于后续分配较大对象。针对Mark-Sweep算法的不足,Mark-Compact算法标记阶段与Mark-Sweep保持一致,清理阶段移动存活对象,进行碎片整理。

Copying复制算法的基本思路是将内存划分成大小相等的两块,当其中一块用完之后,将存活对象复制到另外一块。缺点是可用内存变为原来的一半,而且如果垃圾回收是对象存活率高,复制的成本相应也很高。

分代收集:Hotspot根据对象存活周期将堆划分为新生代和老年代。新生代对象存活周期短,垃圾回收使用复制算法,针对复制算法空间利用率不足的问题,又将新生代细分为一个Eden和两个Survivor区,Eden区和Survivor默认大小比例是8:1,内存分配时使用Eden区和一个Survivor区,垃圾回收时复制到另外一个Survivor区,将空间利用率提升到90%(如果Survivor区空间不够,存活对象通过分配担保机制进入老年代)。而老年代对象存活率高,垃圾回收使用标记-清理或者标记-整理算法,一般来讲,老年代GC要比较新生代GC慢10倍以上。

常用垃圾回收器

这里写图片描述

几点说明:

1. 横线之上为新生代垃圾回收器,之下为老年代垃圾回收器,之间的连线表示他们可以配合使用。

2. Serial代表单线程,在新生代复制和老年代标记-清理过程中都是单线程运作,特点是简单高效,常用于客户端模式下桌面应用,几百兆的内存新生代回收几十毫秒的停顿基本用户是无感知的。Par(Parallel)通过多线程回收来缩短停顿时间,适应于多CPU服务器环境。

3. CMS代表Concurrent Sweep,真正实现了应用线程和垃圾回收线程并发运行,特点是停顿时间最短,是现在强调低延迟响应的互联网应用中最常用的老年代回收器。有这么几个特点:

- 分为initial mark(初始标记,找出GC Root直接关联对象,耗时很短)–>concurrent mark(并发标记,进行引用链搜索耗时很长)–>remark(重新标记,修正并发标记期间用户线程并发运行导致的)–>concurrent sweep(并发清理)四个阶段,在最耗时的并发标记和并发清理阶段都实现了和应用线程的并发。

- 缺点1:CPU占用率略高。默认回收线程为(CPU+3)/4,所以更适合于CPU个数4以上的服务器环境。

- 缺点2:回收过程中应用线程还在运行,会产生新的垃圾,所以需要预留足够空间。一般应用会通过适当调高 -XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,但是如果预留内存不够,会出现“Concurrent Mode Failure”失败,导致虚拟机启动Serial Old回收器再次进行老年代垃圾回收。

4. G1次时代回收器,略高级,目前JDK7生产环境下应用不多,下回切到JDK8再研究。

工具篇

GC日志

在java启动命令中添加以下参数,可以输出更为详细的gc日志

-Xloggc:logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails

下面具体gc日志还是比较好理解的,要关注的有几点:

-XX:+PrintHeapAtGC选项用于输出gc前后堆内存具体信息,可以看出本次gc回收eden区内存效果很明显。因为使用了ParNew和CMS垃圾回收器的组合,所以日志中带有par new和concurrent mark-sweep generation字样。

每次GC都统计了用户cpu时间(0.31s)、系统cpu时间(0.05s)和实际耗时(0.05s)。因为多cpu的缘故,实际耗时反倒小于cpu时间。整个GC耗时50ms,对于3g左右的新生代性能还算凑合。

2015-08-21T12:04:49.733+0800: 66.180: [GC2015-08-21T12:04:49.733+0800: 66.180: [ParNew: 2097152K->48180K(2621440K), 0.0486990 secs] 2097152K->48180

K(12058624K), 0.0488530 secs] [Times: user=0.31 sys=0.05, real=0.05 secs]

Heap after GC invocations=1 (full 0):

par new generation total 2621440K, used 48180K [0x00000004fae00000, 0x00000005bae00000, 0x00000005bae00000)

eden space 2097152K, 0% used [0x00000004fae00000, 0x00000004fae00000, 0x000000057ae00000)

from space 524288K, 9% used [0x000000059ae00000, 0x000000059dd0d280, 0x00000005bae00000)

to space 524288K, 0% used [0x000000057ae00000, 0x000000057ae00000, 0x000000059ae00000)

concurrent mark-sweep generation total 9437184K, used 0K [0x00000005bae00000, 0x00000007fae00000, 0x00000007fae00000)

concurrent-mark-sweep perm gen total 21248K, used 20983K [0x00000007fae00000, 0x00000007fc2c0000, 0x0000000800000000)

}

{Heap before GC invocations=1 (full 0):

par new generation total 2621440K, used 2145332K [0x00000004fae00000, 0x00000005bae00000, 0x00000005bae00000)

eden space 2097152K, 100% used [0x00000004fae00000, 0x000000057ae00000, 0x000000057ae00000)

from space 524288K, 9% used [0x000000059ae00000, 0x000000059dd0d280, 0x00000005bae00000)

to space 524288K, 0% used [0x000000057ae00000, 0x000000057ae00000, 0x000000059ae00000)

concurrent mark-sweep generation total 9437184K, used 0K [0x00000005bae00000, 0x00000007fae00000, 0x00000007fae00000)

concurrent-mark-sweep perm gen total 22208K, used 22172K [0x00000007fae00000, 0x00000007fc3b0000, 0x0000000800000000)

命令行工具

JDK自带了一系列非常有用的命令行工具。

通过jps查看java进程:

[work@nmg01-crm-hina60.nmg01.baidu.com ~]# jps -l

39195 org.apache.hadoop.hbase.regionserver.HRegionServer

33096 sun.tools.jps.Jps

21105 org.apache.hadoop.hdfs.server.datanode.DataNode

57774 org.apache.zookeeper.server.quorum.QuorumPeerMain

36790 start.jar

9761 kafka.Kafka

通过jstat查看jvm运行统计状态,比如下面通过gcutil选项可以得知虚拟机中内存占用比率(老年代O 45.30%)、gc次数(新生代3605次)和gc耗时(135s)等,性能还算能接受。

[work@nmg01-crm-hina60.nmg01.baidu.com ~]# jstat -gcutil 36790

S0 S1 E O P YGC YGCT FGC FGCT GCT

0.00 2.97 84.69 45.30 99.48 3605 135.501 0 0.000 135.501

使用jps -v查看用户启动java程序时显示指定的参数;使用jinfo查看和调整各种参数,包括默认参数;使用jmap dump堆;使用jstack查看线程快照等。

可视化工具

jdk还自带jconsole和jvisualvm两大杀器。jconsole基于jmx进行监控和管理,已经很好用了;jvisualvm比jconsole要新一些,功能也更强大,不仅包含了大部分命令行工具具备的功能,还能通过插件机制扩展性能,比如profiing性能分析等。不多说截张图,图中jvisualvm使用Visual GC插件,新生代锯齿形(内存占用缓慢上升,回收干脆)说明GC情况还是比较理想的。。

这里写图片描述

问题解决

在项目H中,客户端请求并发比较高,达到2000+,服务端线程飙升,所以先把ThriftServer类型改为TThreadedSelectorServer,以select方式处理IO请求,超出处理能力的请求会被等待,OOM现象有一定缓解,但依然会出现。进一步分析,因为ThriftServer仅仅做服务转发,高压力下产生的大量对象都需要被回收掉,我们期望这些临时对象都在新生代就被回收掉,所以我们适当调大了新生代大小,再根据每次新生代GC前后内存占用情况,适当调小了SurvivorRatio参数,最终服务变得稳定起来。

java_opt="-Xms14192m -Xmx14192m -Xmn8000m -XX:SurvivorRatio=3 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:CMSFullGCsBeforeCompaction=4 -XX:PretenureSizeThreshold=20485760"

项目D中的这种性能间歇性下降,怀疑是GC导致。查看solr的GC日志,果然发现后台频繁触发full gc,每次full gc停顿2s以上,而且和前台并发查询性能下降时间点吻合。按照Solr官方手册推荐,我们将JVM垃圾回收起调整为ParNew+CMS组合,并根据我们的数据量和查询场景,调整了相关参数:

java_opt="-Xloggc:logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xmn3G -Xmx12G-Xms12G -XX:SurvivorRatio=4 -XX:CMSInitiatingOccupancyFraction=60 -XX:+UseParNewGC-XX:+UseConcMarkSweepGC"

调整完发现垃圾回收效果不明显,gc停顿的问题也没有明显好转。使用jvisualvm查看SolrCloud的堆增长曲线,很直很陡,3G的新生代和12G的整个堆空间很快就接近满载。使用内存抽样,结果也很抽象,占用比例最高的是byte[],而且查询并发上去之后,内存突增非常明显,有点诡异,因为我们的查询场景并不复杂,而且大部分情况下返回结果只有几十条。是不是什么地方存在内存泄露?

跟踪一下代码发现一个比较有意思地方:SolrCloud分布式搜索中结果由各个Shard分片结果合并排序生成,在QueryComponent中会初始化一个优先级队列用于排序,队列的大小等于用户指定的起始偏移量offset+返回结果rows(而不是实际的查询结果数量)。

代码语言:javascript
复制
public class QueryComponent extends SearchComponent{
    ……
    private void  mergeIds(ResponseBuilder rb, ShardRequest sreq) {
        // Merge the docs via a priority queue so we don't have to sort *all* of the
        // documents... we only need to order the top (rows+start)
        ShardFieldSortedHitQueue queue;
        queue = new ShardFieldSortedHitQueue(sortFields, ss.getOffset() + ss.getCount(), rb.req.getSearcher());
    ……

而在我们项目代码中,在用户没有指定返回结果数量时,使用默认值Integer.MAX_VALUE,这个时候即使后台查询结果只有1条,QueryComponent也会老老实实初始化一个巨大的优先级队列,占用很大的内存空间。

solrQuery.setStart(query.getOffset());

solrQuery.setRows(query.getRows());

在请求压力小的时候,这部分浪费的内存会被GC回收掉没有什么问题;在请求压力大时,内存回收的速度越来越跟不上内存挥霍速度,不得不每隔一段停下来进行full gc。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2016-12-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java帮帮 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
命令行工具
腾讯云命令行工具 TCCLI 是管理腾讯云资源的统一工具。使用腾讯云命令行工具,您可以快速调用腾讯云 API 来管理您的腾讯云资源。此外,您还可以基于腾讯云的命令行工具来做自动化和脚本处理,以更多样的方式进行组合和重用。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档