文章

JVM_调优

JVM 调优

内存调优

1.解决什么问题:解决内存泄漏,define-不使用的对象仍然存在GC引用链上,无法被GC回收。

解决思路

1.发现问题,使用监控工具

2.定位问题,定位代码

3.修复

4.测试

常见场景

1.分布式任务调度系统如Elastic-job、 Quartz等进行任务调度时,被调度的Java应用在
调度任务结束中出现了内存泄漏,最终导致多次调度之后内存溢出。

2.内存泄漏导致溢出的常见场景是大型的Java后端应用中,在处理用户的请求之后,没有及时将用户的数据删除。随着用户请求数量越来越多,内存泄漏的对象占满了堆内存最终导致内存溢出

如何发现

  1. Top-M

    -M 按照内存排序 可用检查java 线程的内存使用情况

    -C 按照CPU使用率排序

image-dvtf.png

缺点:只能查看最基础的进程信息,无法查看到每个部分的内存占用(堆、方法区、堆外)

  1. visual VM:VisualVM是多功能合一的Java故障排除工具并且他是一款可视化工具,整合了命令行 JDK 工具和轻量级分析功能,功能非常强大。

特点:功能丰富,实时监控CPU、内存、线程等详细信息

  1. Arthas:Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率
    特点:功能强大,不止于监控基础的信息,还能监控单个方法的执行耗时等细节内容

具体案例

背景:

小李的团队已经普及了arthas的使用,但是由于使用了微服务架构,生产环境上的应用数量非常多,使用arthas还得登录到每一台服务器上再去操作非常不方便。他看到官方文档上可以使用tunnel来管理所有需要监控的程序。

步骤:

  1. 在Spring Boot程序中添加arthas的依赖(支持Spring Boot2),在配置文件中添加

tunnel服务端的地址,便于tunnel去监控所有的程序。

  1. 将tunnel服务端程序部署在某台服务器上并启动。

  2. 启动java程序

  3. 打开tunnel的服务端页面,查看所有的进程列表,并选择进程进行arthas的操作。

异常的内存的案例

代码的导致的泄露

1.equals()和hashCode()导致的内存泄漏

image 1.png

2.ThreadLocal的使用

image.png

3.String的intern方法

image.png

4.资源没有正常关闭

image.png

并发请求问题
1.并发请求问题指的是用户通过发送请求向Java应用获取数据,正常情况下Java应用将数据返回之后,这部分数据就可以在内存中被释放掉。但是由于用户的并发请求量有可能很大,同时处理数据的时间很长,导致大量的数据存在于内存中,最终超过了内存的上限,导致内存溢出。 这类问题的处理思路和内存泄漏类似,首先要定位到对象产生的根源。(原因就是请求耗时较长,导致线程较多,最终耗尽内存)

诊断

步骤

1.使用visual VM保存内存快照(heap dump)

2.使用MAT打开hprof文件,并选择内存泄漏检测功能, MAT会自行根据内存快照中保存的数据分析内存泄漏的根源

3.添加虚拟机参数:在发生oom的时候自动保存heap dump

-XX:+HeapDumpOnOutOfMemoryError:发生OutOfMemoryError错误时,自动生成hprof内存快照文件。

-XX:HeapDumpPath=:指定hprof文件的输出路径。

  1. 如何在不发生oom的时候保存heap dump:XX:+HeapDumpBeforeFullGC可以在FullGC之前就生成内存快照——可用主动调用system.gc 或者

    通过jdk的jmap命令导出

    jmap -dump:live,format=b,file=文件路径和文件名 进程ID

    通过Arthas导出

    heapdump --live 文件路径和文件名

  2. 定位问题:

    1. 先使用jsp 确认java 的线程Id,使用jmap -histo:live 进程ID > 文件名 存活对象以直方图的形式保存到文件中,这个过程会影响用户的时间,但是时间比较短暂\
    2. 分析内存占用最大的对象,一般就是内存泄露的对象
    3. 使用Arthus的stack命令,追踪对象创建的方法和被调用路径

    image.png

MAT的原理

  1. 支配树

    MAT提供了称为支配树(Dominator Tree) 的对象图。支配树展示的是对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。

    image.png

  2. 深堆 & 浅堆

    1. 支配树中对象本身占用的空间称之为浅堆(Shallow Heap) 。
    2. 支配树中对象的子树就是所有被该对象支配的内容,这些内容组成了对象的深堆(Retained Heap) ,也称之为保留集( Retained Set ) 。 深堆的大小表示该对象如果可以被回收,能释放多大的内存空间
  3. MAT就是根据支配树,从叶子节点向根节点遍历,如果发现深堆的大小超过整个堆内存的一定比例阈值,就会将其标记成内存泄漏的“嫌疑对象”

GC调优

目标:

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

  1. 通用jvm参数设置
  2. 特定的垃圾回收期设置
  3. 解决频繁full gc引引起的程序性能问题

核心指标

  1. 吞吐量:吞吐量分为业务吞吐量和垃圾回收吞吐量,保证高吞吐量的常规手段有两条:

    1. 优化业务执行性能,减少单次业务的执行时间
    2. 优化垃圾回收吞吐量

    垃圾回收吞吐量 :

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

    延迟:延迟指的是从用户发起一个请求到收到响应这其中经历的时间。延迟 = GC延迟 + 业务执行时间,所以如果GC时间过长,会影响到用户的使用

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

发现问题

  1. 使用 jstat -gc 进程ID 每次统计的间隔(毫秒) 统计次数

image.png

  1. 使用visual VM

    image.png

  2. 阅读GC日志

    生成GC日志:使用JVM的参数调整:--XX:+PrintGCDetails

    如何阅读:使用GCV-viewer 或者 GCeasy工具(https://gceasy.io/

常见的GC模式

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

image.png

  1. 缓存对象过多:特点:呈现锯齿状,对象创建之后内存上升,一旦发生垃圾回收之后下降到底部,并且每次下降之后的内存大小接近,处于比较高的位置。问题产生原因: 程序中保存了大量的缓存对象,导致GC之后无法释放,可以使用MAT或者HeapHero等工具进行分析内存占用的原因。

image.png

  1. 内存泄露:呈现锯齿状,每次垃圾回收之后下降到的内存位置越来越高,最后由于垃圾回收无法释放空间导致对象无法分配产生OutOfMemory的错误。问题产生原因: 程序中保存了大量的内存泄漏对象,导致GC之后无法释放,可以使用MAT或者HeapHero等工具进行分析是哪些对象产生了内存泄漏

image.png

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

image.png

  1. 元空间不足导致full gc:内存的大小并不是特别大,但是持续发生FULLGC。问题产生原因: 元空间大小不足,导致持续FULLGC回收元空间的数据。

image.png

解决手段

  1. 优化基础JVM参数

    1. 参数1:

      -Xmx:最大堆空间

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

      最合理的设置方式应该是根据最大并发量估算服务器的配置,然后再根据服务器配置计算最大堆内存的值。

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

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

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

      –XX:MetaspaceSize=256m,参数指的是到达这个值之后会触发FULLGC,后续什么时候再触发JVM会自行计算。如果设置为和MaxMetaspaceSize一样大,就不会FULLGC,但是对象也无法回收

    3. 参数3:

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

    4. 不建议手动设置的参数
      由于JVM底层设计极为复杂,一个参数的调整也许让某个接口得益,但同样有可能影响其他更多接口:

      example:

        • Xmn 年轻代的大小,默认值为整个堆的1/3,可以根据峰值流量计算最大的年轻代大小,尽量让对象只存放在年轻代,不进入老年代。但是实际的场景中,接口的响应时间、创建对象的大小、程序内部还会有一些定时任务等不确定因素都会导致这个值的大小并不能仅凭计算得出,如果设置该值要进行大量的测试。 G1垃圾回收器尽量不要设置该值, G1会动态调整年轻代的大小
      • ‐XX:SurvivorRatio 伊甸园区和幸存者区的大小比例,默认值为8。
      • ‐XX:MaxTenuringThreshold 最大晋升阈值,年龄大于此值之后,会进入老年代。另外JVM有动态年龄判断机制:将年龄从小到大的对象占据的空间加起来,如果大于survivor区域的50%,然后把等于或大于该年龄的对象,放入到老年代

    image.png

  2. 垃圾回收器的选择

    目前的GC组合:

    1. PS(Parallel Scavenge)+PO(Parallel Old)
    2. PN+CMS→退化 serial Old
    3. serial +serial Old
    4. serial + CMS
    5. PS+ serial Old
    6. G1

    image.png

    JDK8 默认为PS+PO,可选PN+CMS

    案例

    • CMS的并发模式失败(concurrent mode failure)现象。由于CMS的垃圾清理线程和用户线程是并行进行的,如果在并发清理的过程中老年代的空间不足以容纳放入老年代的对象,会产生并发模式失败。 并发模式失败会导致Java虚拟机使用Serial Old单线程进行FULLGC回收老年代,出现长时间的停顿。
    • 解决:

    image.png

问题:

image.png

步骤:

  1. 先使用打印GC日志:-XX PrintGcDetail
  2. 使用easyGC分析GC日志
  3. 关注参数:吞吐量,延迟,内存使用,fullGC情况
  4. 如果存在内存问题,使用jmap/arthus 保存内存快照
  5. 使用MAT/heaphero进行分析内存
  6. 修复问题

如何判断当前线程的执行情况:使用jsp 找到线程ID ,再使用jstack-线程id 查看堆栈情况

性能调优

常见问题

1.CPU占用率高

  1. 使用Top -C 按照CPU负载进行排序,多核情况下负载可能CPU负载>100

2.请求单个服务处理时间特别长,多服务使用skywalking等监控系统来判断是哪一个环节性能低下

  1. 程序启动之后运行正常,但是在运行一段时间之后无法处理任何的请求(内存和GC正常)。

线程转储的查看方式

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

    image.png

解决方式

  1. CPU占用率高

    1. 使用Top -c 查找CPU占用高的进程,
    2. 使用Top -p 进程ID,监控指定进程的线程CPU使用情况,找到使用率高的线程
    3. 然后使用jstack 进程ID ,命令可以查看到所有线程正在执行的栈信息。使用 jstack 进
      程ID > 文件名 保存到文件中方便查看
    4. 通过找nid = 线程ID 的栈信息,需要将之前记录下的十进制线程号转换成16进制,通过 printf ‘%x\n’ 线程ID 命令直接获得16进制下的线程ID/或者计算器
    5. 找到栈信息对应的源代码,并分析问题产生原因
    6. 主要关注状态为Runnable的线程,但实际上,有一些线程执行本地方法时并不会消耗CPU,而只是在等待。但 JVM 仍然会将它们标识成“RUNNABLE” 状态,比如等待socket读取数据, IO等待不消耗CPU
  2. 接口响应时间很长的问题,需要快速定位到是哪一个方法的代码执行过程中出现了性能问题

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

    1. 使用Arthus提供的trace 类名 方法名, 命令

    image.png

    b. 使用trace定位到性能较低的方法之后,使用watch命令监控该方法,可以获得更为详细的方法信息,获得执行方法的参数

    image.png

    c. 使用stop命令将所有增强的对象恢复

    d.总结

image.png

  1. 定位偏底层的性能问题

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

    image.png

    思路:Arthas提供了性能火焰图的功能,可以非常直观地显示所有方法中哪些方法执行时间比较长。

    使用方法:使用arthas的profile命令,生成性能监控的火焰图。

    命令1: profiler start 开始监控方法执行性能

    命令2: profiler stop --format html 以HTML的方式生成火焰图火焰图中一般找绿色部分Java中栈顶上比较平的部分,很可能就是性能的瓶颈

    image.png

  2. 线程被耗尽问题

    问题:程序在启动运行一段时间之后,就无法接受任何请求了。将程序重启之后继续运行,依然会出现相同的情况。

    解决思路:线程耗尽问题,一般是由于执行时间过长,分析方法分成两步:
    a. 检测是否有死锁产生,无法自动解除的死锁会将线程永远阻塞。
    b. 如果没有死锁,再使用案例1的打印线程栈的方法检测线程正在执行哪个方法,一般这些大
    量出现的方法就是慢方法。

    1. 死锁的检查

      jstack -l 进程ID > 文件名 将线程栈保存到本地。
      在文件中搜索deadlock即可找到死锁位置:

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

License:  CC BY 4.0