排查了三四个小时,终于解决了这个GC问题,记录解决过程于此,希望对大家有所帮助。本文假定读者已具备基本的GC常识和JVM调优知识,关于JVM调优工具使用可以查看我在同一分类下的另一篇文章:

http://my.oschina.net/feichexia/blog/196575

背景说明

发生问题的系统部署在Unix上,发生问题前已经跑了两周多了。

其中我用到了Hadoop源码中的CountingBloomFilter,并将其修改成了线程安全的实现(详情见:AdjustedCountingBloomFilter),主要利用了AtomicLong的CAS乐观锁,将long[]替换成了AtomicLong[]。这样导致系统中有5个巨大的AtomicLong数组(每个数组大概占50MB),每个数组包含大量AtomicLong对象(所有AtomicLong对象占据大概1.2G内存)。而且这些AtomicLong数组的存活时间都至少为一天。

服务端已先于手机客户端上线,客户端本来计划本周四上线(写这篇文章时是周一),所以我还打算在接下来的几天继续观察下系统的运行状况,开启的仍然是Debug级别日志。

部分JVM参数摘抄如下(JVM参数配置在项目部署的tomcat服务器的根目录下的bin目录下的setenv.sh中,可以通过ps -ef | grep xxx | grep -v grep查看到):

-XX:PermSize=256M -XX:MaxPermSize=256M -Xms6000M -Xmx6000M -Xmn1500M -Xss256k -XX:ParallelGCThreads=8 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+DisableExplicitGC -XX:+CMSParallelRemarkEnabled -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:CMSInitiatingOccupancyFraction=70 -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection -XX:+CMSScavengeBeforeRemark -XX:+HeapDumpOnOutOfMemoryError -Xloggc:/usr/local/webserver/point/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime

可以看到持久代被设置为256M,堆内存被设置为6000M(-Xms和--Xmx设为相等避免了“堆震荡”,能在一定程度减少GC次数,但会增加平均每次GC消耗的时间),年轻代被设置为1500M。

-XX:+UseConcMarkSweepGC设置老年代使用CMS(Concurrent Mark-Sweep)收集器。 -XX:+UseParNewGC设置新生代使用并行收集器,-XX:ParallelGCThreads参数指定并行收集器工作线程数,在CPU核数小于等于8时一般推荐与CPU数目一致,但当CPU核数大于8时推荐设置为:3 + [(5*CPU_COUNT) / 8]。其他参数略去不提。

问题发现与解决过程

早上测试找到我说线上系统突然挂了,报访问超时异常。

首先我第一反应是系统内存溢出或者进程被操作系统杀死了。用ps -ef | grep xxx | grep -v grep命令查看进程还在。然后看tomcat的catlalina.out日志和系统gc日志,也未发现有内存溢出。

接下来用jstat -gcutil pid 1000看了下堆中各代的占用情况和GC情况,发现了一个挺恐怖的现象:Eden区占用77%多,S0占用100%,Old和Perm区都有很大空间剩余。

怀疑是新生代空间不足,但没有确凿证据,只好用jstack获取线程Dump信息,不看不知道,一看就发现了一个问题(没有发现线程死锁,这里应该是“活锁”问题):

从上面第一段可看到有一个Low Memory Detector系统内部线程(JVM启动的监测和报告低内存的守护线程)一直占着锁0x00....00,而下面的C2 CompilerThread1、C2 CompilerThread0、Signal Dispatcher和Surrogate Locker线程都在等待这个锁,导致整个JVM进程都hang住了。

在网上搜索一圈,发现大部分都建议调大堆内存,于是根据建议打算调大整个堆内存大小、调大新生代大小(-Xmn参数)、调大新生代中Survivor区占的比例(-XX:SurvivorRatio参数)。并且由于存在AtomicLong数组大对象,所以设置-XX:PretenureThreshold=10000000,即如果某个对象超过10M(单位为字节,所以换算后为10M)则直接进入老年代,注意这个参数只在Serial收集器和ParNew收集器中有效。另外希望大量的长生命周期AtomicLong小对象能够尽快进入老年代,避免老年代的AtomicLong数组对象大量引用新生代的AtomicLong对象,我调小了-XX:MaxTenuringThreshold(这个参数的默认值为15),即现在年轻代中的对象至多能在年轻代中存活8代,如果超过8代还活着,即使那时年轻代内存足够也会被Promote到老年代。有修改或增加的JVM GC参数如下:

-Xms9000M -Xmx9000M -Xmn1500M -XX:SurvivorRatio=6 -XX:MaxTenuringThreshold=8 -XX:PretenureSizeThreshold=10000000

重启系统后用jstat -gcutil pid 1000命令发现一个更恐怖的现象,如下图:Eden区内存持续快速增长,Survivor占用依然很高,大概每两分钟就Young GC一次,并且每次Young GC后年老代内存占用都会增加不少,这样导致可以预测每三四个小时就会发生一次Full GC,这是很不合理的。

第二列是S1,占用高达87.45%,第三列是Eden区内存占用变化情况,可以看到增长非常快。

于是我用jmap -histo:live(注意jmap命令会触发Full GC,并发访问量较大的线上环境慎用)查看了下活对象,发现有一些Integer数组和一些Character数组占用内存在持续增长,并且占了大概好几百M的内存,然后经过Young GC又下降,然后再次快速增长,再Young GC下降,周而复始。

至此,我推测可能是大量的Integer数组对象和Character数组对象基本占满了Survivor,导致在Eden满了之后,新产生的Integer数组对象和Character数组对象不足以放入Survivor,然后对象被直接被Promote到了年老代,这种推测部分正确,它解释了S1占用那么高的原因,但不能解释上面的Eden区内存占用持续上升。

于是继续查看了下接口调用日志,不看不知道,一看吓一跳:日志刷新非常之快(99%是DEBUG日志)。原来运营和测试在未通知我们服务端的情况下已经于昨天在某个渠道发布了一个Android线上版本(难怪今天就暴露问题了),再看了下使用该系统的用户已经有6400多个了,彻彻底底被他们坑了一把。这就能解释为什么上面有一个Integer数组和Character数组占用内存持续增长了,原因就在于大量的系统接口调用触发了大量DEBUG日志刷新,写日志对于线上系统是一个重量级操作,无论是对CPU占用还是对内存占用,所以高并发线上系统一定要记得调高日志级别为INFO甚至ERROR。

于是修改log4j.properties中的日志级别为INFO,然后用jmap -histo:live pid命令查看活对象,发现Integer数组对象和Character[]数组对象明显下降,并且占用内存也由前面的几百M降到几M。

之后再用jstat -gcutil pid 1000查看了下GC情况,如下:

很明显Survivor占用没这么高了,最重要的Young GC后年老代内存占用不会增加了,此处Eden区增长貌似还是挺快,因为此时用户数比前面多了很多。至此出现的问题基本搞定,但还有待后续观察。

总结

总的来说本系统中存在一个违背GC假设的东西,那就是在JVM堆中存在着海量生命周期较长的小对象(AtomicLong对象)。这无疑会给系统埋坑。

GC分代基本假设是:

JVM堆中存在的大部分对象都是短生命周期小对象。

这也是为什么Hotspot JVM的年轻代采用复制算法的原因。

其他推荐一些非常不错的GC方面的参考文章(前两篇都来自《深入理解Java虚拟机》一书,参考链接大部分是我今天查阅的资料,大家选择性看就好):

JVM内存管理:深入Java内存区域与OOM http://www.iteye.com/topic/802573

JVM内存管理:深入垃圾收集器与内存分配策略 http://www.iteye.com/topic/802638

Oracle GC Tuning http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html

Java 6 JVM参数选项大全 http://kenwublog.com/docs/java6-jvm-options-chinese-edition.htm

Java HotSpot VM Options http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

CMS GC实践总结 http://www.iteye.com/topic/473874

JVM内存的分配及回收 http://blog.csdn.net/eric_sunah/article/details/7893310

一步一步优化JVM系列 http://blog.csdn.net/zhoutao198712/article/category/1194642

Java线程Dump分析 http://www.linuxidc.com/Linux/2009-01/18171.htm http://jameswxx.iteye.com/blog/1041173

利用Java Dump进行JVM故障诊断 http://www.ibm.com/developerworks/cn/websphere/library/techarticles/0903_suipf_javadump/

Detecting Low Memory in Java https://techblug.wordpress.com/2011/07/16/detecting-low-memory-in-java/

Detecting Low Memory in Java Part 2 http://techblug.wordpress.com/2011/07/21/detecting-low-memory-in-java-part-2/

http://blog.sina.com.cn/s/blog_56d8ea9001014de3.html

http://stackoverflow.com/questions/2101518/difference-between-xxuseparallelgc-and-xxuseparnewgc

http://stackoverflow.com/questions/220388/java-concurrent-and-parallel-gc

http://j2eedebug.blogspot.com/2008/12/what-to-look-for-in-java-thread-dumps.html

https://devcenter.heroku.com/articles/java-memory-issues

http://blog.csdn.net/sun7545526/article/category/1193563

http://java.dzone.com/articles/how-tame-java-gc-pauses

原文链接:https://my.oschina.net/feichexia/blog/277391

一次线上GC故障解决过程记录的更多相关文章

  1. [转]线上GC故障解决过程记录

    排查了三四个小时,终于解决了这个GC问题,记录解决过程于此,希望对大家有所帮助.本文假定读者已具备基本的GC常识和JVM调优知识,关于JVM调优工具使用可以查看我在同一分类下的另一篇文章: http: ...

  2. 用“逐步排除”的方法定位Java服务线上“系统性”故障(转)

    一.摘要 由于硬件问题.系统资源紧缺或者程序本身的BUG,Java服务在线上不可避免地会出现一些“系统性”故障,比如:服务性能明显下降.部分(或所 有)接口超时或卡死等.其中部分故障隐藏颇深,对运维和 ...

  3. Java线上应用故障排查之二:高内存占用

    搞Java开发的,经常会碰到下面两种异常: 1.java.lang.OutOfMemoryError: PermGen space 2.java.lang.OutOfMemoryError: Java ...

  4. 一次线上OOM故障排查经过

    转贴:http://my.oschina.net/flashsword/blog/205266 本文是一次线上OOM故障排查的经过,内容比较基础但是真实,主要是记录一下,没有OOM排查经验的同学也可以 ...

  5. java线上应用故障排查之二:高内存占用【转】

    前一篇介绍了线上应用故障排查之一:高CPU占用,这篇主要分析高内存占用故障的排查. 搞Java开发的,经常会碰到下面两种异常: 1.java.lang.OutOfMemoryError: PermGe ...

  6. 【JVM】线上应用故障排查

    高CPU占用 一个应用占用CPU很高,除了确实是计算密集型应用之外,通常原因都是出现了死循环. 根据top命令,发现PID为28555的Java进程占用CPU高达200%,出现故障. 通过ps aux ...

  7. 线上 S1 故障是什么, 线上 S1 故障, 运维故障分级, 运维, 故障分级, P1 级别故障, 故障, P1 , S1

    线上 S1 故障是什么 线上 S1 故障, 运维故障分级, 运维, 故障分级, P1 级别故障, 故障, P1 , S1 故障复盘 https://time.geekbang.org/column/a ...

  8. IIS发布网站 报错500.19 错误解决过程记录

    首先先报上我的环境 WindowsServer 2012 IIS 8.5 网站是FrameWork 4.0 发布网站后浏览,报错信息如下: 解决过程记录如下: 1.看到这个问题首先想到的是权限问题,设 ...

  9. 记一次线上gc调优的过程

           近期公司运营同学经常表示线上我们一个后台管理系统运行特别慢,而且经常出现504超时的情况.对于这种情况我们本能的认为可能是代码有性能问题,可能有死循环或者是数据库调用次数过多导致接口运行 ...

随机推荐

  1. greenplum分布键的hash值计算分析

    greenplum 数据分布策略 greenplum 是一个 MPP 架构的数据库,由一个 master 和多个 segment 组成(还可选配置一个 standby master),其数据会根据设置 ...

  2. Python中Numpy及Matplotlib使用

    Python中Numpy及Matplotlib使用 1. Jupyter Notebooks 作为小白,我现在使用的python编辑器是Jupyter Notebook,非常的好用,推荐!!! 你可以 ...

  3. Python 模块feedparser安装使用

    RSS(简易信息聚合) 简易信息聚合(也叫聚合内容)是一种RSS基于XML标准,在互联网上被广泛采用的内容包装和投递协议.RSS(Really Simple Syndication)是一种描述和同步网 ...

  4. 20191310李烨龙Linux C语言编程基础

    Linux C语言编程基础 任务详情 0. 基于Ubuntu或OpenEuler完成下面的任务(OpenEuler有加分) 1. 选择教材第二章的一节进行编程基础练习(2.10,2.11,2.12,2 ...

  5. 【centos】更换yum源

    yum下载有时候很慢,可以换一下源: 步骤: 1)下载wget yum install -y wget 2)备份默认的yum mv /etc/yum.repos.d /etc/yum.repos.d. ...

  6. Go 跳出 for-switch 和 for-select 代码块

    原文:https://segmentfault.com/a/1190000013739000 没有指定标签的 break 只会跳出 switch/select 语句,若不能使用 return 语句跳出 ...

  7. js中function (res) {}与(res) =>{}的区别

    这里是引用function (res) {} 中 打印this时是undefined 打印that时是有内容的                let that = this;              ...

  8. c++学习笔记6(结构化程序设计的不足)

    结构化程序设计 c语言使用结构化程序设计: 程序=数据结构+算法 程序有全局变量以及众多相互调用的函数组成 算法以函数的形式实现,用于对数据结构进行操作 结构化程序设计不足 软件业的目标是更快,更正确 ...

  9. Visual Studio中使用Macros插件给代码添加注释、时间和以及自动脚本

    title: Visual Studio中使用Macros插件给代码添加注释.时间和以及自动脚本 date: 2020-09-11 sidebarDepth: 2 tags: 代码 Visual st ...

  10. Github webhooks 自动部署博客文章,使用总结【含视频】

    作者:小傅哥 博客:https://bugstack.cn 原文:https://mp.weixin.qq.com/s/VtTHUfyiITNSoGy052jkXQ 沉淀.分享.成长,让自己和他人都能 ...