*性能监控与调优
概述
为什么要调优?
1.防止出现OOM
2.解决OOM
3.减少Full GC出现的频率
监控的依据:
1.运行日志
2.异常堆栈
3.GC日志
4.线程快照
5.堆转储快照
性能优化的步骤:
1.(发现问题)性能监控
GC频繁
cpu load过高
OOM
内存泄露
死锁
程序响应时间较长
2.(排查问题)性能分析:开发环境下进行
打印GC日志,通过GCviewer或者http://gceasy.io分析日志信息
灵活运用命令行工具,jstack,jmap,jinfo等
dump出堆文件,使用内存分析工具分析文件
使用阿里Arthas,或jconsole,JVisualVM来实时查看JVM状态
jstack查看堆栈信息
3.(解决问题)性能调优
适当增加内存,根据业务背景选择垃圾回收器
优化代码,控制内存使用
增加机器,分散节点压力
合理设置线程池数量
使用中间件提高程序效率,比如缓存,消息队列等
其他....
性能评价/测试指标
1.停顿时间(或响应时间):提交请求和返回该请求响应之间使用的时间,一般比较关注平均响应时间【主要关注】
垃圾回收环节中暂停时间(STW):执行垃圾收集器时,程序的工作线程被暂停的时间
2.吞吐量【主要关注】
单位时间内完成的工作量
在GC中:运行用户代码的时间占总运行时间的比例(总运行时间=程序运行时间+内存回收时间)-XX:GCTImeRatio=n 1-1/(1+n)
3.并发数
同一时刻,对服务器有实际交互的请求数
4.内存占用
java堆区所占内存大小
5.相互间关系
高速公路为例
*命令行工具
1.jps(Java Process Status)显示指定系统内所有的Hotspot虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程
jps[options][hostid]
options:
-q:只看pid
-l:多了完整的路径包名
-m:输出虚拟机进程启动时传递给主类main()的参数
-v:列出虚拟机进程启动时的jvm参数 例如Xms100m
以上参数可以综合使用,如果某java进程关闭了默认开启的UsePerfData参数(-XX:-UsePerfData),那么jps就无法使用
hostid:
如果想要远程监控主机上的java程序,需要安装jstatd
2.jstat(JVM Statistics Monitoring):查看jvm统计信息,用于监视虚拟机各种运行状态信息的命令行工具,他可以显示本地或者远程虚 拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据
jstat -<options> [-t] [-h<lines>] <wind> [<interval>[<count>]]
options:
类装载相关的:
-class:显示ClassLoader的相关信息:类的装载、卸载数量、总空间、类装载所消耗的时间等
垃圾回收相关的:
-gc:显示与GC相关堆信息
-gccapacity:与-gc基本相同堆各个区域使用到的最大、最小空间
-gcutil:与-gc基本相同,已使用空间占总空间的百分比
-gccause:与-gc基本相同,重点导致gc原因
-gcnew:显示新生代GC状况
-gcnewcapacity:显示内容与gcnew基本相同,主要关注最大最小空间
-gcold:显示老年代GC状况
-gcoldcapacity:显示内容与-gcold基本相同,输出主要关注使用到的最大、最小空间
-gcpermcapacity:显示永久代使用到的最大、最小空间
JIT相关的:
-compiler:显示JIT编译器编译过的方法、耗时等信息
-printcompilation:输出已经被JIT编译的方法
interval参数:用于指定输出统计数据的周期,单位为毫秒。即:查询间隔(每隔多少打印一次)
count参数:用于指定查询的总次数
-t参数:可以在输出信息前加上一个Timestamp列,显示程序的运行时间。
-hn参数:可以在周期性数据输出时,输出多少行数据后输出一个表头信息 n为每隔几个输出表头
3.jinfo(Configuration Info for java):查看虚拟机配置参数信息,也用于调整虚拟机的配置参数
查看:
jinfo -sysprops PID 一些系统的设置
jinfo -flags PID 查看曾经赋过值的一些参数
jinfo -flag 具体参数PID 查看某个java进程的具体参数的值(垃圾收集器等UseParallelGC)
修改:(并非所有都可以修改,只有被标记为manageable的flag可以被实时修改)
针对boolean类型 :jinfo -flag [+|-]具体参数 PID
针对非boolean类型:jinfo -flag 具体参数=具体参数 PID
拓展:
java -XX:+PrintFlagsInital 查看所有JVM参数启动的初始值
java -XX:+PrintFlagsFinal 查看所有JVM参数的最终值
java -XX:+PrintCommandLineFlags 查看那些已经被用户或者JVM设置过的详细的XX参数的名称和值
4.jmap(JVM memory Map):导出内存映像文件&内存使用情况,一方面是获取dump文件(堆转储快照文件,二进制文件),它还可以获取目标 Java进程的内存相关信息,包括java堆各区域的使用情况、堆中对象的统计信息、类加载信息等
基本语法:
jmap [options] <pid>
jmap [options] <executable <core>>
-dump :生成Java堆转储快照:dump文件 -dump:live 只保存堆中的存活对象【关注什么数据导致内存溢出等等】
手动方式:
jmap -dump:format=b,file=<filename.hprof> <pid>
jmap -dump:live,format=b,file=<filename.hprof> <pid>
自动方式(Dump之前会触发一次Full GC,在报OOM的时候自动生成一个dump文件)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=<filename.hprof >
-heap :输出堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等
jmap -heap pid
jmap -histo pid
-histo:输出堆中对象的统计信息,包括类、实例数量和合计容量; -histo:live只统计堆中的存活对象
-permstat:永久代信息
-finalizerinfo:显示在F-Queue 中等待Finalizer线程执行finalize方法的对象
-F :-dump没有任何响应,可以使用这个强制生成dump文件
5.jhat(JVM Heap Analysis Tool):jdk自带堆分析工具,与jamp搭配使用,jhat内置了一个http/HTML服务器,生成dump文件分析结果
jhat在jdk9、jdk10中删除了,官方建议使用VisualVM
6.jstack(JVM Stack Trace):打印JVM中线程快照,用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪),可用于定位线程出现 长时间停顿的原因。
7.jcmd:多功能命令行,可以用来除了jstat之外所有命令的功能。比如:用它来导出堆、内存使用、好看java进程、导出线程信息、执行GC、 JVM运行时间等。
jcmd -l 列出所有JVM进程 替换jps
jcmd pid help 针对指定进程,列出支持的所有指令
jcmd pid 具体命令 显示指定进程的指令命令的数据
8.jstatd:远程主机信息收集,它的作用相当于代理服务器,建立本地计算机与远程监控工具的通信。jstatd服务器将本机的java应用程序信 息传递到远程计算机。
*JVM监控及诊断GUI工具
一、概述
JDK自带的工具:
jconsole
VisualVM
JMC
第三方:
MAT
JProfiler
Arthas
Btrace
二、jConsole
用于对JVM中内存、线程和类等的监控,是一个基于JMX(java management extensions)的GUI性能监控工具,在cmd输入jconsole
三种连接方式:
Local:连接本地运行的JVM
Remote:使用URL通过RMI连接器连接到一个JMX代理
Advanced:
三、Visual VM
多合一的故障诊断和性能监控的可视化工具。可以显示虚拟机的进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC
堆、方法区及线程的信息(jstat、jstack)等,可以代替JConsole。(https://visualvm.github.io/index.html下载)
也可以支持插件(https://visualvm.github.io/pluginscenters.html)
浅堆与深堆
浅堆:是指一个对象所消耗的内存。引用类型跟具体值无关
深堆:是指对象的保留集中所有的对象的浅堆大小之和。
保留集:对象A的保留集指当对象A被垃圾回收后,可以释放的所有的对象的集合(包括A本身),即对象A的保留集可以被认为是只能通 过对象A被直接访问到的所有对象的集合。
对象实际大小:一个对象所能触及的所有对象的浅堆大小之和
【对象的大小=对象头(要看是多少位计算机)+对象数据+对齐偏移】
支配树(Dominator tree):源自图论
体现了对象实例间的支配关系,所有指向B的路径都经过对象A,则认为对象A支配对象B
内存泄漏:
理解与分类:
可达性分析算法判断对象是否是不再使用的对象,JVM误以为对象还在使用无法回收而造成内存泄漏,宽泛意义上在实际情况很多时候一 些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”。
是否还被使用? 是 是 否
是否还被需要? 是 否 否
内存泄露 否 是 否
内存泄漏与内存溢出的关系?
1.内存泄露
申请了1024M内存,分配了512内存一直不回收,那么可以用的内存就只有512,仿佛泄露了一部分。【占着茅坑不拉屎】
2.内存溢出:
申请内存时没有足够内存可以用。一个厕所三个坑,有两个占着茅坑不走的(内存泄漏),剩下一个坑来了两个人,坑位不够,内存 泄漏变成内存溢出了。
泄露的分类
经常发生:发生内存泄漏的代码会被执行多次,每次执行,泄露一块内存
偶然发生:在某些特定情况下才会发生
一次性:发生内存泄露的方法只会执行一次
隐式泄露:一直占着不释放,直到执行结束;严格的说这个不算内存泄露,以为最终释放掉了,但是如果执行时间特别长,也可能会导 致内存耗尽。
java中内存泄漏的8种情况
1.静态集合类:HashMap、LinkedList等等,如果这些容器为静态的,那么他们的生命周期与jVM程序一致,则容器中的对象在程序 结束之前将不能被释放。就是长生命周期的对象持有短生命周期对象的引用导致不能回收
代码:
public class MemoryLeak{
static List list = new ArrayList();
public void oomTests(){
Object obj = new Object();
lsit.add(obj);
}
}
2.单例模式:和静态导致内存泄露的原因类似,因为单例的静态特性,他的生命周期和JVM的生命周期一样长,所以如果单例对象如果 持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。
3.内部类持有外部类:如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部 类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成 内存泄露。
4.各种连接,如数据库、网络连接和IO连接等:数据库不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后垃 圾回收器才会回收对应的对象,否则如果在访问数据库的过程中,对Connection、 Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,导致泄漏 代码:
public static void main(String[] args){
try{
Connection conn = null;
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("url","root","123");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from student");
}catch(Exception e){
}finally{
//1.关闭结果集 Statement
//2.关闭声明的对象 ResultSet
//关闭连接 Conn.close
}
}
5.变量不合理的作用域:一般而言,一个变量的定义的作用范围大于其适用范围,很有可能会造成内存泄露,另一方面,如果没有及时 地把对象设置为null,很有可能会导致内存泄露
代码:
public class UsingRandom{
private String msg;
public void receiveMsg(){
readFromNet();//从网络中接收数据保存到msg
saveDB();//把msg 保存到数据库中
}
}
如上代码,通过readFromNet方法把接收的消息存到变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已 经没用了,由于msg的生命周期与对象的生命周期相同,此时msg不能被收回,因此造成了内存泄露。 实际上可以把msg放到 receiveMsg方法内,方法使用完就可以回收了,还有一种方法就是在使用完msg之后,把msg设置为null这样垃圾回收器也会回收
6.改变哈希值:当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算的哈希值字段了,否则对象修改后的 哈希值与最初存储进HashSet集合中的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用 作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也导致无法从HashSet集合中单独删除当前对 象,造成内存泄露【总结:如果属性参与了哈希值的计算,在存入集合之后就不能对属性进行修改,如果没有则可以】
7.缓存泄露:一旦你把对象放入缓存中,他就很容易遗忘。对于这个问题可以改成弱引用(WeakHashMap)代表缓存,此种Map的特点是 当除了自身有对Key的引用外,此key没有其他引用那么此map会自动丢弃此值
8.监听器和回调:如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚,需要确保回调立即被当作垃圾回收的最 佳方法是只保存它的弱引用,例如将他们保存成为WeakHashMap中的键。
内存泄漏案例分析
栈中弹栈时只移动了指针没有把具体值回收,事实上是还存在栈中的,所以我们需要把要弹的对象置空。
OQL语言查询对象信息:
SELECT * FROM java.util.Vector v
四、JProfiler:测试运行时内存占用情况(https://www.ej-technologies.com/products/jprofiler/overview.html)
数据采样方式:
Instrumentation重构模式:
这是JProfiler全功能模式。在class加载之前,JProfiler把相关功能代码写入到需要分析的class的bytecode中,对 行的jvm有一定影响
优点:功能强大。在此设置中,调用堆栈信息是准确的
缺点:若要分析的class较多,则对应用的性能影响较大,CPU开销可能很高(取决于Filter的控制)。此模式一般配合Filter 使用,只对特定的类或包进行分析。
Samping抽样模式:类似于样本统计,每隔一定时间将每个线程栈中方法栈中的信息统计出来
优点:对CPU的开销非常低,对应用影响小(即使你不配置任何Filter)
缺点:一些数据/特性不能提供(例如:方法的调用次数、执行时间)
遥感监测Telemetries
内存视图Live Memory
内存中对象的情况:
1.频繁创建的Java对象:死循环、循环次数过多
2.存在大的对象:读取文件时,byte[]应该边读边写。-->如果长时间不写出的话,导致byte[]过大
3.存在内存泄露
堆遍历heap walker
cpu视图cpu views
线程视图 threads
线程分析关注三个方面:
1.web容器的线程最大数 比如:Tomcat的线程容量应该略大于最大并发数
2.线程阻塞
3.线程死锁
监视器&锁 Monitors&locks
五、Arthas:在线排查问题,无需重启;动态跟踪java代码,实时监控jvm状态(https://arthas.aliyun.com/zh-cn)
wget https://alibaba.github.io/arthas/arthas-boot.jar
【具体命令看官方文档】
六、Java Mission Control:java应用程序进行管理、监视、概要分析和故障排除的工具套件
java Flight Recorder java飞行记录器:是jmc的其中一个组件能够以极低的性能开销收集java虚拟机性能数据、
七、其他
1.火焰图(Flame Graphs)
在追求极致性能的场景下,了解你的程序运行过程中cpu在干什么很重要,火焰图就是一种非常直观的展示cpu在程序整个生命周期过程中时 间分配的工具。
2.Tprofiler:可以将TPS(Transactions Per Second 每秒事务数量)由2.5提升到20,并准确定位系统瓶颈
3.Btrace:为java提供安全可靠的动态跟踪分析工具。
*JVM运行时参数
一、JVM参数选项类型
标准参数选项:比较稳定,以-开头,java -help可以看到所有的标准参数选项
java -version
-server
-client
-X参数选项:非标准化,功能稳定,以-X开头,java -X可以看到所有参数选项
-Xms<size> 等价 -XX:InitialHeapSize
-Xmx<size> 等价 -XX:MaxHeapSize
-Xss<size> 等价 -XX:ThreadStackSize java线程堆栈大小
-Xmixed
-Xint
-XX参数选项:非标准化参数,使用的最多的类型,以-XX开头,用于开发和调试jvm
分类:
Boolean类型:-XX:+/-<option> 表示启用/禁用
-XX:-UseParallelGC
-XX:+UseG1GC 表示启用G1收集器
-XX:+UseAdaptiveSizePolicy 自动选择年轻代区大小和相应的Survivor区比例
非Boolean:
数值型 -XX:<option>=<number>
-XX:NewSize=1024m
-XX:GCTimeTRatio=19
非数值型:-XX:<name>=<string>
-XX:HeapDumpPath=/usr/local/heapdump.hprof
特别地: -XX:+PrintFlagsFinal:输出所有参数的名称和默认值,默认不包括Diagnostic和Experimental的参数
二、添加JVM参数选项:
IDEA :Run configuration
运行jar包:java -Xms20m -Xmx20m demo.jar
通过Tomcat运行war包:在catalina.bat中添加类似如下配置 set"JAVA_OPTS=-Xms20m -Xmx20m"
程序运行过程中(比较少):使用jinfo -flag <name>=<value> <pid>设置非Boolean参数类型;
使用jinfo -flag [+|-]<name> <pid>设置Boolean类型参数
三、常用JVM参数:
1.打印设置的XX选项及值
-XX:+PrintCommandLineFlags 可以让程序运行前打印出用户手动设置或者JVM自动设置的xx选项
-XX:+PrintFlagsInitial 表示打印出所有xx选项的默认值
-XX:+PrintFlagsFinal 表示打印出XX选项在运行程序时生效的值
-XX:+PrintVMOptions 打印JVM参数
2.堆、栈、方法区等内存大小设置
栈大小:Xss128k
堆:-Xms -Xmx.........
方法区:永久代和元空间。永久代:-XX:PermSize=256m....元空间:-XX:MetaspaceSize 初始空间大小
3.OutOfMemory相关的选项
-XX:+HeapDumpOnOutOfMemoryError 表示内存出现OOM的时候,把Heap转存(Dump)到文件以便后续分析
-XX:+HeapDumpBeforeFullGC 在FUllGC之前,生成Heap转储文件
-XX:HeapDumpPath=<path> 指定heap转存文件的存储路径
-XX:OnOutOfMemoryError=/opt/server/restart.sh 指定一个可行性程序或者脚本的路径,当发生OOM的 时候,去执行这个脚本
4.垃圾收集器相关选项
查看默认垃圾收集器:jinfo -flag 相关垃圾回收器参数 进程id
SerialGC:client模式下使用,老年代用Serial Old GC -XX:+UseSerialGC
ParNewGC:并行回收器 -XX:+UseParNewGC
ParallelGC:jdk8默认开启主打吞吐量,Parallel OldGC配合使用 -XX:UseParallelGC
CMSGC:低延迟,并发(用户线程与垃圾) -XX:+UseConcMarkSweepGC
G1GC:低延迟,区域分代化 -XX:+UseG1GC
5.GC日志相关选项
常用:
-verbose:gc 输出gc日志信息,默认输出到标准输出
-XX:+PrintGC 等同于-verbose:gc表示打开简化的GC日志
-XX:+PrintGCDetails 在垃圾回收时打印内存回收详细的日志,并在进程退出时输出当前内存各区域分配情况
-XX:+PrintGCTimeStamps 输出GC发生时的时间戳(不能独立使用与PrintGCDetails配合使用)
-XX:+PrintGCDateStamps 时间戳以日期形式
-XX:+PrintHeapAtGC 每一次GC前和gc后都打印堆信息,可以独立使用
-Xloggc:<file> 把gc日志输出到指定文件中
6.其他参数
-XX:+DoEscapeAnalysis 开启逃逸分析
四、通过java代码获取JVM参数:
java.lang.management包用于监视和管理java虚拟机和java运行时中的其他组件
五、分析GC日志
1.GC日志参数
-verbose:gc 输出gc日志信息,默认输出到标准输出
-XX:+PrintGC 等同于-verbose:gc表示打开简化的GC日志
-XX:+PrintGCDetails 在垃圾回收时打印内存回收详细的日志,并在进程退出时输出当前内存各区域分配情况
-XX:+PrintGCTimeStamps 输出GC发生时的时间戳(不能独立使用与PrintGCDetails配合使用)
-XX:+PrintGCDateStamps 时间戳以日期形式
-XX:+PrintHeapAtGC 每一次GC前和gc后都打印堆信息,可以独立使用
-Xloggc:<file> 把gc日志输出到指定文件中
2.GC日志格式
GC分类:分为两大类型:一种是部分收集(Paratial GC) ,一种是整堆收集(FUll GC)
部分收集:
1.新生代收集(Minor Gc/Young GC):只是新生代(Eden,s0,s1)的垃圾回收
2.老年代收集(Major GC/Old GC):只是老年代收集
目前只有CMS GC会单独收集老年代行为
注意:很多时候Major GC会和FUll GC混合使用,需要具体分辨是老年代回收还是整堆回收
3.混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集 目前只有G1GC会有这种行为
整堆收集(FullGC):收集整个java堆和方法区的垃圾回收
哪些情况会触发Full GC?
1.老年代空间不足
2.方法区空间不足
3.显示调用System.gc()
4.Minor GC 进入老年代的平均大小大于老年代的二用内存
5.大对象直接进入老年代而老年年代可用空间不足会触发Full GC
FullGC(发生GC的原因)
GC日志分类:
MinorGC
FullGC(新生代老年代元空间 )
GC日志结构剖析:
垃圾收集器
GC前后情况
GC时间 :user,sys和real real小于sys+user
3.GC日志分析工具
GCeasy
GCViewer