1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > 「JVM 故障诊断」故障分析与处理案例

「JVM 故障诊断」故障分析与处理案例

时间:2019-02-09 15:22:06

相关推荐

「JVM 故障诊断」故障分析与处理案例

面对线上故障,除了要了解 JVM 相关知识和工具,经验同样是很重要的;

通过设计和开发阶段先行避免问题;在应用部署层面去解决问题;通过升级硬件和 JDK 版本(使用新技术)解决问题;

文章目录

1. 大内存硬件上的程序部署策略2. 堆外内存导致的溢出错误3. 外部命令导致的系统缓慢4. 服务器虚拟机进程崩溃5. 不恰当数据结构导致内存占用过大6. Windows 虚拟内存导致的长时间停顿7. 由安全点导致长时间停顿

1. 大内存硬件上的程序部署策略

配置

数量: 1CPU: 4 Core内存: 16 GBJDK: jdk 5 64 位VM Arguments: -Xmx12GB -Xms12GB

问题与分析

作为一个 15W PV/day 的在线文档类网站,经常不定期出现长时间失去响应;

文档从磁盘写入内存时,因为出现很多大对象,大多时候这些对象会直接进入老年代,不会被 Minor GC 回收,每隔几分钟内存就被占满,触发一次 Full GC 停顿 10+s;

处理方式与注意事项

通过一个单独的 JVM 实例管理大量 Java Heap 内存; 需要将 Full GC 的频率控制到足够低,并让 Full GC 在用户不使用的时刻进行;回收大块堆内存可能有长时间停顿;需要 64 位 JVM 才能支持大内存;而由于压缩指针、处理器缓存行容量等因素,64 位 JVM 性能往往低于 32 位;如果发生内存溢出,很难产生 Heap 转储快照,即时生成了也很难分析;需要记住 JMC 这类工具在生产环境进行分析;想通程序 64 位 JVM 比 32 位 JVM 消耗的内存要大(指针碰撞、数据类型对齐等); 通过多个 JVM 实例建立逻辑集群来利用硬件资源; 节点对全局资源的竞争、特别是磁盘 I/O;连接池等资源难以提高利用效率(难以均衡);若大量使用本地内存,将是内存占用的空间翻倍;改用 CMS GC;

2. 堆外内存导致的溢出错误

配置

数量: 1内存: 4 GBJDK: 32 位Server: Jetty 7.1.4

问题与分析

测试期间服务就不时出现内存溢出异常,将 Heap 容量调大,内存溢出异常发生得更加频繁了;开启-XX:+HeapDumpOnOutOfMemoryError后,抛出内存溢出异常时没有输出 Heap 转储快照文件;使用 jstat 观察 GC 状态,发现 GC 并不频繁;且 Heap 各区内存都很稳定,无压力;最后从系统日志找到异常堆栈信息;

[org.eclipse.jetty.util.log] handle failed java.lang.OutOfMemoryError: nullat sun.misc.Unsafe.allocateMemory(Native Method)at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:99)at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)at org.eclipse.jetty.io.nio.DirectNIOBuffer.<init>......

从堆栈信息看出程序在申请 Direct Memory 时空间不足;Direct Memory 只会在老年代满后触发 Full GC 时顺便回收,或者在捕获 OutOfMemoryError 后手动调用System.gc()触发 GC(如果 JVM 开启了-XX:+DisableExplicitGC禁用显示 GC,这招就无用了);

处理方式与注意事项

所有内存总和受到物理内存和操作系统进程最大内存限制;直接内存,通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryErrorOutOfMemoryError: Direct buffer memory线程堆栈,通过-Xss调整大小,内存不足时抛出StackOverflowError(线程的实际栈深超过了 JVM 允许的深度) 或OutOfMemoryError(JVM 允许栈容量动态扩展时,栈扩展无法申请到足够的内存);Socket 缓存区,每个 Socket 连接存在 Receive(37KB) 和 Send(25KB) 两个缓存区,当申请过多的连接而无法分配内存时,可能会抛出IOException: Too many open filesJNI 代码,若使用 JNI 调用本地库,使用的内存是 JVM 的本地方法栈和本地内存;JVM 和 GC,JVM、GC 的工作要消耗一定的内存;

3. 外部命令导致的系统缓慢

配置

数量: 1CPU: 4 CoreOS: Solaris 10中间件: GlassFish

问题与分析

在大并发测试时请求的响应时间比较慢;

通过 mpstat 发现 CPU 使用率很高,但都是被系统占用的(正常应该是用户程序使用绝大部分 CPU 资源);

通过 dtrace 脚本查看当前消耗 CPU 资源最多的系统调用,发现大量fork调用,这个命令会复制一个和当前进程(JVM)一样环境变量的进程;

应用程序为每一次请求通过 Java 的Runtime.gettime().exec()调用外部 Shell 脚本,这会创建一个 shell 进程;

处理方式与注意事项

尽量将调用外部 Shell 脚本的需求改用 Java 的 API 去实现;或者保障所有调用只在一个 shell 进程中进行,避免频繁创建销毁进程,这对系统开销是巨大的;

4. 服务器虚拟机进程崩溃

配置

数量: 1CPU: 2 Core内存: 8 GBServer: WebLogic 9.2

问题与分析

服务允许一段时间后 JVM 进程自动关闭,系统日志 hs_err_pid###.log 留下如下异常堆栈;

.SocketException: Connection resetat .SocketInputStream.read(SocketInputStream.java:168)at java.io.BufferedInputStream.fill(BufferedInputStream.java:218)at java.io.BufferedInputStream.read(BufferedInputStream.java:235)at org.apache.axis.transport.http.HTTPSender.readHeadersFromSocket(HTTPSender.java:583) at org.apache.axis.transport.http.HTTPSender.invoke(HTTPSender.java:143)... 99 more

分析发现用户程序会在请求中通过网络向远程 OA 系统发送一个待办事项同步,由于 OA 系统响应缓慢,为了避免被 OA 系统拖累,系统使用了异步的方式调用 OA 服务,但由于 OA 处理速度实在太慢,导致 Socket 连接越来越多,超出了 JVM 承受范围,导致 JVM 进程崩溃;

处理方式与注意事项

将待办事项同步功能改用消息队列实现;

5. 不恰当数据结构导致内存占用过大

配置

数量: 1JVM: 64 位GC: ParNew + CMSVM Arguments: -Xms4g -Xmx8g -Xmn1g

问题与分析

每 10 min 加载一个 80 MB 的文件进行数据分析,数据在内存中以 100W 个HashMap<Long, Long>Entry实例存在,此时 Minor GC 造成停顿约为 500 ms(平时是 30 ms);

由于这 80 MB 的对象并非朝生夕灭,在每次 Minor GC 后还会在 Survivor 区来回复制,这是一个沉重的负担;

处理方式与注意事项

使用-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0或者-XX:+AlwaysTenure让新生代中存活下来的对象在第一次 Minor GC 后立即进入老年代,或者优化存储;

使用 HashMap 做大对象的数据存储的空间利用率是很低的,以HashMap<Long, Long>为例;

有效数据

Key: 8 bytesValue: 8 bytes

全部内存空间

Long Mark Work: 8 bytesKlass 指针: 8 byteslong 值: 8 bytes Map.Entry Mark Work: 8 bytesKlass 指针: 8 bytesnext: 8 byteshash: 4 bytes(int 型)对齐: 4 bytes HashMap 对 Entry 的引用: 8 bytes

实际效率:(8*2) / ((Long)8*3*2 + (Entry)32 + (HashMap Ref(8))) = 16/88 -> 18%

6. Windows 虚拟内存导致的长时间停顿

配置

VM Arguments: -Xmx256m

问题与分析

GUI 桌面程序,程序每 15s 一次心跳,若 30s 内没有返回信号,则与远程连接断开;通过日志发现正在运行的程序偶尔会出现间隔约 1min 的停顿;

追加-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -Xloggc:gclog.log打印日志发现确实存在 GC 导致应用程序长时间停顿的现象;

// -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -Xloggc:gclog.logTotal time for which application threads were stopped: 0.0112389 secondsTotal time for which application threads were stopped: 0.0001335 secondsTotal time for which application threads were stopped: 0.0003246 secondsTotal time for which application threads were stopped: 41.4731411 secondsTotal time for which application threads were stopped: 0.0489481 secondsTotal time for which application threads were stopped: 0.1110761 secondsTotal time for which application threads were stopped: 0.0007286 secondsTotal time for which application threads were stopped: 0.0001268 seconds

// 追加 -XX:+PrintReferenceGC-08-29T19:14:30.968+0800: 10069.800: [GC10099.225: [SoftReference, 0 refs, 0.0000109 secs]10099.226: [WeakR

且每当程序最小化时,资源管理器内显示的内存大幅减小,但虚拟内存没有变化,存在工作内存被置换到磁盘页面中的可能;可能因为 GC 时页面文件恢复操作导致了 GC 停顿时间变长;

处理方式与注意事项

通过-Dsun.awt.keepWorkingSetOnMinimize=true让 AWT 程序在恢复最小化时能立即响应;

7. 由安全点导致长时间停顿

配置

Server: HBaseJDK: 8GC: G1VM Arguments: -XX:MaxGCPauseMillis=500

问题与分析

集群会存在大量 MapReduce 和 Spark 的离线任务,也会存在 Replication 在线数据写入,读写压力较大,对延迟不敏感;但偶尔的 GC 停顿会长达 3s 以上;

[Times: user=1.51 sys=0.67, real=0.14 secs]-06-25T 12:12:43.376+0800: 3448319.277: Total time for which application threads were stopped: 2.2645818 se

user: 进程执行用户态代码所耗费的处理器时间(单核线程场景下耗时计数);sys: 进程执行核心态代码所耗费的处理器时间;real: 执行动作从开始到结束所耗费的时钟时间(现实世界的时间计数,这个时间才是用户最关心的);

日志显示 GC 耗时 0.14 s,但用户线程停顿了 2.26 s,两者差距超过了正常的 TTSP(Time To Safepoint);使用-XX:+PrintSafePointStatistics -XX:PrintSafepointStatisticsCount=1查看安全点日志;

vmop [threads: total initially_running wait_to_block]65968.203: ForceAsyncSafepoint [931 1 2][time: spin block sync cleanup vmop] page_trap_count[2255 0 2255 11 0] 1

发现当前 JVM 的操作是等待所有用户线程进入安全点,但又两个线程很慢;spin(2255 ms)表示自旋等待时间;

通过-XX:+SafepointTimeout -XX:SafepointTimeoutDelay=2000打印等待线程进入安全点超过 2000 ms 的线程;

# SafepointSynchronize::begin: Timeout detected:# SafepointSynchronize::begin: Timed out while spinning to reach a safepoint.# SafepointSynchronize::begin: Threads which did not reach the safepoint:# "RpcServer.listener,port=24600" #32 daemon prio=5 os_prio=0 tid=0x00007f4c14b22840 nid=0xa621 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE# SafepointSynchronize::begin: (End of list)

发现RpcServer.listener, port=24600线程是阻止线程进入安全点的原因;

安全点事以是否具有让程序长时间执行的特征进行选定的,一般为 方法调用、循环调整、异常调整等位置;但循环次数较少(比如以 int 类型,或范围更小的数据类型作索引的循环,可数循环,Counted Loop)的不会作为安全点(可以使用 -XX:+UseCountedLoopSafepoints 强制放开);

RpcServer.listener, port=24600中存在同时建立大量连接,然后交由一个线程去清理的情况;而其中就使用了 int 作为循环索引,在连接的循环清理过程中,会阻塞所有线程进入到安全点;

处理方式与注意事项

将循环索引的数据类型从 int 改为 long 即可;

上一篇:「JVM 故障诊断」HotSpot VM 插件与工具

下一篇:「JVM 性能调优」应用程序启动耗时与延时优化

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!

参考资料:

[1]《深入理解 Java 虚拟机》

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。