经验整理-1-JVM调优举例-100-@

7 篇文章 0 订阅

-------------------JVM调优实践-------------

?jconsole连接不上失败?

添加:vm optiong=-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false

导致GC出现问题的场景原因及具体现象

巧记:堆、方法区

导致GC出现问题的场景有: 
(堆)创建可达有用的大对象:这个是存储在堆中的,需要控制好对象的数量和大小,尤其是大的对象很容易进入老年代
(堆)创建可达无用的大对象(内存溢出):这个是存储在堆中的,需要控制好对象的数量和大小,尤其是大的对象很容易进入老年代
(堆)全局集合(很容易造成内存溢出):全局集合通常是生命周期比较长的,因此需要特别注意全局集合的使用
(堆)缓存:缓存选用的数据结构不同,会很大程序影响内存的大小和gc
(栈)栈空间不足,创建线程数比较多超过剩余内存(还有一种是单个线程内递归调用报StackOverflowError大家都把它算进OOM范畴了)----1M对应线程数3000~5000
(方法区)ClassLoader:主要是ClassLoader动态加载类新增类信息,容易造成永久代内存不足
(方法区)常量池过大
(本地内存)多线程:线程分配会占用本地内存,过多的线程也会造成内存不足
 
以上使用不当很容易造成具体现象是: 
一、频繁GC -> STW--Stop the world,使你的应用响应变慢,停顿卡顿严重,CPU使用率过高 (可达有用的大对象和内存溢出)
二 、OOM,直接造成内存溢出错误使得程序退出。OOM又可以分为以下几种:
Heap space:堆内存不足
PermGen space:永久代内存不足(
ClassLoader新增类信息/常量池过大)
Native thread:本地线程没有足够内存可分配

JVM调优的选择:

JVM调优主要涉及:
一、堆大小的设置
二、GC收集器的选择:

1)(GC每次吞的多执行的多)吞吐量大大应用,一般采用并行收集,开启多个线程,加快gc的回收。---并行收集器(UseParallelGC)
2)(用户体验)-响应速度高的应用,一般采用并发收集,比如应用服务器。--新生代(ParNew)老年代(UseConcMarkSweepGC)
 CMS是标记清理算法,因此会有碎片,建议设置以下参数---性能调优配参必备:

三、代码BUG

性能调优配参必备:

-XX:+UseConcMarkSweepGC   #并发收集年老代,配置这个以后,-XX:NewRatio=4的配置失效,所以,此时年轻代大小最好用-Xmn设置。
-XX:+CMSScavengeBeforeRemark  //在执行CMS remark之前进行一次youngGC,这样能有效降低remark的时间
-XX:CMSInitiatingOccupancyFraction=80 # 表示年老代空间到80%时就开始执行CMS
-XX:+UseCMSCompactAtFullCollection # 打开对年老代的压缩。可能会影响性能,但是可以消除内存碎片同,--要和下面这个组合用,控制它为多少次CMS才执行一次整理,可以不至于太影响性能
-XX:CMSFullGCsBeforeCompaction=10 # 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此参数设置运行10次FullGC以后对内存空间进行压缩、整理。 

 

调优调哪些东西

(和上面有点重复)

一、堆栈内存的分配的调优;
二、
GC回收器的选择调优;
三、JVM本身调优没有明显变化,考虑代码本身的调优,是否有BUG,效率很低的写法,
代码BUG
,比如,
1)大查询SQL();循环里频繁创建对象();只在一个方法内用到的对象,指给了成员变量对象引用
-----堆相关问题------
代码问题:
1)频繁创建大量存活对象,很快弄满eden,会频繁MinorGC,这种对象快速进入survivor,s0,s1也会频繁的相互复制,很快survivor满了或对象年龄,比较快的进入老年代,等老年代到达阀值,引起fullGC(卡顿);;;;-----
2)频繁创建超大存活对象,比如长数组,长字符串,达到阀值或MinorGC时survivor已装不下满了,直接进入老年代,等老年代到达阀值,引起fullGC(卡顿);
3)以上两种情况,频繁创建超大存活对象或大量存活对象,如果长时间存活没有释放,就会内存不足分配新空间,进入fullGC后,引起OOM(OOM内存溢出);---1、大查询SQL---解决:分批查询且解决循环查询BUG
内存分配问题:
1)xmn和Xms,最大堆大小和默认堆大小设置的太小了,导致fullGC后依然没有空间(OutOfMemoryError堆栈溢出)
1.1)xmn,分配的新生代内存太小了,新创建的对象还存活,导致fullGC(OutOfMemoryError堆栈溢出)
1.2)xmn和Xms,最大堆大小和默认堆大小设置的太小了,导致fullGC(OutOfMemoryError堆栈溢出)
2)在 CMS 启动过程中,老年代碎片化严重,无法容纳新生代提升上来的大对象(OutOfMemoryError堆栈溢出)----
-----栈相关问题------
代码问题:
1)方法深度递归(StackOverflowError栈的最大深度1000-2000);;;;-----
内存分配问题:
1)Xss太小了(OutOfMemoryError栈溢出)


--------------------------------我们怎么操作调优---------------------------

-------------工具选择----------------------------------------------------------
jvisualvm分析程序死锁,用它的VisualGc插件来实时监控生产GC内存变化(看GC日志只是还原这个GC回收过程的内存变化,GC次数、GC时长需要计算一下)、监视标签里还能导出现场堆dump文件(dump文件里能看到类实例对象的大小占比找出大对象--如JVM内存溢出、死锁能直接分析出),另外也能把生产的dump文件导入,找出占比高的大对象
gceasy:用来分析生产GC日志,查内存大小分配比例,查GC回收过程的的内存变化、吞吐量、最大最小停顿时间、普通GC和fullGC的GC耗时时长、GC次数、查找内存泄漏
 

MAT(Eclipse也有插件):直观给我们展示内存信息,能提供一些重要的信息:

  • 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
  • 所有的类信息,包括classloader、类名称、父类、静态变量等
  • GCRoot到所有的这些对象的引用路径
  • 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)
  • 内存泄露的定位与分析:问题重现后才可以进行分析
     

-------------怎么判断GC存在问题----判断标准------------------------------------
耗时时长:长于50毫秒与1000毫秒
周期频率:小于10秒一次和10分钟一次

-------------服务启动配置JVM参数常识------------------------------------
提前把该加的JVM参数配置加上,比如GC日志打印、堆dump文件等,方便异常出现能收集到数据,(catalina.sh文件的启动参数export JAVA_OPTS=)
 GC日志打印:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/data/gc.log)。
 堆dump文件:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\dump\   
(堆dump太大了,用命令式不知道有没有风险,如果能即使处理不会丢失,还是建议用命令输出堆dump文件jmap -dump:format=b,file=文件名 [pid]

案例1-fullgc排查---fullgc次数达到运维报警---停顿卡顿严重,系统越来越慢,CPU使用率过高

---------------频繁fullgc,造成系统卡顿严重,通过GC日志、堆dump文件来帮助我们定位,查找不容易发现的代码本身的问题---
一、 现象:-zabbix监控报警,CPU资源占用过高,最近并没有特别开发任务上线,但是最近有发布过一个新的功能。
二、分析问题,可能原因----巧记:老新永久系统,fanalize执行垃圾碎片,还得看代码
1、老年代不足 (分几种:一、代码问题,频繁创建了大对象或者部分内存泄漏;二、比如新生代太小装不下,普通GC频繁,导致S空间不足及年龄增长加快,进入老年代的速度也加快,容易引起fullGC;三、再比如老年代太小,创建了一些大对象直接引起老年代fullGC)
2、 System.gc()方法的调用
3、 永久代不足
4、执行CMS GC的过程中同时有对象要放入老年代,因老年代浮动垃圾碎片空间临时不足引起Full GC(concurrent mode failure)
5、minor gc时年轻代的存活区空间不足而晋升老年代,老年代又空间不足而触发full gc
6、应用程序过度使用 finalize()。
三 、去证实问题,拿GC日志,生成堆栈dump文件-----把gc日志下载下来,对jvm进行jmap dump命令生成dump文件,通过工具分析。
具体如下:

---测试环境版---
第一步:用top命令找出占用CPU较高的java应用程序进程pid  
第二步:使用top -H -p pid,查看该进程里占用CPU较高的线程ID 
第三步:把得到的线程ID转成16进制(echo 'obase=16;thread_id'|bc)
第四步:打印出jvm实例的线程堆栈(jstack pid),在线程堆栈里找出线程ID对应的代码块,开始优化吧! 

太难搞命令了,测试环境用工具实时监控吧:jvisualvm分析程序死锁,用它的VisualGc插件来实时监控生产GC内存变化(看GC日志只是还原这个GC回收过程的内存变化,GC次数、GC时长需要计算一下)、监视标签里还能导出现场堆dump文件(dump文件里能看到类实例对象的大小占比找出大对象--如JVM内存溢出、死锁能直接分析出),另外也能把生产的dump文件导入,找出占比高的大对象


---生产环境版---
让运维帮忙把gc日志下载下来,对jvm进行jmap的dump命令生成堆dump文件(jmap -dump:format=b,file=文件名 [pid]),jstack的dump命令生成线程dump文件发给我(jstack 1316 |grep -i http-8001 -A 10  >/home/jstack.txt)。(gzip命令压缩很强大)
通过工具分析:
1)GC日志---gceasy:用来分析生产GC日志,查内存大小分配比例,查GC回收过程的的内存变化、吞吐量、最大最小停顿时间、普通GC和fullGC的GC耗时时长、GC次数、查找内存泄漏
2)堆dump快照文件---jvisualvm:把生产的dump文件导入,推dump文件在类标签那里,能找出占比高的大对象

结论-----------------最后会发现几种可能: 1、代码频繁创建了大对象或者部分内存泄漏(如果全内存泄漏就会GC回收不了,OOM且一直卡住),直接或15岁进入了老年代,引起的FULLGC,比如查询大SQL。2、新生代内存太小,频繁GC,加速进入老年代,引起的FULL gc. 3.老年代本来就小


案例1-举例:系统并发执行对账fullGC频繁,优化后恢复正常。
导致原因: 
系统中有一个查询50000笔交易的SQL,SQL条件漏传;被循环执行了10000次。上线的代码有点多,查代码没查出来哪问题。
解决办法: 
先分析gc,最后---SQL条件加上,就没有50000笔了。

案例2-内存泄露排查---易引起的fullgc或严重点造成内存溢出oom--

内存泄露:对象无用了,但仍然可达(未释放),垃圾回收器无法回收---如果有人问oom也可以马这个加强一点当成答案,刚好溢出就是OOM
举例
很多情况:1、全局的容器类(如公用的集合HashMap放元素后业务异常了,忘记删元素)2、像Runnable对象没释放 3、原子数据类

---------------频繁fullgc,造成系统卡顿严重,通过GC日志、堆dump文件来帮助我们定位,查找不容易发现的代码本身的问题---
一、 现象:-zabbix监控报警,CPU资源占用过高,最近并没有特别开发任务上线,但是最近有发布过一个新的功能。
二、分析问题,可能原因----集合HashMap放元素后业务异常了,忘记删元素

---生产环境版------使用jVisualvm分析内存泄漏,gceasy作辅助
1)jmap命令拿到对应的堆dump文件(按照程序输出分开进行堆dump多次,好比较),同时下载GC日志。(如果是本地或远端测试环境可自已监控标签生成dump文件)
2)先把GC日志导入gceasy分析一下,发现老年代回收频繁,但是内存并没有明显减少,说明存在无法被回收的可达对象,可能是内存泄漏了。
3)把dump文件导入jVisualvm分析一下,进入“类”标签,点击“与另一个堆dump文件比较”按扭,把两个堆dump进行比较
比较结果如下:
可以看出在两次间隔时间内堆dump显示的某个目标对象A的实例一直在增加并且多了,一直没回收,说明该对象引用的方法可能存在内存泄漏。
4)选中类目标对象A,右键选择“在实例视图中显示”,如下所示:
左侧是创建的实例总数,右侧上部为该实例的结构,下面为引用说明,从图中可以看出类A被谁引用了。
如此可以确定泄漏的位置,进而根据实际情况进行分析解决。

附:判断对象是否被引用的工具或者方法?
----在我们内存泄漏问题排查的过程中就能看到对象是否被引用。通过jvisualvm导入堆dump文件,分析类标签,下面能按类名过虑收索,收出来这个类之后,会看到后面有一个实例数=0.就是没有被引用或不可达

案例3-线程死锁、死循、请求外部资源导致的长时间wait---易引起:响应慢或无影响、长期会oom

一、 现象:-zabbix监控报警,接口一直超时,但服务端能收到,只是没响应(里面可能死锁了),最近有发布过一个新的功能。
二、分析问题,可能原因----集合HashMap放元素后业务异常了,死锁了---生产环境版------使用jVisualvm分析内存泄漏,gceasy作辅助
1)jstack命令拿到对应的线程dump文件(按照程序输出分开进行堆dump多次,好比较),同时下载GC日志。(如果是本地或远端测试环境可自已线程标签生成dump文件)
2)先把GC日志导入gceasy分析一下,发现老年代回收频繁,但是内存并没有明显减少,说明存在无法被回收的可达对象,可能是内存泄漏了。(频繁调用死锁方法也可以产生大量存活对象)
3)把线程dump文件导入jVisualvm分析一下,导进来就看到各个线程的执行信息,分别比较一下两个dump是否都有同样的同题,比如:

非死锁,但强等待的情况-------线程状态BLOCKED,线程动作wait on monitor entry,调用修饰waiting to lock总是一起出现。表示在代码级别已经存在冲突的调用。必然有问题的代码,需要尽可能减少其发生。
死锁-------dump写的很明显,直接告诉我们 Found one Java-level deadlock

 

-------------秒杀-性能测试-调优-------

再次就是看那些进程和和线程有问题,通过linux命令定位,然后jstack导出内存快照通过线程号找数据

对于大并发量的系统,有几个可能需要优化的点,下面我们要一步步测试来优化这个系统。

测试目标

对于一个系统,几个常用的评价指标是:平均响应时间、吞吐率、qps等。我的测试主要测试3个接口

主页(访问根路径,没有数据库交互)
秒杀接口暴露(暴露秒杀接口,有后台数据交互)
执行秒杀操作(插入秒杀成功记录和减库存一个完整的事务操作)
对于这三个接口,我们主要的测试目标和优化目标是平均响应时间,当然这是建立在数据正确返回的基础上的,失败率太高那这个平均响应时间是没有意义的。
这里的优化侧重于后端数据库和内存方面的优化。

测试环境

我是在Windows10下用jmeter来进行负载测试和压力测试,其他环境如下,涉及具体配置再提。

Tomcat8.0.38
Jdk1.8 hotspot vm
Mysql 5.7
Redis 2.7.3

测试过程

首先进行主页测试,我们访问tomcat的主页,使用jmeter的线程组中的线程数模拟用户数,不断增加线程数对主页进行性能测试。
我们将结果数据写到一个xml文件中。首先我们模拟5000个用户同时请求主页。

5000个用户同时请求主页

设置循环次数为2,即一共有10000个请求将被发送。

从响应的结果可以看到,没有错误数,这10000个请求全部返回成功了,只是有的请求慢有的请求快。平均的响应时间在300ms, 50%的请求的响应时间平均为87ms。到后面越来越多的请求开始等待,这里可以想到的优化的点在于tomcat的线程池中线程的数量,越来越多的请求在等待队列中。查看tomcat的配置后发现最大线程数为maxThreads=”150”,好那我们用150个线程,循环10次,也就是一共1500个请求,那结果会是什么样呢?

平均相应时间为5ms,前50%的请求的平均响应时间为1ms。
但是这里并不能直接修改tomcat的最大线程数来优化。复杂点说就是这是一个复杂的东西,线程数越大,你也要有相应的cpu来执行啊。直接点说就是,我不懂。。。
我把tomcat的线程数设置为500,然后起5000个线程发送10000个请求,然后得到了:

 比之前的更差了。无论是平均相应时间还是错误率。简单粗暴的去改线程数是不可行的。这里我们不去管tomcat的线程数或者是其他层面的优化,我们只专注于后端数据库层面的优化。

500个用户同时请求暴露秒杀接口

为什么用500个,是为了减少因为tomcat请求等待带来的数据误差。 
直接向MySQL请求数据 
先模拟500个用户,每个用户发送10次请求。该请求相应的操作为根据id向数据库查询一条记录。得到了这样的数据。

 期间打开windows的性能监控器,发现磁盘IO有变化,IO百分比最高的时候也不超过15%。 
这样的操作,错误率为0,相当稳定,平均响应时间为1406ms。 

模拟5000个用户,每个用户发送一次请求

 磁盘的IO百分比一度达到了100%。从数据的绝对值来看,这样的测试没有意义了,因为瓶颈不在MySQL,瓶颈是tomcat的连接池最大线程数为maxThreads=”150” 越来越多的请求在等待队列中,因为我们前面分析过的tomcat。但是数据的相对值是有意义的。

使用redis缓存数据 

还是模拟500个用户,每个用户发送10次请求。

 响应速度显著提高,注意一个值,Min=1,有些请求几乎不足1ms,因为redis直接从内存读取数值,非常快。如果不是tomcat的请求在排队,我想平均响应时间是个位数。 

Redis下模拟5000个用户,每个用户发送一次请求。会是什么结果呢?

 可以看到:模拟5000个用户比模拟500个用户的响应时间要慢很多,平均响应时间大概是8倍

使用150个线程,循环100次,即发送15000次请求,得到:

 可以看到,150个用户的话这种响应速度是比较快的,因此可以初步断定:响应的瓶颈在于tomcat的请求排队等待。

这个优化的过程我想到了很多东西,感觉就是,优化是无止尽的。
比如,我想到了内存回收那一块。选用合适的垃圾收集器,尽可能地减少GC时stop the world的时间和次数显然对于一个秒杀系统来说是非常对的优化方向。这里我尝试用过几款垃圾收集器比如parNew,G1来对比他们的平均响应时间,但是多次测试后没有明显的差距。有两个原因,一是这个接口没有产生太多的大对象,二是这个优化并不太明显。后面有机会的话还是希望继续在内存方面进行优化,感觉内存回收方面有点神秘,很想试一试。

可以看到redis的使用很大程度上提高了响应的时间。上面那个接口只是暴露一个地址,这些地址每个产品都只有一个,那这样的场景是可以用redis的。但是有些操作并没有办法使用缓存。比如执行秒杀这个操作。
这个操作是个事务型操作。如果其中一个操作失败了,我就让他rollback,这样的话,应该会有更多的并发问题。
见下篇。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java_爱吃肉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值