性能监控与调优
第一章-概述
大厂面试题
背景说明
生产环境的问题
- 生产环境发生内存溢出如何处理?
- 生产环境应该给服务器分配多少内存合适?
- 如何应对垃圾回收器的性能调优?
- 生产环境CPU负载飙高如何处理?
- 生产环境应该给分配多少线程合适?
- 不加log,如何确定请求是否执行了某一代码?
- 不加log,如何实时查看某个方法的入参与返回值?
调优目的
- 防止出现OOM
- 解决OOM
- 减少Full GC出现概率
性能优化的步骤
第一步(发现问题):性能监控
一种以非强行或者入侵方式收集或查看应用运营性数据的活动。
监控通常是指一种在生产、质量评估或者开发环境下实施的带有预防或主动性的活动。
当应用相关干系人提出性能问题却没有足够多的线索时,首先我们需要进行性能监控,其次是性能分析。
第二步(排查问题):性能分析
一种以侵入方式收集运行性能数据的活动,它会影响应用的吞吐量或者响应性。
性能分析是针对性能问题的答复结果,关注的范围通常比性能监控更加集中。
性能分析很少在生产环境下进行,通常是在质量评估、系统测试或者开发环境下进行,是性能监控之后的步骤。
第三步(解决问题):性能调优
一种为改善应用响应性或吞吐量而更改参数、源代码、属性配置的活动,性能调优是在性能监控、性能分析之后的活动。
性能测评指标
停顿时间(响应时间)
提交请求和返回请求的响应之间使用的时间,一般比较关注平均响应时间
常用操作的响应时间表:
在垃圾回收环节中:
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
-XX:MaxGCPauseMillis
吞吐量
在GC中:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间 + 内存回收时间);吞吐量为 1 - 1/(1+n)
-XX:GCTimeRatio=n
并发数
同一时刻,对服务器有实际交互的请求数
内存占用
Java堆区所占的内存大小
第二章-JVM监控及诊断工具(命令行篇)
概述
性能诊断是软件工程师在日常工作中需要经常面对和解决的问题,在用户体验至上的今天,解决好应用的性能问题能带来巨大的收益。
jps:查看正在运行的 Java 进程 pid
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
远程连接
- 确定远程服务器的ip地址
- 添加JMX(通过JMX技术具体监控远端服务器的哪个Java进程)
- 修改 bin/catalina.sh 文件,连接远程的tomcat
- 在 ../conf 中添加jmxremote.access 和 jmxremote.password 文件
- 将服务器地址改为公网ip地址
- 设置阿里云安全策略和防火墙策略
- 启动tomcat,查看tomcat启动日志和端口监听
- JMX中输出端口号、用户名、密码登录
补充:内存泄漏案例
- 静态集合类
- 单例模式
单例模式,和静态集合导致内存泄漏的原因类似,因为单例的静态特性,其生命周期和JVM生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个对象不会被回收,造成内存泄漏。
3-内部类持有外部类
内部类持有外部类,如果一个外部类的实例对象方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象不会被垃圾回收,造成内存泄漏。
4-各种连接,如数据库连接、网络连接和IO连接
5-变量(对象)不合理的作用域
6-改变哈希值
当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则,对象修改后的哈希值与最初存储进HashSet集合时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。
这也是String为什么被设置成了不可变类型,我们可以放心地把String存入HashSet,或者把String当做HashMap的key值。
7-缓存泄漏
内存泄漏的另外一个常见来源是缓存,一旦把对象引用放到缓存中,就很容易遗忘。对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。
8-监听器和回调
内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显式地取消,那么就会聚集。
需要确保回调即被当做垃圾回收的最佳方法是只保存它的弱引用,例如将它们保存为 WeakHashMap 中的键。
第四章-分析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
FullGC
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后内存占用(该区域内存总大小)
中括号内: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不够用。