JVM调优入门学习笔记

116 阅读10分钟

性能监控与调优

第一章-概述

大厂面试题

截屏2021-10-06上午10.13.20 截屏2021-10-06上午10.15.16

背景说明

生产环境的问题

  • 生产环境发生内存溢出如何处理?
  • 生产环境应该给服务器分配多少内存合适?
  • 如何应对垃圾回收器的性能调优?
  • 生产环境CPU负载飙高如何处理?
  • 生产环境应该给分配多少线程合适?
  • 不加log,如何确定请求是否执行了某一代码?
  • 不加log,如何实时查看某个方法的入参与返回值?

调优目的

  • 防止出现OOM
  • 解决OOM
  • 减少Full GC出现概率

性能优化的步骤

第一步(发现问题):性能监控

一种以非强行或者入侵方式收集或查看应用运营性数据的活动。

监控通常是指一种在生产、质量评估或者开发环境下实施的带有预防或主动性的活动。

当应用相关干系人提出性能问题却没有足够多的线索时,首先我们需要进行性能监控,其次是性能分析。

第二步(排查问题):性能分析

一种以侵入方式收集运行性能数据的活动,它会影响应用的吞吐量或者响应性。

性能分析是针对性能问题的答复结果,关注的范围通常比性能监控更加集中。

性能分析很少在生产环境下进行,通常是在质量评估、系统测试或者开发环境下进行,是性能监控之后的步骤。

第三步(解决问题):性能调优

一种为改善应用响应性或吞吐量而更改参数、源代码、属性配置的活动,性能调优是在性能监控、性能分析之后的活动。

性能测评指标

停顿时间(响应时间)

提交请求和返回请求的响应之间使用的时间,一般比较关注平均响应时间

常用操作的响应时间表:

截屏2021-10-06上午10.36.06

在垃圾回收环节中:

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。 -XX:MaxGCPauseMillis

吞吐量

在GC中:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间 + 内存回收时间);吞吐量为 1 - 1/(1+n) -XX:GCTimeRatio=n

并发数

同一时刻,对服务器有实际交互的请求数

内存占用

Java堆区所占的内存大小

第二章-JVM监控及诊断工具(命令行篇)

概述

性能诊断是软件工程师在日常工作中需要经常面对和解决的问题,在用户体验至上的今天,解决好应用的性能问题能带来巨大的收益。

jps:查看正在运行的 Java 进程 pid

截屏2021-10-06上午11.04.55

jstat:查看JVM统计信息

jstat -<option> [-t][-h<lines>]<vmid> [<interval> [<count>]]

option参数

  • 垃圾回收相关
    • -gc:显示GC相关的堆信息。包括Eden区、两个Survivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息。
    • -gccapacity:显示内容与-gc基本相同,但输出主要关注Java堆各个区域用到的最大、最小空间
    • -gcutil:显示内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
    • -gccause:与上面功能一样,但是会额外输出导致最后一次或当前正在发生的GC产生的原因。
    • -gcnew:显示新生代GC状况
    • -gcnewcapacity:显示内容与上面基本相同,输出主要关注使用到的最大、最小空间
    • -geold:显示老年代GC状况
    • -gcoldcapacity:显示内容与-gcold基本相同,输出主要关注使用到的最大、最小空间
    • -gcpermcapacity:显示永久代使用到的最大、最下空间
  • JIT相关
    • -compiler:显示JIT编译器编译过的方法、耗时等信息
    • -printcompilation:输出已经被JIT编译的方法

jinfo:实时查看和修改JVM配置参数

  • jinfo -flag 具体参数 PID 查看某个java进程的具体参数的值
  • jinfo -floag [+|-]具体参数 PID 修改该参数的值,针对boolean类型
  • jinfo -floag 具体参数=n PID 修改该参数的值,针对数值类型
  • java -XX:+PrintFlagsInitial 查看所有JVM参数启动的初始值
  • java -XX:+PrintFlagsFinal 查看所有JVM参数的最终值

jmap:导出内存映像文件&内存使用情况

  • -dump:生成Java堆转储快照-dump文件,-dump:live 只保存堆中存活的对象

  • -heap:输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等,-heap:live 只保存堆中存活的对象

  • -histo:输出堆中对象的统计信息,包括类、实例数量和合计容量

  • -permstat:以ClassLoader为统计口径输出永久代的内存状况信息(仅linux/solaris)

  • -finalizerinfo:显示在F-Queue中等带Finalizer线程执行finalize方法的对象(仅linux/solaris)

  • -F:当虚拟机进程对-dump选项没有任何响应时,可以使用此选项强制生成dump文件(仅linux/solaris)

  • -h | -help:jmap工具使用帮助命令

  • -J :传递参数给jmap启动的jvm

  • 手动生成:

    • jmap -dump:live,format=b,file=
  • 自动生成

    • -XX:+HeapDumpOnOutOfMemoryError 设置开启
    • -XX:+HeapDumpPath=<filename.hrof> 指定位置

jhat:JDK自带堆分析工具

略。请使用图形化分析工具

jstack:打印JVM中线程快照

jstack option pid

  • -F:当正常输出的请求不被响应时,强制输出线程堆栈
  • -l:除堆栈外,显示关于锁的附加信息
  • -m:如果调用到本地方法的话,可以显示C/C++的堆栈
  • -h:帮助操作

jcmd:多功能命令行

  • jcm -l:列出所有JVM进程
  • jcmd pid help:针对指定的进程,列出支持的所有命令
  • jcmd pid 具体命令:显示指定进程的指令命令的数据

第三章-JVM监控及诊断工具(图形界面)

Visual VM

远程连接

  1. 确定远程服务器的ip地址
  2. 添加JMX(通过JMX技术具体监控远端服务器的哪个Java进程)
  3. 修改 bin/catalina.sh 文件,连接远程的tomcat
  4. 在 ../conf 中添加jmxremote.access 和 jmxremote.password 文件
  5. 将服务器地址改为公网ip地址
  6. 设置阿里云安全策略和防火墙策略
  7. 启动tomcat,查看tomcat启动日志和端口监听
  8. JMX中输出端口号、用户名、密码登录

补充:内存泄漏案例

  1. 静态集合类
截屏2021-10-07上午10.31.13
  1. 单例模式

单例模式,和静态集合导致内存泄漏的原因类似,因为单例的静态特性,其生命周期和JVM生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个对象不会被回收,造成内存泄漏。

3-内部类持有外部类

内部类持有外部类,如果一个外部类的实例对象方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象不会被垃圾回收,造成内存泄漏。

4-各种连接,如数据库连接、网络连接和IO连接

截屏2021-10-07上午10.36.13

5-变量(对象)不合理的作用域

截屏2021-10-07上午10.37.41

6-改变哈希值

当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则,对象修改后的哈希值与最初存储进HashSet集合时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。

这也是String为什么被设置成了不可变类型,我们可以放心地把String存入HashSet,或者把String当做HashMap的key值。

7-缓存泄漏

内存泄漏的另外一个常见来源是缓存,一旦把对象引用放到缓存中,就很容易遗忘。对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。

8-监听器和回调

内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显式地取消,那么就会聚集。

需要确保回调即被当做垃圾回收的最佳方法是只保存它的弱引用,例如将它们保存为 WeakHashMap 中的键。

截屏2021-10-07上午11.07.07

第四章-分析GC日志

01-GC日志参数

见附录

02-GC日志格式

复习:GC分类

针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)

  • 部分收集:不是完整收集整个Java堆的垃圾。其中又分为:
    • 新生代收集(Minor GC / Young GC):只是新生代(Eden\S1,S1)的垃圾收集
    • 老年代收集(Major GC / Old GC):只是老代的垃圾收集
      • 目前,只有CMS GC会有单独收集老年代的行为。
      • 注意,很多时候Major GC 和 Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收
    • 混合收集(Mixed GC):收集整个新生代以及老年代的垃圾。
      • 目前,只有G1 GC会有这种行为
  • 整堆收集(Full GC):收集整个java堆和方法区的垃圾

GC日志分类

MinorGC

20210217184224387

FullGC

20210217184232633

03-GC日志分析工具

垃圾收集器

  • Serial -> Default New Generation -> [DefNew]
  • ParNew -> Parallel New Generation -> [ParNew]
  • Parallel Scavenge -> [PSYoungGen]
  • Parallel Old Generation -> [ParOldGen]
  • G1 -> garabage-first heap
  • Allocation Failure -> 表名本次引起GC的原因是年轻代中没有足够的空间能够存储新的数据了

GC前后情况

通过图示,我们可以发现GC日志的规律一般都是: GC前内存占用 -> GC后内存占用(该区域内存总大小)

截屏2021-10-08下午12.58.07

中括号内:GC回收前年轻代堆大小,回收后大小,(年轻代堆总大小) 括号外:GC回收前年轻代和老年代的大小,回收后大小,(年轻大和老年代总大小)

GC时间

GC日志中有三个时间:user、sys、real

  • user - 进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际CPU时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示GC线程执行所使用的CPU总时间。
  • sys - 进程在内核态消耗的CPU时间,即 在内核执行系统调用或等待系统事件所使用的的CPU时间
  • real - 程序从开始到结束所使用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等带 I/O 完成)。对于并行GC,这个数字应该接近(用户时间 + 系统时间)除以垃圾收集器使用的线程数。

由于多核的原因,一般的GC事件中,real time要小于 sys+user 的,因为一般是多个线程并发的去做GC,所以real time是要小于sys + user的。如果 real > sys + user 的话,则你的应用可能存在以下问题:IO负载非常重或者CPU不够用。