JVM调优

380 阅读11分钟

1. 内存调优

离线分析

Mysql大量查询结果导致内存OOM

问题代码:

image.png

出现问题是因为没有限制分页,导致一次从数据库里面查询了大量的数据,撑爆了内存。

解决思路
  1. 服务出现 OOM 内存溢出的时候,生成内存快照
  2. 使用 MAT 分析内存快照,找到内存溢出的对象
按照深堆倒序排序

image.png

我们按照深堆倒序排列,可以看到 ResultSet 占用了大量的内存。(这是JDBC的返回结果)

可以点开 ResultSetImpl查看具体的返回结果

image.png

其实一般可以通过 ResultSetImpl的具体返回结果定位到是哪里查询出现了问题。

从tomcat线程池中的线程入手
展开tomcat线程池中的线程

image.png

通过SpringMVC处理器定位是哪个接口(HandlerMethod)

image.png

list objects -> with outgoing references

展开 HandlerMethod 所关联的对象 image.png

通过HandlerMethod的 description即可定位到是哪个接口。

image.png

Mybatis导致的内存溢出

问题代码:

image.png

前面的过程略过,这里直接展开深堆占用内存最大的线程入手。

展开深堆占用内存最大的线程

image.png

发现一个深堆占用内存最大的字符串

image.png

发现一个深堆占用内存最大的HashMap

image.png

综上所述,我们发现的原因就是 动态sql进行了大量的 forEach动态标签解析,生成了两个大对象

  1. 最后生成的SQL
  2. 存储SQL里面的变量 和 变量值之间的映射

具体的原理你可以参考 : MyBatis源码-SqlNode语法树 - 掘金 (juejin.cn)

在线分析

jmap + stack

  1. 使用jmap -histo:live 进程ID 〉文件名 命令将内存中存活对象以直方图的形式保存到文件中,这个过程会影响用户的时间,但是时间比较短暂
  2. 分析内存占用最多的对象,一般这些对象就是造成内存泄漏的原因
  3. 使用arthas的stack命令,追踪对象创建的方法被调用的调用路径,找到对象创建的根源。也可以使用btrace工具编写脚本追踪方法执行的过程。
存活对象直方图

image.png

使用 stack 命令
stack com.itheima.jvmoptimize.entity.UserEntity -n 1

image.png

这样就可以分析出占用内存较高的对象创建的调用栈。

btrace

  1. BTrace 是一个在Java 平台上执行的追踪工具,可以有效地用于线上运行系统的方法追踪,具有侵入性小、对性能的影响微乎其微等特点。
  2. 项目中可以使用btrace工具,打印出方法被调用的信息。

使用方法:

  1. 下载btrace工具,官方地址:github.com/btraceio/bt…
  2. 编写btrace脚本,通常是一个java文件。
  3. 将btrace工具和脚本上传到服务器,在服务器上运行 btrace 进程ID 脚本文件名
  4. 观察执行结果。

编写 btrace脚本

image.png

执行命令

btrace 3386841 TracingUserEntity.java

总结对比

image.png

2. GC调优

概念

GC调优指的是对垃圾回收(Garbage collection)进行调优。GC调优的主要目标是避免由垃圾回收引起程 序性能下降。

GC调优的核心指标

吞吐量

垃圾回收吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量=执行用户代码时间/(执行用户代码时间 +GC时间)。吞吐量数值越高,垃圾回收的效率就越高,允许更多的CPU时间去处理用户的业务,相应的业务吞吐量也就越高。

image.png

延迟

延迟指的是从用户发起一个请求到收到响应这其中经历的时间。

image.png

内存使用量

内存使用量指的是Java应用占用系统内存的最大值,一般通过Jvm参数调整,在满足上述两个指标的前提下 这个值越小越好。

GC日志

通过GC日志,可以更好的看到垃圾回收细节上的数据,同时也可以根据每款垃圾回收器的不同特点更好地发现存在的问题。

使用方法(JDK 8及以下): -XX:+PrintGcDetails -Xloggc:文件名

使用方法(JDK 9+):-Xlog:gc*:file=文件名

将GC日志转化为可视化图表

GC Viewer

GCViewer是一个将GC日志转换成可视化图表的小工具,github地址: github.com/chewiebug/G…

使用方法:java -jar gcviewer_1.3.4.jar 日志文件.log

image.png

  1. Accumulated pauses : 累计停顿时间
  2. Number of full gc pauses : 整个full gc 产生的次数
  3. Throughput : GC关注指标-吞吐量

垃圾回收带来的一些停顿时间

image.png

GCeasy
  1. Gceasy是业界首款使用AI机器学习技术在线进行GC分析和诊断的工具。定位内存泄漏、GC延迟高的问题,提供JVM 参数优化建议,支持在线的可视化工具图表展示。
  2. 官方网站 : gceasy.io/

常见内存趋势图

正常情况

特点:呈现锯齿状,对象创建之后内存上升,一旦发生垃圾回收之后下降到底部,并且每次下降之后的内存大小 接近,存留的对象较少。

image.png

缓存对象过多

特点:呈现锯齿状,对象创建之后内存上升,接近,处于比较高的位置。一旦发生垃圾回收之后下降到底部,并且每次下降之后的内存大小接近,处于比较高的位置。

问题产生原因: 程序中保存了大量的缓存对象,导致GC之后无法释放,可以使用MAT或者HeapHero等工具进行分析内存占用的原因。

image.png

内存泄漏

特点:呈现锯齿状,每次垃圾回收之后下降到的内存位置越来越高,最后由于垃圾回收无法释放空间导致对象无法分配产生0utofMemory的错误。

问题产生原因: 程序中保存了大量的内存泄漏对象,导致GC之后无法释放,可以使用MAT或者HeapHero等工具进行分析是哪些对象产生了内存泄漏。

image.png

持续的full gc

特点:在某个时间点产生多次Full GC,CPU使用率同时飙高,用户请求基本无法处理。一段时间之后恢复正常。问题产生原因: 在该时间范围请求量激增,程序开始生成更多对象,同时垃圾收集无法跟上对象创建速率,导致持续地在进行FULL GC。

image.png

元空间不足导致的 full gc

特点:堆内存的大小并不是特别大,但是持续发生FULLGC。

问题产生原因: 元空间大小不足,导致持续FULLGC回收元空间的数据。

image.png

JVM参数设置

-Xmx 和 -Xms

-Xmx参数设置的是最大堆内存,但是由于程序是运行在服务器或者容器上,计算可用内存时,要将元空间、操作系统. 其它软件占用的内存排除掉。

image.png

-Xms用来设置初始堆大小,建议将-Xms设置的和-Xmx一样大,有以下几点好处:

  1. 运行时性能更好,堆的扩容是需要向操作系统申请内存的,这样会导致程序性能短期下降。
  2. 可用性问题,如果在扩容时其他程序正在使用大量内存,很容易因为操作系统内存不足分配失败。
  3. 启动速度更快,Oracle官方文档的原话:如果初始堆太小,Java 应用程序启动会变得很慢,因为 JVM 被迫频繁执行垃圾收集,直到堆增长到更合理的大小。为了获得最佳启动性能,请将初始堆大小设置为与最大堆大小相同
-XX:MaxMetaspaceSize和XX:MetaspaceSize

-XX:MaxMetaspacesize=值 参数指的是最大元空间大小,默认值比较大,如果出现元空间内存泄漏会让操作系统可用内存不可控,建议根据测试情况设置最大值,一般设置为256m。

-Xx:MetaspaceSize=值 参数指的是到达这个值之后会触发FULLGC (网上很多文章的初始元空间大小是错误的) 后续什么时候再触发JVM会自行计算。如果设置为和MaxMetaspaceSize一样大,就不会FULLGC,但是对象也无法回收。

image.png

-Xss

如果我们不指定栈的大小,JVM 将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构。比如Linux86 64位:1MB,如果不需要用到这么大的栈内存,完全可以将此值调小节省内存空间,合理值为256k -1m之间

年轻代相关
-Xmn

-Xmn 年轻代的大小,默认值为整个堆的1/3,可以根据峰值流量计算最大的年轻代大小,尽量让对象只存放在年轻代,不进入老年代。但是实际的场景中,接口的响应时间、创建对象的大小、程序内部还会有一些定时任务等不确定因素都会导致这个值的大小并不能仅凭计算得出,如果设置该值要进行大量的测试。G1垃圾回收器尽量不要设置该值,G1会动态调整年轻代的大小。

image.png

-XX:SurvivorRatio && MaxTenuringThreshold

image.png

其他参数
-XX:+DisableExplicitGC

禁止在代码中使用System.c(),system.gc()可能会引起FULLGC,在代码中尽量不要使用。使用 DisableExplicitGc参数可以禁止使用System.gc()方法调用。

-XX:+HeapDumpOnOutOfMemoryError

发生OutofMemoryError错误时,自动生成hprof内存快照文件:

-XX:HeapDumpPath=< path >:

指定hprof文件的输出路径。

打印GC日志

JDK8及之前 : -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:文件路径

JDK9及之后 : -Xlog:gc*:file=文件路径

总结

image.png

垃圾回收器的参数设置

优化垃圾回收器的参数,比如-XX:CMSInitiatingoccupancyFraction=值,当老年代大小到达该阈值时,会自动进行CMS垃圾回收,通过控制这个参数提前进行老年代的垃圾回收,减少其大小。JDK8中默认这个参数值为-1,根据其他几个参数计算出值:((100-MinHeapFreeRatio)+(double)(CMSTriggerRatio * MinHeapFreeRatio)/ 100.0)

3. 性能调优

线程转储文件

线程转储(Thread Dump)提供了对所有运行中的线程当前状态的快照。线程转储可以通过jstack、visualvm等工具获取。其中包含了线程名、优先级、线程ID、线程状态、线程栈信息等等内容,可以用来解决CPU占用率高死锁等问题。

采用 jstack命令就可以导出线程转储文件。

线程转储(Thread Dump)中的几个核心内容

  1. 名称: 线程名称,通过给线程设置合适的名称更容易“见名知意
  2. 优先级:线程的优先级
  3. Java ID:JVM中线程的唯一ID
  4. 本地 ID:操作系统分配给线程的唯一ID
  5. 状态:NEW(刚刚创建),RUNNABLE(准备执行),BLOCKED(阻塞),WAITING(没有时间等待),TIME_WAITING(有时间等待),TERMINATED(已完成)
  6. 栈追踪:显示整个方法的栈帧信息。

线程转储的可视化在线分析平台:

  1. jstack.review/
  2. fastthread.io/

定位CPU占用率高问题的解决方案

  1. 通过top-c命令找到CPU占用率高的进程,获取它的进程ID.

image.png

2、使用top -p 进程ID单独监控某个进程,按H可以查看到所有的线程以及线程对应的CPU使用率,找到CPU使用率特别高的线程。

image.png

3、使用 istack 进程ID 命令可以查看到所有线程正在执行的栈信息。使用 jstack 进程ID >文件名 保存到文件中方便查看。

4、找到nid线程ID相同的栈信息,需要将之前记录下的十进制线程号转换成16进制通过 printf'%x\n'线程ID 命令直接获得16进制下的线程ID.

image.png

5、找到栈信息对应的源代码,并分析问题产生原因。

问题:如果方法中嵌套方法比较多,如何确定栈信息中哪一个方法性能较差?

接口响应时间很长的问题

在程序运行过程中,发现有几个接口的响应时间特别长,需要快速定位到是哪一个方法的代码执行过程中出现了性能问题。

解决思路:

已经确定是某个接口性能出现了问题,但是由于方法嵌套比较深,需要借助于arthas定位到具体的方法。

image.png

trace

image.png

watch

image.png

定位偏底层的性能问题

有一个接口中使用了for循环向ArrayList中添加数据,但是最终发现执行时间比较长,需要定位是由于什么原因导致的性能低下。

解决思路

image.png

profile

image.png

源代码:

所以耗时主要在

所以我们应该给 ArrayList设置元素size

死锁问题定位

  1. Jstack -| 进程ID > 文件名 将线程栈保存到本地。

在文件中搜索deadlock即可找到死锁位置

  1. 开发环境中使用 visual vm 或者 Jconsole 工具,都可以检测出死锁。使用线程快照生成工具就可以看到死锁的根源。生产环境的服务一般不允许使用这两种工具连接

获取到 线程转储文件之后可以通过 fastThread这个网站进行可视化的分析。

fastthread.io/