捅破JVM调优这层薄薄的窗户纸

说起JVM调优,大多数人都闻风丧胆,但其实它并没有那么难,你与JVM调优的距离仅差一层窗户纸,今天让我来帮你们将它捅破。

调优是什么

调优是为了追求吞吐量或响应时间而做的一系列硬件、软件的优化活动。简单理解为了提升吞吐量或相应时间,结合具体的业务场景我们应该使用多大内存、什么样的CPU等硬件和给JVM设置什么样的参数使得JVM的垃圾回收时间缩短。

调优是为了什么

在谈到调优时,我们应该明确,调优是为了追求什么?其实很简单,调优有两个指标,第一,吞吐量;第二,响应时间。我们所做的调优工作就是围绕这两个根本性的目的而进行,不同的项目所追求的目标不同,这两个目标不可能同时达到极致。

  • 吞吐量:用户代码执行时间/(用户代码执行时间+垃圾回收时间)
  • 响应时间:垃圾回收时STW时间越短,相应时间越短

调优怎么做

我们可将调优分为3个阶段,分别进行调优

1. 前期规划

结合具体的业务,计算需要的机器内存大小、CPU,选择合适的垃圾回收器(组合)、设置合适的各年龄代内存大小、升级年龄,设置好GC日志参数。
注: 响应时间:CMS 吞吐量:PS

2. 优化运行环境(卡、慢)

结合业务过程和出现的问题,借助一些工具找到原因所在,根据具体的问题再进行优化,可扩大内存、更换垃圾回收器、调节JVM参数。

  • 案例参考: 有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器32位,1.5G 的堆,用户反馈网站比较缓慢,因此公司决定升级,新的服务器为64位,16G 的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了
    为什么原网站慢?
    很多用户浏览数据,很多数据load到内存,内存不足,频繁GC,STW长,响应时间变慢
    为什么会更卡顿?
    内存越大,FGC时间越长
    咋办?
    PS -> PN + CMS 或者 G1

  • 系统CPU经常100%,如何调优?(面试高频) CPU100%那么一定有线程在占用系统资源,
    找出哪个进程cpu高(top)
    该进程中的哪个线程cpu高(top -Hp)
    导出该线程的堆栈 (jstack)
    查找哪个方法(栈帧)消耗时间 (jstack)
    工作线程占比高(是否死锁,死循环) | 垃圾回收线程占比高(为什么频繁GC,哪类对象有问题还是内存不足)

  • 系统内存飙高,如何查找问题?(面试高频)
    导出堆内存 (jmap)
    分析 (jhat jvisualvm mat … ),看哪类对象创建的多,而且不能被回收

  • 如何监控JVM
    jstat jvisualvm jprofiler arthas top…

3. 解决运行中出现的问题

一般是运维团队首先收到报警信息(CPU Memory)

  • 具体排查常用的一些命令:

    1. top命令 可看到具体的进程占用CPU、内存的情况
    2. top -Hp 进程号 观察进程中的线程,哪个线程CPU和内存占比高
    3. jstack 线程号 定位线程的具体情况
    4. 也可用jsp命令找出所有的java进程,然后再用jstack定位具体的线程
    5. jstat -gc 动态观察gc情况
    6. jstat -gc 进程号 500 : 每隔500个毫秒打印GC的情况
    7. jmap - histo 进程号 查看对象创建信息,有多少对象创建
    8. jmap -dump:format=b,file=xxx 进程号 将对象信息转储,转储时耗费资源,会对系统造成影响
    9. 使用MAT / jhat /jvisualvm 进行dump文件分析 jhat -J-mx512M xxx.dump jhat使用
    10. 通过排查最终定位代码

    注: 线上系统,内存特别大,jmap执行期间会对进程产生很大影响,甚至卡顿(电商不适合)可设定了参数HeapDump,OOM的时候会自动产生堆转储文件 -XX:+HeapDumpOnOutOfMemoryError

  • 一个JDK自带的图形化工具jvisualvm

  • arthas在线排查工具

  1. 在生产上我们经常会碰到一些不好排查的问题,例如线程安全问题,用最heapdump不好查到问题原因。为了排查这些问题,有时我们会临时加一些日志,比如在一些关键的函数里打印出入参,然后重新打包发布,如果打了日志还是没找到问题,继续加日志,重新打包发布。对于上线流程复杂而且审核比较严的公司,从改代码到上线需要层层的流转,会大大影响问题排查的进度。
  2. 步骤:1)启动arthas java -jar arthas-boot.jar 2 )绑定java进程 3)ashboard命令观察系统整体情况 4)help 查看帮助 5 )help xx 查看具体命令帮助
  3. jvm 观察jvm信息
  4. thread 定位线程问题
  5. dashboard 观察系统情况
  6. heapdump + jhat分析
  7. jad反编译 动态代理生成类的问题定位 第三方的类(观察代码) 版本问题(确定自己最新提交的版本是不是被使用)
  8. redefine 热替换 目前有些限制条件:只能改方法实现(方法已经运行完成),不能改方法名, 不能改属性
  9. 没有包含的功能:jmap
  10. arthas使用帮助

JVM常用的命令行参数

JVM的命令行参数参考:

HotSpot参数分类

标准: - 开头,所有的HotSpot都支持
非标准:-X 开头,特定版本HotSpot支持特定命令
不稳定:-XX 开头,下个版本可能取消

GC常用参数
-Xmn -Xms -Xmx -Xss 年轻代 最小堆 最大堆 栈空间
-XX:+UseTLAB 使用TLAB,默认打开
-XX:+PrintTLAB 打印TLAB的使用情况
-XX:TLABSize 设置TLAB大小
-XX:+DisableExplictGC System.gc()不管用 ,FGC
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintHeapAtGC
-XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationConcurrentTime  打印应用程序时间
-XX:+PrintGCApplicationStoppedTime) 打印暂停时长
-XX:+PrintReferenceGC  记录回收了多少种不同引用类型的引用
-verbose:class 类加载详细过程
-XX:+PrintVMOptions
-XX:+PrintFlagsFinal -XX:+PrintFlagsInitial 必须会用
-Xloggc:opt/log/gc.log
-XX:MaxTenuringThreshold 升代年龄,最大值15

Parallel常用参数
-XX:SurvivorRatio
-XX:PreTenureSizeThreshold 大对象到底多大
-XX:MaxTenuringThreshold
-XX:+ParallelGCThreads 并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同
-XX:+UseAdaptiveSizePolicy 自动选择各区大小比例

CMS常用参数
-XX:+UseConcMarkSweepGC
-XX:ParallelCMSThreads CMS线程数量
-XX:CMSInitiatingOccupancyFraction 使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生			SerialOld卡顿,应该调小,(频繁CMS回收)
-XX:+UseCMSCompactAtFullCollection 在FGC时进行压缩
-XX:CMSFullGCsBeforeCompaction 多少次FGC之后进行压缩
-XX:+CMSClassUnloadingEnabled
-XX:CMSInitiatingPermOccupancyFraction 达到什么比例时进行Perm回收
GCTimeRatio 设置GC时间占用程序运行时间的百分比
-XX:MaxGCPauseMillis 停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代

G1常用参数
-XX:+UseG1GC
-XX:MaxGCPauseMillis 建议值,G1会尝试调整Young区的块数来达到这个值
-XX:GCPauseIntervalMillis ?GC的间隔时间
-XX:+G1HeapRegionSize 分区大小,建议逐渐增大该值,1 2 4 8 16 32。 随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长 ZGC做了改进(动态区块大小)
G1NewSizePercent 新生代最小比例,默认为5%
G1MaxNewSizePercent 新生代最大比例,默认为60%
GCTimeRatio GC时间建议比例,G1会根据这个值调整堆空间
ConcGCThreads 线程数量
InitiatingHeapOccupancyPercent 启动G1的堆空间占用比例

常用的垃圾回收器组合参数

-XX:+UseSerialGC = Serial New (DefNew) + Serial Old
小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器

-XX:+UseParNewGC = ParNew + SerialOld
这个组合已经很少用(在某些版本中已经废弃)

-XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old
-XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】
-XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
-XX:+UseG1GC = G1

GC日志详解

  1. PS\PO日志格式:
    在这里插入图片描述
  2. heap dump信息
    在这里插入图片描述
    eden space 5632K, 94% used [0x00000000ff980000,0x00000000ffeb3e28,0x00000000fff00000)
    后面的内存地址指的是,起始地址,使用空间结束地址,整体空间结束地址
  3. CMS日志
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8511K(13696K)] 9866K(19840K), 0.0040321 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
   //8511 (13696) : 老年代使用(最大)
   //9866 (19840) : 整个堆使用(最大)
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.018/0.018 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
   //这里的时间意义不大,因为是并发执行
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
   //标记Card为Dirty,也称为Card Marking
[GC (CMS Final Remark) [YG occupancy: 1597 K (6144 K)][Rescan (parallel) , 0.0008396 secs][weak refs processing, 0.0000138 secs][class unloading, 0.0005404 secs][scrub symbol table, 0.0006169 secs][scrub string table, 0.0004903 secs][1 CMS-remark: 8511K(13696K)] 10108K(19840K), 0.0039567 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
   //STW阶段,YG occupancy:年轻代占用及容量
   //[Rescan (parallel):STW下的存活对象标记
   //weak refs processing: 弱引用处理
   //class unloading: 卸载用不到的class
   //scrub symbol(string) table: 
   	//cleaning up symbol and string tables which hold class-level metadata and 
   	//internalized string respectively
   //CMS-remark: 8511K(13696K): 阶段过后的老年代占用及容量
   //10108K(19840K): 阶段过后的堆占用及容量

[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.005/0.005 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
   //标记已经完成,进行并发清理
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
   //重置内部结构,为下次GC做准备
  1. G1日志
[GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0015790 secs]
//young -> 年轻代 Evacuation-> 复制存活对象 
//initial-mark 混合回收的阶段,这里是YGC混合老年代回收
  [Parallel Time: 1.5 ms, GC Workers: 1] //一个GC线程
     [GC Worker Start (ms):  92635.7]
     [Ext Root Scanning (ms):  1.1]
     [Update RS (ms):  0.0]
        [Processed Buffers:  1]
     [Scan RS (ms):  0.0]
     [Code Root Scanning (ms):  0.0]
     [Object Copy (ms):  0.1]
     [Termination (ms):  0.0]
        [Termination Attempts:  1]
     [GC Worker Other (ms):  0.0]
     [GC Worker Total (ms):  1.2]
     [GC Worker End (ms):  92636.9]
  [Code Root Fixup: 0.0 ms]
  [Code Root Purge: 0.0 ms]
  [Clear CT: 0.0 ms]
  [Other: 0.1 ms]
     [Choose CSet: 0.0 ms]
     [Ref Proc: 0.0 ms]
     [Ref Enq: 0.0 ms]
     [Redirty Cards: 0.0 ms]
     [Humongous Register: 0.0 ms]
     [Humongous Reclaim: 0.0 ms]
     [Free CSet: 0.0 ms]
  [Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs] 
//以下是混合回收其他阶段
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0000078 secs]
[GC concurrent-mark-start]
//无法evacuation,进行FGC
[Full GC (Allocation Failure)  18M->18M(20M), 0.0719656 secs]
  [Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)], [Metaspace: 38
76K->3876K(1056768K)] [Times: user=0.07 sys=0.00, real=0.07 secs]

调优的一些案例

OOM产生的原因多种多样,有些程序未必产生OOM,不断FGC(CPU飙高,但内存回收特别少)

  1. 硬件升级系统反而卡顿的问题
    当内存变大时,垃圾回收器没有改变,可能会使得STW时间边长

  2. 线程池不当运用产生OOM问题

  3. 系统老是卡顿,但不知问题所在,需要不间断重启才能使用
    采用 加内存 + 更换垃圾回收器 G1后解决问题 真正问题在哪儿 ?不知道

  4. tomcat中 http-header-size设置过大,造成OOM,http-header-size设置过大,会使得Http11OutputBuffer这个对象过大,造成OOM 参考

  5. lambda表达式导致方法区溢出问题
    当方法区设置过小,而不断的有lambda表达式产生时,会造成方法区OOM; 在使用lambda表达式时会有内部类产生。

  6. 栈溢出问题 -Xss设定太小

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章