深入理解JVM之虚拟机性能监控与故障处理工具

概述

定位问题时,知识、经验是关键基础,数据(运行日志、异常堆栈、GC日志、堆转储快照、线程快照)是依据,工具是运用知识处理数据的手段。

JDK的命令行工具

虚拟机自带命令行工具

jps—>虚拟机进程状况工具

格式:jps  [options]       [hostid]
option参数如下:
jps命令
* 功能:列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID(Local Vistual Machine Identifier, LVMID);
* 对于本地虚拟机来说,LVMID和操作系统的进程ID一致;
* 其他的工具通常都要依赖jps获取LVMID;
* 主要选项:-q(只输出LVMID)、-m(输出传给main函数的参数)、-l(输出主类的全名)、-v(输出虚拟机启动JVM参数);

jstat—>虚拟机统计信息监视工具

格式:jstat    [options        vmid    [interval   [s|ms]  [count]]    ]
option参数如下:查询的信息主要分为3类:类装载、垃圾收集、运行期编译状况。
jstat命令
* 功能:件事虚拟机的各种运行状态信息,包括类装载、内存、垃圾收集、JIT等;
* 纯文本监控首选;

jinfo—>Java配置信息工具

格式:jinfo    [options]       pid
* 功能:实时地查看虚拟机各项参数。虽然jps -v可以查看虚拟机启动参数,但是无法查看一些系统默认的参数;
* 支持运行期修改参数的能力,格式为“jinfo -flag name=value pid”;

jmap—>Java内存映像工具

格式:jmap [options]       [vmid]
options参数如下:
jmap命令
* 功能是用于生成堆转储快照(heapdump或者dump文件);
* 还能用其他方式生成堆转储快照:使用参数-XX:+HeapDumpOnOutOfMemoryError;使用参数-XX:+HeapDumpOnCtrlBreak然后使用Ctrl+Break生成;Linux系统使用kill -3生成;
* 除了生成堆转储快照外,还可用于查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等; 

jhat—>虚拟机堆转储快照分析工具

格式:jhat [快照]
* 功能是和jmap搭配使用,分析jmap生成的堆转储快照,内置了一个微型的HTTP/HTML服务器,可在浏览器查看分析结果;
* 实际中很少使用jhat,主要是因为分析会消耗服务器资源并且功能相对于BisualVM、IBM HeapAnalyzer等相对单一;

jstack—>Java堆栈跟踪工具

格式:jstack       [options]       vmid
options参数如下:
jstack命令
* 功能是用于生成虚拟机当前时刻的线程快照(threaddump或者javacore文件),主要目的是定位线程长时间停顿的原因(线程间死锁、死循环、请求外部资源导致的长时间等待等);
* JDK1.5之后java.lang.Thread新增了getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象,可以基于该方法增加管理页面分析线程堆栈;

HSDIS:JIT生成代码反汇编

JIT生成反汇编代码
* 现代虚拟机的实现细节已经和虚拟机规范所描述的内容差距越来越大,虚拟机规范中的描述逐渐成为虚拟机实现的“概念模型”。如果要分析程序如果执行,最常见的就是通过软件调试工具(GDB、Windbg等)断点调试,但是对于Java来说,很多执行代码是通过JIT动态生成到CodeBuffer中的;
* 功能:HSDIS是官方推荐的HotSpot虚拟机JIT编译代码的反汇编工具,它包含在HotSpot虚拟机的源码中但没有提供编译后的程序,可以自己下载放到JDK的相关目录里;

JDK的可视化工具

JConsole Java监视与管理控制台

* 是一种基于JMX的可视化监控和管理工具,它管理部分的功能是针对MBean进行管理,由于MBean可以使用代码、中间件服务器或者所有符合JMX规范的软件进行访问,因此这里着重介绍JConsole的监控功能;
* 通过jconsole命令启动JConsole后,会自动搜索本机所有虚拟机进程。另外还支持远程进程的监控;
* 进入主界面,支持查看以下标签页:概述、内存、线程、类、VM摘要和MBean;

VisualVM 多合一故障处理工具

* 目前为止JDK发布的功能最强大的运行监控和故障处理程序,另外还支持性能分析;
* VisualVM还有一个很大的优点:不需要被监视的程序基于特殊Agent运行,对应用程序的实际性能影响很小,可直接应用在生成环境中;
* VisualVM基于NetBeans平台开发,具备插件扩展功能的特性,基于插件可以做到:显示虚拟机进程以及进程配置、环境信息(jps、jinfo)、监视应用程序的CPU、GC、堆、方法区以及线程的信息(jstat、jstack)、dump以及分析堆转储快照(jmap、jhat)、方法级的程序运行性能分析,找出被调用最多运行时间最长的方法、离线程序快照(收集运行时配置、线程dump、内存dump等信息建立快照)、其他plugins的无限可能…;
* 使用jvisualvm首次启动时需要在线自动安装插件(也可手工安装);
* 特色功能:生成浏览堆转储快照(摘要、类、实例标签页、OQL控制台)、分析程序性能(Profiler页签可以录制一段时间程序每个方法执行次数和耗时)、BTrace动态日志跟踪(不停止目标程序运行的前提下通过HotSwap技术动态加入调试代码);

总结

总结

调优案例分析与实战

除了第四章介绍的知识和工具外,在处理实际问题时,经验同样很重要。

案例分析

高性能硬件上的程序部署策略

1. 问题描述
    * 一个每天15万PV左右的在线文档网站升级了硬件,4个CPU,16GB物理内存,操作系统为64位CentOS 5.4,使用Resin作为Web服务器,没有部署其他的应用;
    * 管理员选用了64位的JDK 1.5,并通过-Xmx和-Xms参数将Java堆固定在12GB;
    * 使用一段时间不定期出现长时间失去响应的情况;
2. 问题分析
    * 升级前使用32位系统,Java堆设置为1.5GB,只是感觉运行缓慢没有明显的卡顿;
    * 通过监控发现是由于GC停顿导致的,虚拟机运行在Server模式,默认使用吞吐量优先收集器,回收12GB的堆,一次Full GC的停顿时间高达14秒;
    * 由于程序设计的原因,很多文档从磁盘加载到内存中,导致内存中出现很多由文档序列化生成的大对象,这些大对象进入了老年代,没有在Minor GC中清理掉;
3. 解决方案
    * 在虚拟机上建立5个32位的JDK逻辑集群,每个进程按2GB内存计算(其中堆固定为1.5GB),另外建议一个Apache服务作为前端均衡代理访问门户;
    * 另外考虑服务压力主要在磁盘和内存访问,CPU资源敏感度较低,因此改为CMS收集器;
    * 最终服务没有再出现长时间停顿,速度比硬件升级前有较大提升;

在高性能硬件上部署程序主要有两种方式:

1. 通过64位JDK来使用大内存;
2. 使用若干个32位JDK建立逻辑集群来利用硬件资源;

使用64位JDK管理大内存可能存在下列问题

1. 内存回收导致长时间停顿;
2. 性能测试结果普遍低于32位(之前);
3. 需要保证程序足够稳定,有的应用无法产生堆转储快照(快照大小达到几十G甚至更大),即使产生了也无法分析;
4. 相同的程序64位JDK消耗的内存比32位的要大(由于指针膨胀、数据类型对齐补白等因素);

逻辑集群方式部署程序可能会出现下列问题

1. 尽量避免节点竞争全局的资源,最典型的是磁盘竞争(各个节点同时访问某个磁盘文件并发写,很容易导致IO问题);
2. 很难高效的利用某些资源池,譬如连接池。一般都是每个节点建立自己的连接池,这会导致某些节点池满而某些节点有大量空闲连接(可以使用集中式的JNDI,复杂并且可能带来额外的性能开销);
3. 各个节点仍然不可避免受系统32位内存限制;
4. 大量使用本地缓存的应用,由于每个节点都存在一份缓存造成浪费,此时应考虑集中式缓存;

集群建同步导致的内存溢出

1. 问题描述
    * 一个基于B/S的MIS系统,硬件为两台2个CPU、8GB内存的HP小型机,服务器为WebLogic 9.2,每台机器启动了3个WebLogic实例,构建一个6台节点的亲和式集群(一个固定的用户请求永远分配到固定的节点处理);
    * 由于有部分数据需要共享,原先采用数据库,后因为读写性能问题使用了JBossCache构建了一个全局缓存;
    * 正常使用一段较长的时间,最近不定期出现了多次的内存溢出问题;
3. 问题分析
    * 监控发现,服务内存回收状况一直正常,每次内存回收后都能恢复到一个稳定的可用空间;
    * 此次未升级业务代码,排除新修改代码引入的内存泄漏问题;
    * 服务增加-XX:+HeapDumpOnOutOfMemoryError参数,在最近一次内存溢出时,分析heapdump文件发现存在大量的org.jgroups.protocols.pbcast,NAKACK对象;
    * 最终分析发现是由于JBossCache的NAKACK栈在页面产生大量请求时,有个负责安全校验的全局Filter导致集群各个节点之间网络交互非常频繁,当网络情况不能满足传输要求时,大量的需要失败重发的数据在内存中不断堆积导致内存溢出;
4. 解决方案
    * JBossCache版本改进;
    * 程序设计优化,JBossCahce集群缓存同步,不大适合有频繁写操作的情况;

堆外内存导致的内存溢出

1. 问题描述
    * 一个学校的小型项目,基于B/S的电子考试系统,服务器是Jetty 7.1.4,硬件是一台普通PC机,Core i5 CPU,4GB内存,运行32位Windows操作系统;
    * 为了实现客户端能实时地从服务器端接收考试数据,使用了逆向AJAX技术(也称为Comet或Server Side Push),选用CometD 1.1.1作为服务端推送框架;
    * 测试期间发现服务端不定期抛出内存溢出;加入-XX:+HeapDumpOnOutOfMemoryError后抛出内存溢出时什么问题都没有,采用jstat观察GC并不频繁且GC回收正常;最后在内存溢出后从系统日志发现如下异常堆栈:
异常堆栈
3. 问题分析
    * 在第二章里曾经说过直接内存溢出的场景,垃圾收集时,虚拟机虽然会对直接内存进行回收,但它只能等老年代满了触发Full GC时顺便清理,否则只能等内存溢出时catch住然后调用System.gc(),如果虚拟机还是不听(比如打开了-XX:+DisableExplictGC)则只能看着堆中还有许多空闲内存而溢出;
    * 本案例中的CometD框架正好有大量的NIO操作需要使用直接内存;

堆外内存的垃圾收集

垃圾收集时,虚拟机虽然会对Direct Memory内存进行回收,但是Direct Memory回收机制和新生代、老年代不同,当Direct Memory内存发现内存不足时,并不会通知垃圾收集器进行垃圾回收,它只能等到老年代满了之后Full GC,然后顺带的清理掉堆外内存这块空间;否则只能等到抛出内存溢出异常,crash掉,再在catch代码块里System.gc(),如果虚拟机设置了-XX:+DisableExplicitGC,明明堆中还有很多空间,但是堆外内存也只能抛出内存溢出异常。

除Java堆和永久代之外,如下区域也会占用较多内存(这里所有的内存总和受到操作系统进程最大内存限制)

1. Direct Memory内存:可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或者OutOfMemoryError:Direct buffer memory;
2. 线程堆栈:通过-Xss调整大小,内存不足时抛出StackOverflowError(纵向无法分配新的栈帧)或OutOfMemoryError:unable to create new native thread(横向无法建立新的线程);
3. Socket缓存区:每个Socket连接都有Receiver(37KB)和Send(25KB)两个缓冲区,连接多的话这块内存占用也比较多,如果无法分配,可能会抛出IOException:Too many open files异常;
4. JNI代码:程序中使用JNI调用本地库,本地库使用的内存也不在堆中;
5. 虚拟机和GC:虚拟机、GC的执行需要一定的内存;

外部命令导致系统缓慢

1. 问题描述
    * 一个数字校园应用系统,运行在一个4个CPU的Solaris 10操作系统上,中间件为GlassFish服务器;
    * 系统在做大并发压力测试时,发现请求响应时间比较慢,通过监控工具发现CPU使用率很高,并且系统占用绝大多数的CPU资源的程序并不是应用系统本身;
    * 通过Dtrace脚本发现最消耗CPU的竟然是fork系统调用(Linux用来产生新进程的);
2. 问题分析
    * 最终发现是每个用户请求需要执行一个外部的shell脚本来获取一些系统信息,是通过Runtime.getRuntime().exec()方法调用的;
    * Java虚拟机在执行这个命令时先克隆一个和当前虚拟机拥有一样环境变量的进程,再用这个新进程去执行外部命令,如果频繁地执行这个操作,系统消耗会很大;
3. 解决方案
最终修改使用Java的API去获取这些信息,系统恢复了正常;

服务器JVM进程崩溃

1. 问题描述
    * 一个基于B/S的MIS系统,硬件为两台2个CPU、8GB内存的HP系统,服务器是WebLogic 9.2(和案例"集群间同步导致的内存溢出"相同的系统);
    * 正常运行一段时间后发现运行期间频繁出现集群节点的虚拟机进程自动关闭的现象,留下一个hs_err_pid###.log,奔溃前不久都发生大量相同的异常,日志如下所示:
错误日志
3. 问题分析
    * 这是一个远端断开连接的异常,得知在MIS系统工作流的待办事项变化时需要通过Web服务通知OA门户系统;
    * 通过SoapUI测试发现调用后竟然需要长达3分钟才能返回,并且返回结果都是连接中断;
    * 由于MIS使用异步方式调用,两边处理速度不对等,导致在等待的线程和Socket连接越来越多,最终在超过虚拟机承受能力后进场奔溃;
5. 解决方案
将异步调用修改为生产者/消费者模型的消息队列处理,系统恢复正常;

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

1. 问题描述
    * 有一个后台RPC服务器,使用64位虚拟机,内存配置为-Xms4g -Xmx8g -Xmn1g,使用ParNew + CMS的收集器组合;
    * 平时Minor GC时间约在20毫秒内,但业务需要每10分钟加载一个约80MB的数据文件到内存进行数据分析,这些数据会在内存中形成超过100万个HashMap<Long, Long> Entry,在这段时间里Minor GC会超过500毫秒,这个时间过长,GC日志如下:
GC日志1

GC日志2
2. 问题分析
    * 在分析数据文件期间,800M的Eden空间在Minor GC后对象还是存活的,而ParNew垃圾收集器使用的是复制算法,把这些对象复制到Survivor并维持这些对象引用成为沉重的负担,导致GC时间变长;
    * 从GC可以将Survivor空间去掉(加入参数-XX:SurvivorRatio=65536、-XX:MaxTenuringThreshold=0或者-XX:AlwaysTenure),让新生代存活的对象第一次Minor GC后立即进入老年代,等到Major GC再清理。这种方式可以治标,但也有很大的副作用;
    * 另外一种是从程序设计的角度看,HashMap<Long, Long>结构中,只有key和value所存放的两个长整形数据是有效数据,共16B(2 * 8B),而实际耗费的内存位88B(长整形包装为Long对象需要多8B的MarkWord、8B的Klass指针,Map.Entry多了16B的对象头、8B的next字段和4B的int型hash字段、为对齐添加的4B空白填充,另外还有8B的Entry引用),内存空间效率(18%)太低;

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

1. 问题描述
    * 有一个带心跳检测功能的GUI桌面程序,每15秒发送一次心跳检查信号,如果对方30秒内都没有信信号返回,则认为和对方已断开连接;
    * 程序上线后发现有误报,查询日志发现误报是因为程序会偶尔出现间隔约1分钟左右的时间完全无日志输出,处于停顿状态;
    * 另外观察到GUI程序最小化时,资源管理中显示的占用内存大幅减小,但虚拟内存没变化;
    * 因为是桌面程序,所需内存不大(-Xmx256m),加入参数-XX:+PrintGCApplicationStoppedTime -XX:PrintGCDateStamps -Xloggc:gclog.log后,从日志文件确认是GC导致的,大部分的GC时间在100ms以内,但偶尔会出现一次接近1min的GC;
    * 加入参数-XX:PrintReferenceGC参数查看GC的具体日志信息,发现执行GC动作的时间并不长,但从准备开始GC到真正GC直接却消耗了大部分时间,如下所示:
GC日志
2. 问题分析
    * 初步怀疑是最小化时工作内存被自动交换到磁盘的页面文件中,这样发生GC时就有可能因为恢复页面文件的操作而导致不正常的GC停顿;
    * 在MSDN查证确认了这种猜想,加入参数-Dsun.awt.keepWorkingSetOnMinimize=true来解决;这个参数在很多AWT程序如VisualVM都有应用。

Eclipse运行速度调优

* 升级JDK;
* 设置-XX:MaxPermSize=256M解决Eclipse判断虚拟机版本的bug;
* 加入参数-Xverfify:none禁止字节码验证;
* 虚拟机运行在client模式,采用C1轻量级编译器;
* 把-Xms和-XX:PermSize参数设置为-Xmx和-XX:MaxPermSize一样,这样强制虚拟机启动时把老年代和永久代的容量固定下来,避免运行时自动扩展;
* 增加参数-XX:DisableExplicitGC屏蔽掉显式GC触发;
* 采用ParNew+CMS的垃圾收集器组合;
最终从Eclipse启动耗时15秒到7秒左右, eclipse.ini配置如下:
配置参数

总结

总结
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266