JVM性能问题的处理依据:运行日志与异常堆栈、GC日志(printGCDetails)、线程快照(threaddump/javacore)、堆转储快照(heapdump)。
一、工具
1. JDK命令行工具
1)jps:process status,输出进程ID,main class;提供多个参数:-v输出虚拟机启动参数;-l输出主类的全名/jar包的地址;-m输出main函数接收的参数
2)jinfo:用于查看、调整VM配置。jps -v只查看显式指定的VM参数,但jinfo查看所有参数,并修改允许运行时修改的参数。jinfo pid;jinfo -flag <flagName> <newFlagValue>。
3)jstat:显示VM运行数据statistics。服务器上定位VM性能问题的首选,但可视化不及VisualVM。jstat-<option> vmid interval count,如jstat-gc 2764 250 20表示每250ms查看一次2764进程的GC情况,共20次。提供option包括1)class:监测类加载、卸载;2)gc:监测Eden、Survivor、老年代、永久代的容量、已用空间、GC时间;3)gccause:上次GC原因;4)compile:输出JIT编译过的方法,耗时。
4)jmap:提供option包括:1)dump:生成heapdump;2)finalizerinfo:查询finalise执行队列;3)heap:显示堆信息。
5)jhat:heap analysis tool,分析heapdump并用html展示结果。分析工作耗时耗CPU,通常拷贝到其他机器用visual VM等工具分析,而不是在运行应用服务器用jhat分析。相对于Visual VM,jhat功能简陋,Intellij没有捆绑的分析工具。
6)jstack:线程栈快照,分析线程长时间停顿原因,如死锁/死循环/请求外部资源等待。参数:-F无响应时强制输出线程栈;-l锁的附加信息;-m本地方法栈;Thread类的getAllStackTrace可实现管理员页面,通过浏览器查看线程堆栈。
2. JDK的可视化工具
1)Jconsole:运行jdk/bin下面的jconsole.exe启动Jconsole,自动搜索虚拟机进程,双击某进程进行监控,支持监控远程虚拟机;展示堆使用情况,线程数目等。
2)VisualVM(主流):基于插件可扩展,显示进程/进程配置信息,生成和浏览heapdump。性能分析profiler:运行时分析方法级的CPU执行时间和内存,对程序运行效率影响大,通常不用于生产环境;CPU分析统计每个方法的执行次数/耗时;内存分析统计每个方法关联的对象数和对象所占空间。BTrace:在不停止目标程序运行的前提下,动态加入原本不存在的调试代码
二、调优案例实战与分析
1. 高性能硬件上的服务部署策略🌟
问题:在线文档网站,原有硬件1.5G堆内存,用户感觉使用缓慢;进行了硬件升级16G内存,12G堆内存,反而明显卡顿;系统监控显示经常出现长时间失去响应的情况。
原因:使用吞吐量优先的收集器,频繁发生Full GC,每次耗时14秒;频繁Full GC的原因:文档反序列化生成的大对象未被minor GC回收,直接进老年代导致老年代很快满载
解决方式:1. 通过64位JDK使用大内存,用户交互型(时间敏感)应用必须控制full GC次数(如每天一次),深夜定时触发full GC。控制full GC次数的关键是多数对象(尤其大对象)符合朝生夕死,以保障老年代的稳定。网站应用中对象的生命周期多是请求/页面级,代码合理情况下能保证Full GC的频率。当前64位JDK的问题在于:性能低于32位,堆溢出无法生成dump文件,或dump文件过大难以分析。2. 若干32位虚拟机建立逻辑集群,在物理机启动多个应用进程,分配不同端口,通过负载均衡器分配访问请求;问题:容易出现资源竞争,比如磁盘资源竞争引起的IO异常。
最终解决方案:调整为4个32位JDK的逻辑集群;使用CMS收集器
2. 集群间同步导致的内存溢出
问题:MIS系统;硬件是两台小型机,每个机器部署3个实例,共享数据存储在数据库和全局缓存。全局缓存启用后,服务正常使用了一段时间,最近不定期出现内存溢出。
排查原因:监测GC,发现内存不溢出时GC运行合理:每次GC后内存都回到合理水平;没有代码更新,排除代码引起的内存泄漏。让应用带HeapDumpOnOutOfMemoryError参数执行,管理员拿到最近内存溢出后的heapdump文件,发现大量NAKACK对象,该对象服务于全局缓存。问题一部分在于JBossCache本身的缺陷(论坛上有讨论,并且后续有版本更新),另一部分在于MIS系统实现方式的缺陷(使用全局缓存时可以有频繁的读操作,但不适合有频繁的写操作)
3. 堆外内存导致的溢出🌟
问题&排查:基于B/S架构的电子考试系统;测试发现服务器不定时抛出OOM异常,管理员把内存调大后OOM异常更频繁;加入HeapDumpOnOutOfMemory参数,没有输出;运行jstat,堆/方法区内存压力不大。日志中错误堆栈:OOM-null,直接内存溢出。
原因:除了堆内存,直接内存/栈内存/socket缓存区/JNI代码/虚拟机和GC也需要占用内存,本例中硬件2G内存,堆内存1.6G,那堆外内存<0.4G;不同于新生代/老年代,直接内存不足时不会通知收集器进行GC,只能等待Full GC发生时顺带清理直接内存,否则内存溢出并在catch语句中调用system.gc触发full GC,如果虚拟机打开了DisableExplicitGC,gg。
Socket缓存区:每个socket连接都需要receive和send两个缓存区,连接数过多也会OOM:Too many open files;JNI用于调用本地库;虚拟机和GC执行占用内存。
4. 外部命令导致系统缓慢
问题:数字校园系统,压力测试发现响应慢,监控发现CPU占用率高,且占用CPU多数资源的并非应用本身(应用的CPU利用率应占主要地位)。
排查:通过Dtrace发现fork大量占用CPU,linux用fork产生新进程。检查代码大量调用Runtime.getRuntime().exec()执行外部shell脚本获取系统信息,它会克隆一个和当前虚拟机拥有相同环境变量的进程,用于执行外部命令,最后退出进程;频繁调用产生的创建进程的开销大。
5. 服务器JVM进程崩溃
问题:MIS系统,硬件是2个HP系统(2个CPU,8G内存),正常运行一段时间后,频繁出现虚拟机进程自动关闭。
排查:崩溃前错误日志包含大量connection reset远程断开链接异常。该系统最近与另一个系统A集成,通过soapui调用系统A,发现要3分钟才能响应,且经常出现断开连接。MIS系统虽然采用异步方式调用系统A,但速度完全不对等导致等待的线程和socket连接越来越多,最终导致虚拟机崩溃。
解决:通知A系统修复无法使用的接口;异步调用改为生产者/消费者模式的消息队列。
6. 不恰当数据结构导致内存占用过大
问题:后台服务器,采用ParNew+CMS收集器,平时minorGC只需要30ms,但业务上需要每10分钟加载一个80M的文件进行处理,在内存中形成100万个HashMap entry,这段时间的Minor GC会造成0.5s停顿。因为加载文件时新生代迅速填满从而引发Minor GC,同时对象仍然存活引发大量拷贝。
解决方案:1.不修改程序,仅从GC调优的角度解决:去掉survivor,新生代的存活对象直接进入老年代。2.治本的方案是修改代码,不用hashMap存储数据。hashmap存储数据空间效率低,HashMap中key和value是有效数据,两个长整型共16字节; 长整型包装为Long需要叠加8B的Markword,8B的Klass指针,膨胀为24字节。两个Long组成hashmap entry还需要16B的对象头,8B的next字段,4B的hash字段,和为了对齐的4B填充,以及hashMap对entry的8B引用,实际消耗内存88B。空间利用率16/88
三、Eclipse启动速度调优
启动时间包含:GC4.2秒,full GC19次,共3.1s;minorGC 378次,共1.1s;类加载:9115个,4.1s;JIT编译时间:2s,由后台线程完成。
如何优化:
1)虚拟机版本升级,希望从虚拟机本身得到免费的性能提升。升级之后意外出现OOM,打开visual VM,查看堆曲线和永久代曲线。堆曲线正常:堆大小的曲线和使用的堆的曲线一直存在很大的间隔,每当两条线接近时,最大堆的曲线会快速向上转向(堆扩容),而使用的堆曲线则向下(触发了一次GC)。永久代曲线不正常:永久代大小曲线和使用曲线几乎重合,说明永久代没有可回收的资源,也没有可扩展空间,因此内存溢出是永久代引起的。通过配置MaxPermSize来解决,默认值64M。
2)调整内存设置控制GC频率:Minor GC频繁,新生代空间太小,通过-Xmn设置。Full GC执行效率:回收时只做了扩容,回收效率低,固定堆内存和永久代大小,避免内存自动扩展。Eclipse用户通常选择run in background,CMS适合后台运行,在eclipse.ini文件中加入jvm配置参数。