JVM(五)JVM调优
文章目录
- 一、调优实践
- 1.1 规划
- 1.1.1 规划步骤
- 1.1.2 规划案例
- 1.2 解决JVM运行过程中出现的问题
- 1.2.1 用jstack定位锁相关问题
- 1.2.2 OOM问题的定位方式
- 1.3 arthas在线排查工具
- 1.3.1 用jhat分析dump文件
- 1.3.2 用JVirtualVM分析dump文件
- 1.3.3 arthas的特有功能
- 1.4 生产上如何配置垃圾收集器的
- 二、OOM问题调优
- 三、JVM调优案例
- 3.1 高性能硬件上的程序部署策略
- 3.2 集群间同步导致的内存溢出
- 3.3 堆外内存导致的溢出错误
- 3.4 外部命令导致系统缓慢
- 3.5 服务器JVM进程崩溃
- 3.6 不恰当数据结构导致内存占用过大
- 3.7 服务器升级后效率变低
- 四、JVM调优相关问题
本系列文章:
JVM(一)Java运行时区域、对象的使用
JVM(二)垃圾回收
JVM(三)类文件结构、类加载机制
JVM(四)JVM调试命令、JVM参数
JVM(五)JVM调优
一般垃圾收集器跟内存大小的对应关系:
- Serial:几十M
- PS:上百M - 几个G
- CMS:20G
- G1:上百G
- ZGC:4T - 16T(JDK13)
一、调优实践
调优可以简单分为三个方面:
根据需求进行JVM规划和预调优
优化JVM运行环境(慢,卡顿)
解决JVM运行过程中出现的各种问题(OOM)
1.1 规划
调优,从业务场景开始,没有业务场景的调优都没有意义。
无监控(也就是要进行压力测试,这样能看到结果),不调优。
1.1.1 规划步骤
参考调优步骤:
- 1、熟悉业务场景
(没有最好的垃圾回收器,只有最合适的垃圾回收器),根据业务场景来选择垃圾回收器。
响应时间、停顿时间 [CMS、G1、ZGC] (需要给用户作响应)
吞吐量 = 用户时间 /( 用户时间 + GC时间) [PS]
- 2、选择回收器组合
JVM调优(垃圾回收器方面)指什么?两个目标:1、尽量减少FGC,将转移到老年代的对象数量降低到最小;2、减少STW时间。
1.吞吐量
:用户代码执行时间 /(用户代码执行时间 + 垃圾回收时间)
2.响应时间
:STW越短,响应时间越好
所谓调优,首先确定目的?吞吐量优先,还是响应时间优先?还是在满足一定的响应时间的情况下,要求达到多大的吞吐量?
比如科学计算/数据挖掘,追求
吞吐量优先。该种情况一般选用PS + PO
。
网站、带界面的程序、对外程序的API,一般是响应时间优先。看JDK版本,优先选G1
。
- 3、计算内存需求
该步骤比较难以计算,范围较大,内存较小的话,可以回收的频繁一些。 - 4、选定CPU
- 5、设定年代大小、升级年龄
- 6、设定日志参数
两个例子:
1)滚动日志:
-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
上面的命令意思是:按GC时间来生成滚动日志,最多可以生成5个,每个最大20M。
2)或者每天产生一个日志文件
1.1.2 规划案例
看一些案例:
- 案例1:垂直电商,最高每日百万订单,处理订单系统需要什么样的服务器配置?
这个问题不太专业,因为垂直电商不可能做到每日百万订单。
遇到这种问法时,还是需要去分析。比如考虑高峰访问量,假设一小时产生36w订单,即100订单/秒,高峰就再次假设1000订单/秒。
接下来较考虑一个订单产生多少内存。即new出来订单对象,需要多少内存
。假设一个订单对象为512k,1000订单总和是500M左右。
这样新生代设置500M就可以,当然250M也可以,多回收几次就行。所以此时一般有响应时间要求,即在多少响应时间(比如100ms)内进行设计,然后进行压测。
初次设定参数后,就可以进行压测,满足不了要求就扩大参数,再不行就加服务器数量
。 - 案例2:12306遭遇春节大规模抢票应该如何支撑?
12306应该是中国并发量最大的秒杀网站,号称并发量100W最高CDN -> LVS -> NGINX -> 业务系统 -> 每台机器1W并发(10K问题) 100台机器
一般先从CDN开始,在全国做不同的CDN缓存,接下来是一堆的LVS,接下来就是NGINX,接下来就是Tomcat等服务器。
Redis可以撑得住单机1w并发。
此外,架构设计也是和业务逻辑紧密相关的。
在商城付款流程中,普通电商订单 -> 下单 ->订单系统(IO)减库存,减库存和订单的生成应该是异步进行的,最后一步是用户付款。
在具体的功能模块,比如订单生成,最后还会把压力压到一台服务器,可以做分布式本地库存 + 单独服务器做库存均衡。
大流量的处理方法:分而治之
。
1.2 解决JVM运行过程中出现的问题
- 问题1、系统CPU经常100%,如何调优
CPU100%那么一定有线程在占用系统资源。
- 找出哪个进程cpu高(top)
- 该进程中的哪个线程cpu高(top -Hp)
- 导出该线程的堆栈 (jstack)
- 查找哪个方法(栈帧)消耗时间 (jstack)
CPU经常100%,需要考虑:工作线程占比高和垃圾回收线程占比高两种情况。
- 问题2、系统内存飙高,如何查找问题
- 导出堆内存 (jmap)
- 分析 (jhat jvisualvm mat jprofiler … )
- 问题3、如何监控JVM
使用jstat、top等命令,和jvisualvm、jprofiler、arthas等工具。
1.2.1 用jstack定位锁相关问题
用一个例子来尝试分析问题:
package com.test.jvm.gc;import java.math.BigDecimal;import java.util.ArrayList;import java.util.Date;import java.util.List;import java.util.concurrent.ScheduledThreadPoolExecutor;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;/*** 从数据库中读取信用数据,套用模型,并把结果进行记录和传输*/public class T15_FullGC_Problem01 {private static class CardInfo {BigDecimal price = new BigDecimal(0.0);String name = "张三";int age = 5;Date birthdate = new Date();public void m() {}}private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,new ThreadPoolExecutor.DiscardOldestPolicy());public static void main(String[] args) throws Exception {executor.setMaximumPoolSize(50);for (;;){modelFit();Thread.sleep(100);}}private static void modelFit(){List<CardInfo> taskList = getAllCardInfo();taskList.forEach(info -> {// do somethingexecutor.scheduleWithFixedDelay(() -> {//do sth with infoinfo.m();}, 2, 3, TimeUnit.SECONDS);});}private static List<CardInfo> getAllCardInfo(){List<CardInfo> taskList = new ArrayList<>();for (int i = 0; i < 100; i++) {CardInfo ci = new CardInfo();taskList.add(ci);}return taskList;}}
使用java -Xms200M -Xmx200M -XX:+PrintGC com.test.jvm.gc.T15_FullGC_Problem01
让程序运行,输出GC信息。
一般公司都使用网管监控软件,检测服务器,可以进行告警等操作。一般是运维团队首先受到报警信息(CPU Memory)
接着开发才会进行下一步的定位分析。首先要先使用top
命令找到内存使用和CPU占比较高的进程。
然后用top -Hp + 进程ID
查看该进程内的线程内存使用和CPU占比情况,观察进程中的线程,找到哪个线程CPU和内存占比高。示例:
top命令查看的是所有的进程信息,jps可以查看Java进程信息。
图中第一列的PID就是该进程内的线程号。接下来就要用jstack
命令定位具体的线程情况。"jstack + 进程ID"会把该进程的线程情况都列出来:
上图中的NID是十六进制的线程号,用top -Hp + 进程ID
命令看到的线程号是十进制的。
此时就可以看到每个线程的状况,要重点关注的是线程的异常状态,如:WAITING、BLOCKED
jstack中的主要线程状态:
RUNNABLE 线程运行中或I/O等待
BLOCKED 线程在等待monitor锁(synchronized关键字)
TIMED_WAITING 线程在等待唤醒,但设置了时限
WAITING 线程在无限等待唤醒
看一段关键的日志信息:
图中的"t2"是示例的用户线程名称,状态是WAITING,有这么一段:
waiting on <0x0000000088ca3310> (a java.lang.Object)
即在等待着一把锁的释放。假如有一个进程中100个线程,很多线程都在waiting on <xx>
,一定要找到是哪个线程持有这把锁,这时候一般是这个线程长期持有这把锁不释放。怎么找?搜索jstack dump的信息,找<xx>
,看哪个线程持有这把锁,状态一般是RUNNABLE。
同时,此时也能看到出问题代码的具体位置:
此时就明白阿里Java开发规范中,线程的名称(尤其是线程池)都要写有意义的名称。在使用线程池时,自定义线程名称的方式是:自定义ThreadFactory。
1.2.2 OOM问题的定位方式
在上面的实验中,用到了jstack、top、top -Hp等命令,当然还有别的命令可以使用:
jps,查看Java进程信息
jinfo,查看一些配置信息,用法是jinfo+进程号,示例:
jstat,查看一些进程信息,但是内容较乱,不常用:
比如jstat -gc 动态观察gc情况;jstat -gc 4655 500 : 每过500个毫秒,动态打印GC的情况
远程的服务器一般是不安装图形化界面的,所以可以在本地和远程服务器建立连接,此时有个标准的协议JMX。也就是说如果要在本地和远程服务器建立连接,就需要在远程服务器上进行JMX的相关配置。一些配置的示例:
- 程序启动加入参数:
shell
java -Djava.rmi.server.hostname=192.168.17.11 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=11111 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false XXX
- 如果遭遇 Local host name unknown:XXX的错误,修改/etc/hosts文件,把XXX加入进去
192.168.17.11 basic localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
- 关闭linux防火墙(实战中应该打开对应端口)
service iptables stop
chkconfig iptables off #永久关闭
- windows上打开 jconsole远程连接 192.168.17.11:11111
JConsole是JDK自带的图形化CPU监测工具,如果要连接远程服务器,需要连接远程进程:
JConsole连接成功后的界面:
类似工具JVirtualVM界面:
该工具上添加远程连接,成功后界面:
JVirtualVM能看到CPU、类、堆、线程的一些信息。
下面的这张图是最直观的信息显示:有多少类,占多少个字节,有多少个实 例。了解这些信息,也大致能够进行问题定位了,因为有大量的对象未被回收,一定是相关代码出了问题。通过这种图形化界面工具,能够较简单地定位到OOM问题的原因
。
那怎么定位OOM问题的?不是通过图形化工具。因为如果通过图形化界面定位OOM问题的话,代表在远程服务器上一直有个服务在后台运行。那么此时就会有两个问题:
- 已经上线的系统不用图形界面用什么来定位OOM问题?
1)可以用arthas等命令行模拟图形化界面
2)线上系统一般会配置一个参数:java -Xms20M -Xmx20M -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError
,这个参数代表OOM的时候会自动产生堆转储文件。
3)很多服务器备份(高可用),停掉这台服务器对其他服务器不影响
4)在线定位(一般小点儿公司用不到)
那要获取和JVirtualVM相似的查看对象数量、占用字节相似效果的话,需要用什么么?jmap。示例:jmap - histo 4655 | head -20
,查看数量排名前20的对象信息:
注意在线上系统中,尽量不使用jmap -dump:format=b,file=xxx pid
类似的命令在线转储日志,jmap执行期间会对进程产生很大影响,甚至卡顿(电商不适合)。
2. 图形界面到底用在什么地方?测试!测试的时候进行监控!(压测观察)
总结来说:先由运维团队报告问题,如CPU高、内存占用高等--->用top命令查出出问题的线程--->如果是锁相关的问题,就继续用stack进行定位到具体线程--->如果发现频繁GC,就用jmap定位到是什么对象一直占用内存,未被回收
。
如果是数据库连接未释放之类的问题,不容易通过上述调试JVM的方式看出,需要看数据库连接池日志。
1.3 arthas在线排查工具
- 为什么需要在线排查
在生产上我们经常会碰到一些不好排查的问题,例如线程安全问题,用最简单的threaddump或者heapdump不好查到问题原因。为了排查这些问题,有时我们会临时加一些日志,比如在一些关键的函数里打印出入参,然后重新打包发布,如果打了日志还是没找到问题,继续加日志,重新打包发布。对于上线流程复杂而且审核比较严的公司,从改代码到上线需要层层的流转,会大大影响问题排查的进度。
线上系统一般不用图形化工具来排查问题,因为远程服务器没有装,如果本地连接远程服务器的话,还需要管理员来权限之类的。
arthas不包含jmap功能
。
arthas是阿里的开源在线分析工具。该工具的下载安装可以在github上寻找:
该工具的文件目录:
可以通过命令的方式,启动:
此时输入"1",就可以将arthas绑定在该进程上,然后就可以使用arthas相关命令来观察该进程。绑定成功:
help
可以查看常用命令:
jvm
命令,可以查看JVM的相关信息:
thread
命令可以查看线程相关情况:
thread + 线程号
,可以查看某个线程的详细情况:
dashboard
命令,观察系统情况,类似于top
命令效果:
heapdump
命令可以导出dump文件:
1.3.1 用jhat分析dump文件
此时可以用jhat命令分析.hprof(dump)文件:
图中表示用最多512M内存来分析,分析4244718个对象。
jhat命令起了Server,7000端口,所以可以在浏览器进行访问jhat解析过的内容。示例:
该界面对底部,还可以查看其它问题
点击第三个、第四个可以查看对象的数量,类似于jmap:
最底部的"OQL Query",可以查询特定问题对象:
点某个,可以看某个对象所占用的字节数和相关的引用:
1.3.2 用JVirtualVM分析dump文件
生产dump文件后,当然也可以用本地图形化工具分析,比如JVirtualVM:
也可以使用OQL查询。
1.3.3 arthas的特有功能
- 1、反编译
jad
命令可以用来反编译:
该功能可以排查动态代理相关的问题;还可以排查版本问题,即提交的代码是否被使用。 - 2、热替换
目前有些限制条件:只能改方法实现(方法已经运行完成),不能改方法名, 不能改属性。
假设有这样两个文件:
然后用javac命令编译这两个java文件。然后目前的情况是运行T,再随便输入,就会输出1:
此时直接改TT.java,改成输出"2",然后编译TT.java,接着:
就已经完成了热替换:
1.4 生产上如何配置垃圾收集器的
首先是内存大小问题,基本上每一个内存区域都可以设置一个上限,来避免溢出问题,比如元空间。通常,堆空间我会设置成操作系统的2/3(这是想给其他进程和操作系统预留一些时间),超过8GB的堆优先选用G1。
可以对JVM进行初步优化。比如根据老年代的对象提升速度,来调整年轻代和老年代之间的比例。
再接下来,就是专项优化,主要判断的依据就是系统容量、访问延迟、吞吐量等。比如服务是高并发的,那么对STW的时间就会非常敏感。
接下来可以通过记录详细的GC日志,来找到这个瓶颈点,借用gceasy之类的日志分析工具,定位到问题。之所以选择采用工具,是因为gc日志看起来实在是太麻烦了,gceasy号称是AI学习分析问题,可视化做的较好。
二、OOM问题调优
OOM产生的原因多种多样,有些程序未必产生OOM,不断FGC(CPU飙高,但内存回收特别少)。下面是一些可能会产生OOM的原因。
- 1、硬件升级系统反而卡顿的问题
原因:堆内存变大,FGC的时间也会变长了。
解决方法:使用合适的垃圾回收器。 - 2、线程池不当运用产生OOM问题
原因:利用Executors创建线程池(四种方式都有隐藏风险)会造成OOM问题。
解决方法:使用ThreadPoolExecutor方式创建线程池。 - 3、jira问题(排查不出真实原因的问题)
现象是系统卡顿,从log来看不断FGC,查不出原因。
像这种问题的通常解决方法:加内存 + 更换垃圾回收器(如G1)。 - 4、tomcat http-header-size过大问题
Tomcat配置中有个字数:max-http-header-size,该参数设置过大会产生问题,每来一个请求就会占用这么多内存(单位:字节),导致OOM。出问题的对象是Http11OutputBuffer,该对象产生过多:
解决方法:将该参数调小。 - 5、栈溢出问题(较简单)
java.lang.StackOverflowError栈内存溢出。原因:-Xss设定太小。解决方法:将该参数设置大点。
一个栈溢出的小例子:
public class StackOverFlow {public static void main(String[] args) {m();}static void m() { m(); }
}
- 6、比较一下这两段程序的异同,分析哪一个是更优的写法【非案例】
Object o = null;
for(int i=0; i<100; i++) {o = new Object();//业务处理
}
for(int i=0; i<100; i++) {Object o = new Object();
}
第一种写法较好,因为当重新创建对象时,之前创建的对象就可以回收,而第二种不能回收。
- 7、重写finalize引发频繁GC
现象是卡顿,原因是C++程序员写Java代码,仿照C++写法,重写了finalize方法,并且在finalize方法中进行了耗时操作,到时频繁GC。 - 8、如果有一个系统,内存一直消耗不超过10%,但是观察GC日志,发现FGC总是频繁产生(较简单)
原因:有人显式调用了System.gc()。
三、JVM调优案例
3.1 高性能硬件上的程序部署策略
例如,一个15万PV(页面浏览量)/天左右的在线文档类型网站最近更换了硬件系统,新的硬件为4个CPU、16GB物理内存,操作系统为64位CentOS 5.4,Resin作为Web服务器。整个服务器暂没有部署别的应用,所有硬件资源都可以提供给这访问量并不算太大的网站使用。管理员为了尽量利用硬件资源选用了64位的JDK1.5,并通过-Xmx和-Xms参数将Java堆固定在12GB。使用一段时间后发现使用效果并不理想,网站经常不定期出现长时间失去响应的情况。
监控服务器运行状况后发现网站失去响应是由GC停顿导致的,虚拟机运行在Server模式,默认使用吞吐量优先收集器,回收12GB的堆,一次Full GC的停顿时间高达14秒。并且由于程序设计的关系,访问文档时要把文档从磁盘提取到内存中,导致内存中出现很多由文档序列化产生的大对象,这些大对象很多都进入了老年代,没有在Minor GC中清理掉。这种情况下即使有12GB的堆,内存也很快被消耗殆尽,由此导致每隔十几分钟出现十几秒的停顿,令网站开发人员和管理员感到很沮丧。
这里先不延伸讨论程序代码问题,程序部署上的主要问题显然是过大的堆内存进行回收时带来的长时间的停顿。硬件升级前使用32位系统1.5GB的堆,用户只感觉到使用网站比较缓慢,但不会发生十分明显的停顿,因此才考虑升级硬件以提升程序效能,如果重新缩小给Java堆分配的内存,那么硬件上的投资就显得很浪费。
在高性能硬件上部署程序,目前主要有两种方式:
通过64位JDK来使用大内存。
使用若干个32位虚拟机建立逻辑集群来利用硬件资源。
此案例中的管理员采用了第一种部署方式。对于用户交互性强、对停顿时间敏感的系统,可以给Java虚拟机分配超大堆的前提是有把握把应用程序的Full GC频率控制得足够低,至少要低到不会影响用户使用
,譬如十几个小时乃至一天才出现一次Full GC,这样可以通过在深夜执行定时任务的方式触发Full GC甚至自动重启应用服务器来保持内存可用空间在一个稳定的水平。
控制Full GC频率的关键是看应用中绝大多数对象能否符合“朝生夕灭”的原则,即大多数对象的生存时间不应太长,尤其是不能有成批量的、长生存时间的大对象产生,这样才能保障老年代空间的稳定
。
在大多数网站形式的应用里,主要对象的生存周期都应该是请求级或者页面级的,会话级和全局级的长生命对象相对很少
。只要代码写得合理,应当都能实现在超大堆中正常使用而没有Full GC,这样的话,使用超大堆内存时,网站响应速度才会比较有保证。除此之外,如果读者计划使用64位JDK来管理大内存,还需要考虑下面可能面临的问题:
内存回收导致的长时间停顿。
现阶段,64位JDK的性能测试结果普遍低于32位JDK。
需要保证程序足够稳定,因为这种应用要是产生堆溢出几乎就无法产生堆转储快照(因为要产生十几GB乃至更大的Dump文件),哪怕产生了快照也几乎无法进行分析。
相同程序在64位JDK消耗的内存一般比32位JDK大,这是由于指针膨胀,以及数据类型对齐补白等因素导致的。
上面的问题听起来有点吓人,所以现阶段不少管理员还是选择第二种方式:使用若干个32位虚拟机建立逻辑集群来利用硬件资源。具体做法是在一台物理机器上启动多个应用服务器进程,每个服务器进程分配不同端口,然后在前端搭建一个负载均衡器,以反向代理的方式来分配访问请求
。读者不需要太过在意均衡器转发所消耗的性能,即使使用64位JDK,许多应用也不止有一台服务器,因此在许多应用中前端的均衡器总是要存在的。
考虑到在一台物理机器上建立逻辑集群的目的仅仅是为了尽可能利用硬件资源,并不需要关心状态保留、热转移之类的高可用性需求,也不需要保证每个虚拟机进程有绝对准确的均衡负载,因此使用无Session复制的亲合式集群是一个相当不错的选择。我们仅仅需要保障集群具备亲合性,也就是均衡器按一定的规则算法(一般根据SessionID分配)将一个固定的用户请求永远分配到固定的一个集群节点进行处理即可,这样程序开发阶段就基本不用为集群环境做什么特别的考虑了。
当然,很少有没有缺点的方案,如果读者计划使用逻辑集群的方式来部署程序,可能会遇到下面一些问题:
- 尽量避免节点竞争全局的资源,最典型的就是磁盘竞争,各个节点如果同时访问某个磁盘文件的话(尤其是并发写操作容易出现问题),很容易导致IO异常。
- 很难最高效率地利用某些资源池,譬如连接池,一般都是在各个节点建立自己独立的连接池,这样有可能导致一些节点池满了而另外一些节点仍有较多空余。尽管可以使用集中式的JNDI,但这个有一定复杂性并且可能带来额外的性能开销。
- 各个节点仍然不可避免地受到32位的内存限制,在32位Windows平台中每个进程只能使用2GB的内存,考虑到堆以外的内存开销,堆一般最多只能开到1.5GB。在某些Linux或UNIX系统(如Solaris)中,可以提升到3GB乃至接近4GB的内存,但32位中仍然受最高4GB(2 32 )内存的限制。
- 大量使用本地缓存(如大量使用HashMap作为K/V缓存)的应用,在逻辑集群中会造成较大的内存浪费,因为每个逻辑节点上都有一份缓存,这时候可以考虑把本地缓存改为集中式缓存。
介绍完这两种部署方式,再重新回到这个案例之中,最后的部署方案调整为建立5个32位JDK的逻辑集群,每个进程按2GB内存计算(其中堆固定为1.5GB),占用了10GB内存
。
另外建立一个Apache服务作为前端均衡代理访问门户
。
考虑到用户对响应速度比较关心,并且文档服务的主要压力集中在磁盘和内存访问,CPU资源敏感度较低,因此改为CMS收集器进行垃圾回收
。部署方式调整后,服务再没有出现长时间停顿,速度比硬件升级前有较大提升。
3.2 集群间同步导致的内存溢出
例如,有一个基于B/S的MIS系统,硬件为两台2个CPU、8GB内存的HP小型机,服务器是WebLogic 9.2,每台机器启动了3个WebLogic实例,构成一个6个节点的亲合式集群。由于是亲合式集群,节点之间没有进行Session同步,但是有一些需求要实现部分数据在各个节点间共享。开始这些数据存放在数据库中,但由于读写频繁竞争很激烈,性能影响较大,后面使用JBossCache构建了一个全局缓存。全局缓存启用后,服务正常使用了一段较长的时间,但最近却不定期地出现了多次的内存溢出问题。
在内存溢出异常不出现的时候,服务内存回收状况一直正常,每次内存回收后都能恢复到一个稳定的可用空间,开始怀疑是程序某些不常用的代码路径中存在内存泄漏,但管理员反映最近程序并未更新、升级过,也没有进行什么特别操作。只好让服务带着-XX:+HeapDumpOnOutOfMemoryError参数运行了一段时间。在最近一次溢出之后,管理员发回了heapdump文件,发现里面存在着大量的org.jgroups.protocols.pbcast.NAKACK对象。
BossCache是基于自家的JGroups进行集群间的数据通信,JGroups使用协议栈的方式来实现收发数据包的各种所需特性自由组合,数据包接收和发送时要经过每层协议栈的up()和down()方法,其中的NAKACK栈用于保障各个包的有效顺序及重发。JBossCache协议栈如图5-1所示。
由于信息有传输失败需要重发的可能性,在确认所有注册在GMS(Group MembershipService)的节点都收到正确的信息前,发送的信息必须在内存中保留。而此MIS的服务端中有一个负责安全校验的全局Filter,每当接收到请求时,均会更新一次最后操作时间,并且将这个时间同步到所有的节点去,使得一个用户在一段时间内不能在多台机器上登录。在服务使用过程中,往往一个页面会产生数次乃至数十次的请求,因此这个过滤器导致集群各个节点之间网络交互非常频繁。当网络情况不能满足传输要求时,重发数据在内存中不断堆积,很快就产生了内存溢出。
这个案例中的问题,既有JBossCache的缺陷,也有MIS系统实现方式上缺陷。JBossCache官方的maillist中讨论过很多次类似的内存溢出异常问题,据说后续版本也有了改进。而更重要的缺陷是这一类被集群共享的数据要使用类似JBossCache这种集群缓存来同步的话,可以允许读操作频繁,因为数据在本地内存有一份副本,读取的动作不会耗费多少资源,但不应当有过于频繁的写操作,那样会带来很大的网络同步的开销。
3.3 堆外内存导致的溢出错误
例如,一个学校的小型项目:基于B/S的电子考试系统,为了实现客户端能实时地从服务器端接收考试数据,系统使用了逆向AJAX技术(也称为Comet或者Server Side Push),选用CometD 1.1.1作为服务端推送框架,服务器是Jetty 7.1.4,硬件为一台普通PC机,Core i5CPU,4GB内存,运行32位Windows操作系统。
测试期间发现服务端不定时抛出内存溢出异常,服务器不一定每次都会出现异常,但假如正式考试时崩溃一次,那估计整场电子考试都会乱套,网站管理员尝试过把堆开到最大,而32位系统最多到1.6GB就基本无法再加大了,而且开大了基本没效果,抛出内存溢出异常好像还更加频繁了。加入-XX:+HeapDumpOnOutOfMemoryError,居然也没有任何反应,抛出内存溢出异常时什么文件都没有产生。无奈之下只好挂着jstat并一直紧盯屏幕,发现GC并不频繁,Eden区、Survivor区、老年代以及永久代内存全部都表示“情绪稳定,压力不大”,但就是照样不停地抛出内存溢出异常,管理员压力很大。最后,在内存溢出后从系统日志中找到异常堆栈,如代码清单5-1所示。
[org.eclipse.jetty.util.log]handle failed java.lang.OutOfMemoryError:null
at 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>
大家知道操作系统对每个进程能管理的内存是有限制的,这台服务器使用的32位Windows平台的限制是2GB,其中划了1.6GB给Java堆,而Direct Memory内存并不算入1.6GB的堆之内,因此它最大也只能在剩余的0.4GB空间中分出一部分。在此应用中导致溢出的关键是:垃圾收集进行时,虚拟机虽然会对Direct Memory进行回收,但是Direct Memory却不能像新生代、老年代那样,发现空间不足了就通知收集器进行垃圾回收,它只能等待老年代满了后Full GC,然后“顺便地”帮它清理掉内存的废弃对象
。否则它只能一直等到抛出内存溢出异常时,先catch掉,再在catch块里面“大喊”一声:“System.gc()!”。要是虚拟机还是不听(譬如打开了-XX:+DisableExplicitGC开关),那就只能眼睁睁地看着堆中还有许多空闲内存,自己却不得不抛出内存溢出异常了。而本案例中使用的CometD 1.1.1框架,正好有大量的NIO操作需要使用到Direct Memory内存。
从实践经验的角度出发,除了Java堆和永久代之外,我们注意到下面这些区域还会占用较多的内存,这里所有的内存总和受到操作系统进程最大内存的限制。
Direct Memory:可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或者OutOfMemoryError:Direct buffer memory。
线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(纵向无法分配,即无法分配新的栈帧)或者OutOfMemoryError:unable to create new native thread(横向无法分配,即无法建立新的线程)。
Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会抛出IOException:Toomany open files异常。
JNI代码:如果代码中使用JNI调用本地库,那本地库使用的内存也不在堆中。
虚拟机和GC:虚拟机、GC的代码执行也要消耗一定的内存。
3.4 外部命令导致系统缓慢
这是一个来自网络的案例:一个数字校园应用系统,运行在一台4个CPU的Solaris 10操作系统上,中间件为GlassFish服务器。系统在做大并发压力测试的时候,发现请求响应时间比较慢,通过操作系统的mpstat工具发现CPU使用率很高,并且系统占用绝大多数的CPU资源的程序并不是应用系统本身。这是个不正常的现象,通常情况下用户应用的CPU占用率应该占主要地位,才能说明系统是正常工作的。
通过Solaris 10的Dtrace脚本可以查看当前情况下哪些系统调用花费了最多的CPU资源,Dtrace运行后发现最消耗CPU资源的竟然是“fork”系统调用。众所周知,“fork”系统调用是Linux用来产生新进程的,在Java虚拟机中,用户编写的Java代码最多只有线程的概念,不应当有进程的产生。
这是个非常异常的现象。通过本系统的开发人员,最终找到了答案:每个用户请求的处理都需要执行一个外部shell脚本来获得系统的一些信息。执行这个shell脚本是通过Java的Runtime.getRuntime().exec()方法来调用的。这种调用方式可以达到目的,但是它在Java虚拟机中是非常消耗资源的操作,即使外部命令本身能很快执行完毕,频繁调用时创建进程的开销也非常可观。Java虚拟机执行这个命令的过程是:首先克隆一个和当前虚拟机拥有一样环境变量的进程,再用这个新的进程去执行外部命令,最后再退出这个进程。如果频繁执行这个操作,系统的消耗会很大,不仅是CPU,内存负担也很重。
用户根据建议去掉这个Shell脚本执行的语句,改为使用Java的API去获取这些信息后,系统很快恢复了正常。
3.5 服务器JVM进程崩溃
例如,一个基于B/S的MIS系统,硬件为两台2个CPU、8GB内存的HP系统,服务器是WebLogic 9.2。正常运行一段时间后,最近发现在运行期间频繁出现集群节点的虚拟机进程自动关闭的现象,留下了一个hs_err_pid###.log文件后,进程就消失了,两台物理机器里的每个节点都出现过进程崩溃的现象。从系统日志中可以看出,每个节点的虚拟机进程在崩溃前不久,都发生过大量相同的异常,见代码清单5-2。
java.net.SocketException:Connection reset
at java.net.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门户做了集成,在MIS系统工作流的待办事项变化时,要通过Web服务通知OA门户系统,把待办事项的变化同步到OA门户之中。通过SoapUI测试了一下同步待办事项的几个Web服务,发现调用后竟然需要长达3分钟才能返回,并且返回结果都是连接中断。
由于MIS系统的用户多,待办事项变化很快,为了不被OA系统速度拖累,使用了异步的方式调用Web服务,但由于两边服务速度的完全不对等,时间越长就累积了越多Web服务没有调用完成,导致在等待的线程和Socket连接越来越多,最终在超过虚拟机的承受能力后使得虚拟机进程崩溃。解决方法:通知OA门户方修复无法使用的集成接口,并将异步调用改为生产者/消费者模式的消息队列实现后,系统恢复正常。
3.6 不恰当数据结构导致内存占用过大
例如,有一个后台RPC服务器,使用64位虚拟机,内存配置为-Xms4g-Xmx8g-Xmn1g,使用ParNew+CMS的收集器组合。平时对外服务的Minor GC时间约在30毫秒以内,完全可以接受。但业务上需要每10分钟加载一个约80MB的数据文件到内存进行数据分析,这些数据会在内存中形成超过100万个HashMap<Long,Long>Entry,在这段时间里面Minor GC就会造成超过500毫秒的停顿,对于这个停顿时间就接受不了了,具体情况如下面GC日志所示。
{Heap before GC invocations=95(full 4):
par new generation total 903168K,used 803142K[0x00002aaaae770000,0x00002aaaebb70000,0x00002aaaebb70000)
eden space 802816K,100%used[0x00002aaaae770000,0x00002aaadf770000,0x00002aaadf770000)
from space 100352K,0%used[0x00002aaae5970000,0x00002aaae59c1910,0x00002aaaebb70000)
to space 100352K,0%used[0x00002aaadf770000,0x00002aaadf770000,0x00002aaae59700000)
concurrent mark-sweep generation total 5845540K,used 3898978K[0x00002aaaebb70000,0x00002aac507f9000,0x00002aacae770000)
concurrent-mark-sweep perm gen total 65536K,used 40333K[0x00002aacae770000,0x00002aacb2770000,0x00002aacb2770000)
2 0 1 1-1 0-2 8 T 1 1:4 0:4 5.1 6 2+0 8 0 0:2 2 6.5 0 4:[G C 2 2 6.5 0 4:[P a r N e w:803142K->100352K(903168K),0.5995670 secs]4702120K->
4056332K(6748708K),0.5997560
secs][Times:user=1.46 sys=0.04,real=0.60 secs]
Heap after GC invocations=96(full 4):
par new generation total 903168K,used 100352K[0x00002aaaae770000,0x00002aaaebb70000,0x00002aaaebb70000)
eden space 802816K,0%used[0x00002aaaae770000,0x00002aaaae770000,0x00002aaadf770000)
from space 100352K,100%used[0x00002aaadf770000,0x00002aaae5970000,
0x00002aaae5970000)
to space 100352K,0x00002aaaebb70000)0%used[0x00002aaae5970000,0x00002aaae5970000,
concurrent mark-sweep generation total 5845540K,used 3955980K[0x00002aaaebb70000,0x00002aac507f9000,0x00002aacae770000)
concurrent-mark-sweep perm gen total 65536K,used 40333K[0x00002aacae770000,0x00002aacb2770000,0x00002aacb2770000)
}
Total time for which application threads were stopped:0.6070570 seconds
观察这个案例,发现平时的Minor GC时间很短,原因是新生代的绝大部分对象都是可清除的,在Minor GC之后Eden和Survivor基本上处于完全空闲的状态。而在分析数据文件期间,800MB的Eden空间很快被填满从而引发GC,但Minor GC之后,新生代中绝大部分对象依然是存活的。我们知道ParNew收集器使用的是复制算法,这个算法的高效是建立在大部分对象都“朝生夕灭”的特性上的,如果存活对象过多,把这些对象复制到Survivor并维持这些对象引用的正确就成为一个沉重的负担,因此导致GC暂停时间明显变长。
如果不修改程序,仅从GC调优的角度去解决这个问题,可以考虑将Survivor空间去掉(加入参数-XX:SurvivorRatio=65536、-XX:MaxTenuringThreshold=0或者-XX:+AlwaysTenure),让新生代中存活的对象在第一次Minor GC后立即进入老年代,等到MajorGC的时候再清理它们。这种措施可以治标,但也有很大副作用,治本的方案需要修改程序,因为这里的问题产生的根本原因是用HashMap<Long,Long>结构来存储数据文件空间效率太低。
下面具体分析一下空间效率。在HashMap<Long,Long>结构中,只有Key和Value所存放的两个长整型数据是有效数据,共16B(2×8B)。这两个长整型数据包装成java.lang.Long对象之后,就分别具有8B的MarkWord、8B的Klass指针,在加8B存储数据的long值。在这两个Long对象组成Map.Entry之后,又多了16B的对象头,然后一个8B的next字段和4B的int型的hash字段,为了对齐,还必须添加4B的空白填充,最后还有HashMap中对这个Entry的8B的引用,这样增加两个长整型数字,实际耗费的内存为
(Long(24B)×2)+Entry(32B)+HashMap Ref(8B)=88B,空间效率为16B/88B=18%,实在太低了。
3.7 服务器升级后效率变低
该问题和3.1章节问题相似,具体问题:有一个50万PV(页面浏览量)的资料类网站(从磁盘提取文档到内存)原服务器32位,1.5G的堆,用户反馈网站比较缓慢,因此公司决定升级,新的服务器为64位,16G的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了?
- 原1.5G为什么慢?
很多用户浏览数据,很多数据load到内存,内存不足,频繁GC
,STW长,响应时间变慢。 - 为什么会更卡顿?
内存变大,FGC时间变长
。 - 怎么解决?
可以将PS换成PN+CMS或者G1(即使用响应时间优先的垃圾回收器)。
四、JVM调优相关问题
- 1、生产环境中,倾向于将最大堆内存和最小堆内存设置为(为什么?)
A: 相同 B:不同
A,好处是:
1) 避免JVM在运行过程中向操作系统申请内存
2)延后启动后首次GC的发生时机
3)减少启动初期的GC次数
2、什么是响应时间优先?
注重的是垃圾回收时STW的时间最短
。3、什么是吞吐量优先?
吞吐量是指应用程序线程用时占程序总用时的比例
,也就是说尽量多让用户程序去执行。4、ParNew和PS的区别是什么?
都是年轻代多线程收集器。
ParNew回收器是通过控制垃圾回收的线程数来进行参数调整,而Parallel Scavenge回收器更关心的是程序运行的吞吐量
。即一段时间内,用户代码运行时间占总运行时间的百分比。5、ParNew和ParallelOld的区别是什么?(年代不同,算法不同)
前者是年轻代收集器,后者是老年代收集器,然后解释两者。6、长时间计算的场景应该选择:吞吐量优先的收集器和策略。
7、大规模电商网站应该选择:停顿时间少(即响应时间快)的收集器和策略。
8、JDK1.7 1.8 1.9的默认垃圾回收器是什么?如何查看?
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代);
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代);
jdk1.9 默认垃圾收集器G1。
java -XX:+PrintCommandLineFlags -version
命令可以查看使用的垃圾回收器。9、所谓调优,到底是在调什么?
是根据业务需要,在吞吐量和响应时间之间做出选择。10、如果采用PS + ParrallelOld组合,怎么做才能让系统基本不产生FGC
应该和下个问题的答案是有相通之处的。11、如果采用ParNew + CMS组合,怎样做才能够让系统基本不产生FGC
1)加大JVM内存
2)加大Young(年轻代)的比例
3)提高Y-O(最大值是15)的年龄
4)提高S(survivor)区比例
5)避免代码内存泄漏
12、如果G1产生FGC,你应该做什么?
1)扩内存
2)提高CPU性能(回收的快,业务逻辑产生对象的速度固定,垃圾回收越快,内存空间越大)
3)降低MixedGC触发的阈值,让MixedGC提早发生(默认是45%)
。具体的参数是:
-XX:InitiatingHeapOccupancyPercent=45
- 13、生产环境中能够随随便便的dump吗?
小堆影响不大,大堆会有服务暂停或卡顿(加live可以缓解),dump前会有FGC。 - 14、CPU资源占用过高问题怎么排查
参考步骤:
- 使用top查看当前CPU情况,找到占用CPU过高的进程,如PID=123。
- top -H -p 123 好到两个CPU占用较高的进程,记录下来PID=2345、3456,转换为十六进制。
- jstack -l 123 > temp.txt,打印出当前进程的线程栈。
- 查找到对应于第二步的两个线程运行栈,分析代码。
- 15、OOM异常问题怎么排查
参考步骤:
- 使用top命令查询服务器系统状态。
- ps -aux |grep java 找到当前Java进程的PID。
- jstat -gcutil pid interval 查看当前GC的状态。
- jmap -histo:live pid 统计存活对象的分布情况,从高到低查看占据内存最多的对象。
- jmap -dump:format=b,file= filename 生成dump文件。
- 使用性能分析工具对dump出来的文件进行分析,工具有MAT等。
JVM(五)JVM调优相关推荐
- JVM原理和调优的理解和学习
JVM原理和调优的理解和学习 一.详解JVM内存模型 二.JVM中一次完整的GC流程是怎样的 三.GC垃圾回收的算法有哪些 四.简单说说你了解的类加载器 五.双亲委派机制是什么,有什么好处,怎么打破 ...
- 2020年薪30W的Java程序员都要求熟悉JVM与性能调优!
前言 作为Java程序员,你有没有被JVM伤害过?面试的时候是否碰到过对JVM的灵魂拷问? 一.JVM 内存区域划分 1.程序计数器(线程私有) 程序计数器(Program Counter Regis ...
- JVM体系结构与调优
JVM GC与调优 前言 一.JVM体系结构概述 二.GC算法 三.HotSpot内存管理 四.HotSpot垃圾回收器 五.调优 六.监控工具 前言 本文章来自一篇参考,学习实践. 一.JVM体系结 ...
- Tomcat性能调优-JVM监控与调优
参数设置 在Java虚拟机的参数中,有3种表示方法用"ps -ef |grep "java"命令,可以得到当前Java进程的所有启动参数和配置参数: 标准参数(-),所有 ...
- Java系列笔记(4) - JVM监控与调优【转】
Java系列笔记(4) - JVM监控与调优[转] 目录 参数设置 收集器搭配 启动内存分配 监控工具和方法 调优方法 调优实例 光说不练假把式,学习Java GC机制的目的是为了实用,也 ...
- Java生产环境下性能监控与调优详解 第7章 JVM层GC调优
第7章 JVM层GC调优 7-1 JVM的内存结构 7-2 常见的垃圾回收算法 7-3 垃圾收集器-1 7-4 垃圾收集器-2 7-5 GC日志格式详解 7-6 可视化工具分析GC日志 7-7 Par ...
- [java] JVM监控与调优
原文出处:http://www.cnblogs.com/zhguang/p/java-jvm-gc.html 光说不练假把式,学习Java GC机制的目的是为了实用,也就是为了在JVM出现问题时分 ...
- JVM原理及调优--网页链接收藏
此篇用于收藏大神们关于JVM原理及调优通俗易懂的文章链接,用于随时查看 JVM调优总结 JVM参数配置大全 JVM调优:选择合适的GC collector 菜菜鸟想了解下大概的JVM内存模型可以看这个 ...
- 【JVM】四、JVM优化-GC调优
传送门 [JVM]一.JVM体系结构 [JVM]二.JVM垃圾收集器 [JVM]三.JVM内存溢出问题分析查看 [JVM]四.JVM优化-GC调优 上一篇:[JVM]三.JVM内存溢出问题分析查看 文 ...
- jvm与Tomcat调优【详解】——有这一篇就够了
jvm与Tomcat调优 一.JVM性能调优 1.1 什么是JVM? 1.2 JVM调优工具 1.3 JVM调优经验 1.4常用JVM参数参考: 1.5 Java文件编译的过程 1.6 为什么说jav ...
最新文章
- c语言比较大小 谁大选谁,C语言比较优先级
- Python3标准库built-in、itertools、functools中的生成器
- CentOS 7 安装 JAVA环境(JDK 1.8)
- 计算机图形学------空间摄像机
- php word 图片无法显示,Word文档不显示图片解决方法
- 第一个ExtJS练习(添加用户面板)
- yii2 跨域请求配置_如何在SpringBoot应用中实现跨域访问资源和消息通信?
- c#进销存(1):需求分析
- unity2019 vuforia 使用小记
- leetcode 171. Excel Sheet Column Number
- 游戏计算机中有缓冲内存吗,内存再大都不行!CPU缓存竟这么重要
- NOIP学习之综合应用:163.出书最多
- iOS 5.1实现旋转屏幕
- c++构造函数的定义
- 谷歌浏览器截图(截取整个网页)
- Java内存溢出排查(必看)
- Progressive Scanning (逐行扫描) vs Interlaced Scanning (隔行扫描)
- 使用ULIB+Altium Designer绘制元件原理图及封装
- C语言简易程序设计————20、寻找完数
- EQ、NE、GT、LT、GE、LE分别代表含义
热门文章
- 自己动手实现远程执行功能
- uva 10859 放置街灯--Placing Lampposts
- CF 285 E Positions in Permutations 题解
- BP算法误差逆传播参数更新公式推导
- java.lang.ClassNotFoundException: org.apache.commons.dbcp.BasicDataSource解决方法
- 学计算机编程我有什么好处,学习计算机编程对我们都有什么好处?
- 中国跨5个时区,东南西北的极点坐标信息
- 创建pdf java 字体_如何使用自定义字体从servlet使用iText XMLWorker创建PDF?
- 邮件多面手!Foxmail 6.5正式版新体验
- 【Java】一个公司职员薪水管理系统(顺序表)