【JVM】JVM 监控及诊断工具
JVM 调优概述
生产环境中的问题
- 生产环境发生了内存溢出该如何处理?
- 生产环境应该给服务器分配多少内存合适?
- 如何对垃圾回收器的性能进行调优?
- 生产环境CPU负载飙高该如何处理?
- 生产环境应该给应用分配多少线程合适?
- 不加log,如何确定请求是否执行了某一行代码?
- 不加log,如何实时查看某个方法的入参与返回值?
为什么要调优
- 防止出现OOM
- 解决OOM
- 减少Full GC出现的频率
不同阶段的考虑
- 上线前
- 项目运行阶段
- 线上出现OOM
调优概述
监控的依据
- 运行日志
- 异常堆栈
- GC日志
- 线程快照
- 堆转储快照
调优的大方向
- 合理地编写代码
- 充分并合理的使用硬件资源
- 合理地进行JVM调优
性能优化的步骤
第1步:性能监控
- GC频繁
- cpu load过高
- OOM
- 内存泄露
- 死锁
- 程序响应时间较长
第2步:性能分析
- 打印GC日志,通过GCviewer或者 http://gceasy.io 来分析异常信息
- 灵活运用命令行工具、jstack、jmap、jinfo等
- dump出堆文件,使用内存分析工具分析文件
- 使用阿里Arthas、jconsole、JVisualVM来实时查看JVM状态
- jstack查看堆栈信息
第3步:性能调优
- 适当增加内存,根据业务背景选择垃圾回收器
- 优化代码,控制内存使用
- 增加机器,分散节点压力
- 合理设置线程池线程数量
- 使用中间件提高程序效率,比如缓存、消息队列等
性能评价/测试指标
- 停顿时间(或响应时间):提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间。在垃圾回收环节中:
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
- 通过
-XX:MaxGCPauseMillis
参数进行设置
- 吞吐量:
- 对单位时间内完成的工作量(请求)的量度
- 在GC中:运行用户代码的事件占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)
- 吞吐量为
1-1/(1+n)
,其中-XX::GCTimeRatio=n
- 并发数
- 同一时刻,对服务器有实际交互的请求数
- 内存占用
- Java堆区所占的内存大小
相互间的关系(以高速公路通行状况为例):
- 吞吐量:每天通过高速公路收费站的车辆的数据
- 并发数:高速公路上正在行驶的车辆的数目
- 响应时间:车速
补充:常用操作的响应时间列表
操作 | 响应时间 |
---|---|
打开一个站点 | 几秒 |
数据库查询一条记录(有索引) | 十几毫秒 |
机械磁盘一次寻址定位 | 4毫秒 |
从机械磁盘顺序读取1M数据 | 2毫秒 |
从SSD磁盘顺序读取1M数据 | 0.3毫秒 |
从远程分布式换成Redis 读取一个数据 | 0.5毫秒 |
从内存读取 1M数据 | 十几微妙 |
Java程序本地方法调用 | 几微妙 |
网络传输2Kb数据 | 1 微妙 |
浅堆深堆
浅堆(Shallow Heap)
浅堆是指一个对象所消耗的内存。在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节。根据堆快照格式不同,对象的大小可能会同8字节进行对齐。
以String为例:2个int值共占8字节,对象引用占用4字节,对象头8字节,合计20字节,向8字节对齐,故占24字节。(JDK 7中)
int | hash32 | 0 |
---|---|---|
int | hash | 0 |
ref | value | C:\Users\Administrat |
这24字节为String对象的浅堆大小。它与String的value实际取值无关,无论字符串长度如何,浅堆大小始终是24字节。
浅堆指对象本身占用的内存,不包括其内部引用对象的大小
保留集(Retained Set)
对象A的保留集指当对象A被垃圾回收后,可以被释放的所有的对象集合(包括对象A本身),即对象A的保留集可以被认为是 只能通过 对象A被直接或间接访问到的所有对象的集合。通俗地说,就是指仅被对象A所持有的对象的集合。
这些对象无法被其他对象所访问到,所以一旦A对象需要被回收,那么其所持有的的保留集中的对象都需要被回收。
深堆(Retained Heap)
深堆是指对象的保留集中所有的对象的浅堆大小之和。
注意:浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。
对象的实际大小
这里,对象的实际大小定义为一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小。与深堆相比,似乎这个在日常开发中更为直观和被人接受,但实际上,这个概念和垃圾回收无关。
下图显示了一个简单的对象引用关系图,对象A引用了C和D,对象B引用了C和E。那么对象A的浅堆大小只是A本身,不含C和D,而A的实际大小为A、C、D三者之和。而A的深堆大小为A与D之和,由于对象C还可以通过对象B访问到,因此不在对象A的深堆范围内。
支配树(Dominator Tree)
支配树的概念源自图论。MAT提供了一个称为支配树(Dominator Tree)的对象图。支配树体现了对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。支配树是基于对象间的引用图所建立的,它有以下基本性质:
- 对象A的子树(所有被对象A支配的对象集合)表示对象A的保留集(retained set),即深堆。
- 如果对象A支配对象B,那么对象A的直接支配者也支配对象B。
- 支配树的边与对象引用图的边不直接对应。
如下图所示:左图表示对象引用图,右图表示左图所对应的支配树。对象A和B由根对象直接支配,由于在到对象C的路径中,可以经过A,也可以经过B,因此对象C的直接支配者也是根对象。对象F与对象D相互引用,因为到对象F的所有路径必然经过对象D,因此,对象D是对象F的直接支配者。而到对象D的所有路径中,必然经过对象C,即使是从对象F到对象D的引用,从根节点出发,也是经过对象C的,所以,对象D的直接支配者为对象C。同理,对象E支配对象G。到达对象H的可以通过对象D,也可以通过对象E,因此对象D和E都不能支配对象H,而经过对象C既可以到达D也可以到达E,因此对象C为对象H的直接支配者。
命令行工具
概述
性能诊断是软件工程师在日常工作中需要经常面对和解决的问题,在用户体验至上的今天,解决好应用的性能问题能带来非常大的收益。
Java 作为最流行的编程语言之一,其应用性能诊断一直受到业界广泛关注。可能造成 Java 应用出现性能问题的因素非常多,例如线程控制、磁盘读写、数据库访问、网络I/O、垃圾收集等。想要定位这些问题,一款优秀的性能诊断工具必不可少。
在JDK的bin目录下,有很多官方提供的监控与诊断工具:
jps:查看正在运行的 Java 进程
jps(Java Process Status):显示指定系统内所有的HotSpot虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。
格式:
1 | jps [options] [hostid] |
常用参数:
jps -q
:只显示Java进程号jps -l
:显示主类全类名jps -m
:显示主类main()
的参数jps -v
:显示进程启动的JVM参数。比如:-Xms20m
-Xmx50m
是启动程序指定的JVM参数。
上述参数可以组合使用。
如果某 Java 进程关闭了默认开启的UsePerfData参数(即使用参数-XX:-UsePerfData),那么jps命令(以及下面介绍的jstat)将无法探知该Java 进程。
jstat:查看 JVM 统计信息
jstat(JVM Statistics Monitoring Tool):用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。
jstat是命令行中运行期间定位虚拟机性能问题首选工具,常用于检查垃圾回收以及内存泄漏问题。
格式:
1 | jstat -<option> [-t] [-h] <vmid> [interval] [count] |
常用参数:
-option
:要查看什么统计信息-t
:输出程序运行到现在好费时间-h
:输出多少行后输出一次表头信息vmid
:要查看的进程号interval
:间隔多少毫秒输出一次统计信息count
:输出多少次终止
常用命令:
jstat -class:显示ClassLoader的相关信息:类的装载、卸载数量、总空间、类装载所消耗的时间等
jstat -compiler:显示JIT编译器编译过的方法、耗时等信息
jstat -printcompilation:输出已经被JIT编译的方法
jstat -gc:显示与GC相关的堆信息。包括Eden区、两个Survivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息。
jstat -gccapacity:显示内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间。
jstat -gcutil:显示内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比。
jstat -gccause:与-gcutil功能一样,但是会额外输出导致最后一次或当前正在发生的GC产生的原因。
jstat -gcnew:显示新生代GC状况
jstat -t
jstat -t -h
表头 | 含义(字节) |
---|---|
EC | Eden区的大小 |
EU | Eden区已使用的大小 |
S0C | 幸存者0区的大小 |
S1C | 幸存者1区的大小 |
S0U | 幸存者0区已使用的大小 |
S1U | 幸存者1区已使用的大小 |
MC | 元空间的大小 |
MU | 元空间已使用的大小 |
OC | 老年代的大小 |
OU | 老年代已使用的大小 |
CCSC | 压缩类空间的大小 |
CCSU | 压缩类空间已使用的大小 |
YGC | 从应用程序启动到采样时young gc的次数 |
YGCT | 从应用程序启动到采样时young gc消耗时间(秒) |
FGC | 从应用程序启动到采样时full gc的次数 |
FGCT | 从应用程序启动到采样时的full gc的消耗时间(秒) |
GCT | 从应用程序启动到采样时gc的总时间 |
经验:
- 当GC时间占总时间比率很大时,说明频繁GC,越大越可能OOM
- 计算GC占比公式 = 2次GC耗时时间相减 / 这2次程序持续时间相减
- 当老年代占用内存不断上涨,可能出现内存泄漏
补充: jstat还可以用来判断是否出现内存泄漏。
- 第1步:在长时间运行的 Java 程序中,我们可以运行jstat命令连续获取多行性能数据,并取这几行数据中 OU 列(即已占用的老年代内存)的最小值
- 第2步:然后,我们每隔一段较长的时间重复一次上述操作,来获得多组 OU 最小值。如果这些值呈上涨趋势,则说明该 Java 程序的老年代内存已使用量在不断上涨,这意味着无法回收的对象在不断增加,因此很有可能存在内存泄漏。
jinfo:实时查看和修改 JVM 配置参数
jinfo(Configuration Info for Java):查看虚拟机配置参数信息,也可用于实时修改虚拟机的配置参数。在很多情况卡,Java应用程序不会指定所有的Java虚拟机参数。而此时,开发人员可能不知道某一个具体的Java虚拟机参数的默认值。在这种情况下,可能需要通过查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了jinfo工具,开发人员可以很方便地找到Java虚拟机参数的当前值。
jinfo 可以在程序运行时动态修改某些参数(只有少数参数支持运行时修改)。更多的应用是查看JVM的一些参数
格式:
1 | jinfo [options] pid |
参数列表:
选项 | 选项说明 |
---|---|
no option | 输出全部的参数和系统属性 |
-flag name | 输出对应名称的参数 |
-flag [±]name | 开启或者关闭对应名称的参数 只有被标记为manageable的参数才可以被动态修改 |
-flag name=value | 设定对应名称的参数 |
-flags | 输出全部的参数 |
-sysprops | 输出系统属性 |
常用参数:
jinfo -sysprops: 输出系统属性
1 | jinfo -sysprops |
jinfo -flags:输出全部的参数
1 | jinfo -flags 25592 |
jinfo -flag name:输出对应名称的参数
1 | jinfo -flag UseParallelGC 25592 |
jinfo -flag [±]name:开启或者关闭对应名称的参数,只有被标记为manageable的参数才可以被动态修改
1 | jinfo -flag +PrintGCDetails 25592 |
拓展:
java -XX:+PrintFlagsInitial
查看所有JVM参数启动的初始值
1 | [Global flags] |
java -XX:+PrintFlagsFinal
查看所有JVM参数的最终值
1 | [Global flags] |
java -XX:+PrintCommandLineFlags
查看哪些已经被用户或者JVM设置过的详细的XX参数的名称和值
1 | -XX:InitialHeapSize=332790016 -XX:MaxHeapSize=5324640256 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC |
jmap:导出内存映像文件和内存使用情况
jmap(JVM Memory Map):作用一方面是获取dump文件(堆转储快照文件,二进制文件),它还可以获取目标Java进程的内存相关信息,包括Java堆各区域的使用情况、堆中对象的统计信息、类加载信息等。开发人员可以在控制台中输入命令“jmap -help”查阅jmap工具的具体使用方式和一些标准选项配置。
jmap用于获取dump文件以及目标Java进程内存相关信息(堆使用情况,对象统计信息,类加载信息)
基本使用语法为:
jmap [option] <pid>
jmap [option] <executable <core>
jmap [option] [server_id@] <remote server IP or hostname>
常用选项:
选项 | 作用 |
---|---|
-dump | 生成dump文件(Java堆转储快照),-dump:live 只保存堆中的存活对象 |
-heap | 输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等 |
-histo | 输出堆空间中对象的统计信息,包括类、实例数量和合计容量,-histo:live 只统计堆中的存活对象 |
-J <flag> | 传递参数给jmap启动的jvm |
-finalizerinfo | 显示在F-Queue中等待Finalizer线程执行finalize方法的对象,仅linux/solaris平台有效 |
-permstat | 以ClassLoader为统计口径输出永久代的内存状态信息,仅linux/solaris平台有效 |
-F | 当虚拟机进程对-dump选项没有任何响应时,强制执行生成dump文件,仅linux/solaris平台有效 |
该命令常用于导出dump文件:
- 手动导出dump文件:
1 | jmap -dump:format=b,file=<filename.hprof> <pid> |
- 自动导出:启动程序时需要带参数,发生OOM时自动导出
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=导出目录\文件名.hprof
说明:这些参数和Linux下输入显示的命令多少会有不同,包括也受jdk版本的影响。
由于jmap将访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap需要借助安全点机制,让所有线程停留在不改变堆中数据的状态。也就是说,由jmap导出的堆快照必定是安全点位置的。这可能导致基于该堆快照的分析结果存在偏差。
举个例子,假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么:live选项将无法探知到这些对象。
另外,如果某个线程长时间无法跑到安全点,jmap将一直等下去。与前面讲的jstat则不同,垃圾回收器会主动将jstat所需要的摘要数据保存至固定位置之中,而jstat只需直接读取即可。
jhat:JDK自带堆分析工具
jhat(JVM Heap Analysis Tool):Sun JDK提供的jhat命令与jmap命令搭配使用,用于分析jmap生成的heap dump文件(堆转储快照)。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,用户可以在浏览器中查看分析结果(分析虚拟机转储快照信息)。
使用了jhat命令,就启动了一个http服务,端口是7000,即http://localhost:7000/,就可以在浏览器里分析。使用的时候会占用CPU所以不会在生成环境中使用jhat来分析
说明:jhat命令在JDK9、JDK10中已经被删除,官方建议用VisualVM代替。
格式:
1 | jhat <option> <dumpfile> |
常用参数:
option参数 | 作用 |
---|---|
-stack false|true | 关闭|打开对象分配调用栈跟踪 |
-refs false|true | 关闭|打开对象引用跟踪 |
-port port-number | 设置jhat HTTP Server的端口号,默认7000 |
-exclude exclude-file | 执行对象查询时需要排除的数据成员 |
-baseline exclude-file | 指定一个基准堆转储 |
-debug int | 设置debug级别 |
-version | 启动后显示版本信息就退出 |
-J <flag> | 传入启动参数,比如-J-Xmx512m |
jstack:打印 JVM 中线程快照
jstack(JVM Stack Trace):用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合。
生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack显示各个线程调用的堆栈情况。
在thread dump中,要留意下面几种状态
- 死锁,
Deadlock
(重点关注) - 等待资源,
Waiting on condition
(重点关注) - 等待获取监视器,
Waiting on monitor entry
(重点关注) - 阻塞,
Blocked
(重点关注) - 执行中,
Runnable
- 暂停,
Suspended
- 对象等待中,
Object.wait()
或TIMED_WAITING
- 停止,
Parked
格式:
1 | jstack option pid |
jcmd:多功能命令行
jcmd可以实现除了jstat外所有指令的功能
在JDK 1.7以后,新增了一个命令行工具jcmd。它是一个多功能的工具,可以用来实现前面除了jstat之外所有命令的功能。比如:用它来导出堆、内存使用、查看Java进程、导出线程信息、执行GC、JVM运行时间等。jcmd拥有jmap的大部分功能,并且在Oracle的官方网站上也推荐使用jcmd命令代jmap命令。
常用命令:
jcmd -l
:列出所有的JVM进程jcmd 进程号 help
:针对指定的进程,列出支持的所有具体命令jcmd 进程号 具体命令
:显示指定进程的指令命令的数据Thread.print
可以替换 jstack指令GC.class_histogram
可以替换 jmap中的-histo
操作GC.heap_dump
可以替换 jmap中的-dump
操作GC.run
可以查看GC的执行情况VM.uptime
可以查看程序的总执行时间,可以替换jstat指令中的-t
操作VM.system_properties
可以替换jinfo -sysprops 进程id
VM.flags
可以获取JVM的配置参数信息
jstatd:远程主机信息收集
之前的指令只涉及到监控本机的Java应用程序,而在这些工具中,一些监控工具也支持对远程计算机的监控(如jps、jstat)。为了启用远程监控,则需要配合使用jstatd 工具。命令jstatd是一个RMI服务端程序,它的作用相当于代理服务器,建立本地计算机与远程监控工具的通信。jstatd服务器将本机的Java应用程序信息传递到远程计算机。
GUI 工具
使用上一章命令行工具或组合能帮我们获取目标Java应用性能相关的基础信息,但它们存在下列局限:
- 无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)。
- 要求用户登录到目标 Java 应用所在的宿主机上,使用起来不是很方便。
- 分析数据通过终端输出,结果展示不够直观。
为此,JDK提供了一些内存泄漏的分析工具,如jconsole,jisualvm等,用于辅助开发人员定位问题,但是这些工具很多时候并不足以满足快速定位的需求。所以这里我们介绍的工具相对多一些、丰富一些。
JDK 自带的工具
- jconsole:JDK自带的可视化监控工具。查看Java应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等
- Visual VM:Visual VM是一个工具,它提供了一个可视界面,用于查看Java虚拟机上运行的基于Java技术的应用程序的详细信息。
- JMC:Java Mission Control,内置Java Flight Recorder。能够以极低的性能开销收集Java虚拟机的性能数据。
第三方工具
- MAT:MAT(Memory Analyzer Tool)是基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
- JProfiler:商业软件,需要付费。功能强大。
JConsole
jconsole 可用于观察堆内存情况、检查线程是否发生死锁
jconsole:从Java 5开始,在JDK中自带的java监控和管理控制台。用于对JVM中内存、线程和类等的监控,是一个基于JMX(java management extensions)的GUI性能监控工具。
官方地址:https://docs.oracle.com/javase/7/docs/technotes/guides/management/jconsole.html
启动
连接
无法连接解决方案,启动JVM参数加上
1 | -Dcom.sun.management.jmxremote |
查看信息
概述信息
内存信息
线程信息 可检查死锁
关于类信息
有关VM以及参数等信息
Visual VM
Visual VM 集合了上一章讲解的多个命令行工具的功能,并提供了可视化界面,能够替代 JConsole
Visual VM是一个功能强大的多合一故障诊断和性能监控的可视化工具。它集成了多个JDK命令行工具,使用Visual VM可用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)等,甚至代替JConsole。在JDK 6 Update 7以后,Visual VM便作为JDK的一部分发布(VisualVM 在JDK/bin目录下)即:它完全免费。
IDEA 可以继承 Visual VM 插件。另外,Visual VM 也需要安装一个插件:Visual GC,用于观察GC情况。
主要功能:
- 生成/读取堆内存/线程快照
- 查看JVM参数和系统属性
- 查看运行中的虚拟机进程
- 程序资源的实时监控
- JMX代理连接、远程环境监控、CPU分析和内存分析
官方地址:https://visualvm.github.io/index.html
查看堆dump文件
查看线程dump文件
查看CPU,内存抽样
Eclipse MAT
MAT(Memory Analyzer Tool)工具是一款功能强大的Java堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。MAT是基于Eclipse开发的,不仅可以单独使用,还可以作为插件的形式嵌入在Eclipse中使用。是一款免费的性能分析工具,使用起来非常方便。
MAT可以分析heap dump文件。在进行内存分析时,只要获得了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观地看到当前的内存信息。一般说来,这些内存信息包含:
- 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
- 所有的类信息,包括classloader、类名称、父类、静态变量等
- GCRoot到所有的这些对象的引用路径
- 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)
MAT 不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如Sun,HP,SAP 所采用的 HPROF 二进制堆存储文件,以及 IBM的 PHD 堆存储文件等都能被很好的解析。
最吸引人的还是能够快速为开发人员生成内存泄漏报表,方便定位问题和分析问题。虽然MAT有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从MAT展现给我们的信息当中通过经验和直觉来判断才能发现。
官方地址: https://www.eclipse.org/mat/downloads.php
JProfiler
在运行Java的时候有时候想测试运行时占用内存情况,这时候就需要使用测试工具查看了。在eclipse里面有 Eclipse Memory Analyzer tool(MAT)插件可以测试,而在IDEA中也有这么一个插件,就是JProfiler。JProfiler 是由 ej-technologies 公司开发的一款 Java 应用性能诊断工具。功能强大,但是收费。
特点:
- 使用方便、界面操作友好(简单且强大)
- 对被分析的应用影响小(提供模板)
- CPU,Thread,Memory分析功能尤其强大
- 支持对jdbc,noSql,jsp,servlet,socket等进行分析
- 支持多种模式(离线,在线)的分析
- 支持监控本地、远程的JVM
- 跨平台,拥有多种操作系统的安装版本
主要功能:
- 方法调用:对方法调用的分析可以帮助您了解应用程序正在做什么,并找到提高其性能的方法
- 内存分配:通过分析堆上对象、引用链和垃圾收集能帮您修复内存泄露问题,优化内存使用
- 线程和锁:JProfiler提供多种针对线程和锁的分析视图助您发现多线程问题
- 高级子系统:许多性能问题都发生在更高的语义级别上。例如,对于JDBC调用,您可能希望找出执行最慢的SQL语句。JProfiler支持对这些子系统进行集成分析
官网地址:https://www.ej-technologies.com/products/jprofiler/overview.html
数据采集方式:
JProfier数据采集方式分为两种:Sampling(样本采集)和Instrumentation(重构模式)
- Instrumentation:这是JProfiler全功能模式。在class加载之前,JProfier把相关功能代码写入到需要分析的class的bytecode中,对正在运行的jvm有一定影响。
- 优点:功能强大。在此设置中,调用堆栈信息是准确的。
- 缺点:若要分析的class较多,则对应用的性能影响较大,CPU开销可能很高(取决于Filter的控制)。因此使用此模式一般配合Filter使用,只对特定的类或包进行分
- Sampling:类似于样本统计,每隔一定时间(5ms)将每个线程栈中方法栈中的信息统计出来。
- 优点:对CPU的开销非常低,对应用影响小(即使你不配置任何Filter)
- 缺点:一些数据/特性不能提供(例如:方法的调用次数、执行时间)
注:JProfiler本身没有指出数据的采集类型,这里的采集类型是针对方法调用的采集类型。因为JProfiler的绝大多数核心功能都依赖方法调用采集的数据,所以可以直接认为是JProfiler的数据采集类型。
摇杆检测 Telemetries
内存视图分析 Live memory
Live memory 内存剖析:class/class instance的相关信息。例如对象的个数,大小,对象创建的方法执行栈,对象创建的热点。
- 所有对象 All Objects:显示所有加载的类的列表和在堆上分配的实例数。只有Java 1.5(JVMTI)才会显示此视图。
- 记录对象 Record Objects:查看特定时间段对象的分配,并记录分配的调用堆栈。
- 分配访问树 Allocation Call Tree:显示一棵请求树或者方法、类、包或对已选择类有带注释的分配信息的J2EE组件。
- 分配热点 Allocation Hot Spots:显示一个列表,包括方法、类、包或分配已选类的J2EE组件。你可以标注当前值并且显示差异值。对于每个热点都可以显示它的跟踪记录树。
- 类追踪器 Class Tracker:类跟踪视图可以包含任意数量的图表,显示选定的类和包的实例与时间。
可以通过对比分析,如果增加很多对象可能有几种情况:
- 频繁创建对象,死循环或循环次数多
- 存在大对象(读取文件byte[]不要太大,边写边读,长时间不写出会导致byte[]过大)
- 每次GC后,内存依次递增可能存在内存泄漏
- All Objects 所有对象:Size显示的是该实例对象的浅堆(不包含它引用字段的实际大小)
- Recorded Objects 记录对象:可以动态看到类的对象变化情况 (默认不开启,开启后影响性能)
- Allocation Call Tree 分配访问树:将执行方法所占时间显示成树 (默认不开启,开启后影响性能)
- Allocation Hots Spots 分配热点:显示什么方法时间占比大
- Class Tracker 类追踪器
堆遍历 Heap Walker
通过对比发现对象增长过快,可以查看该对象的引用链
CPU 视图 CPU views
JProfiler 提供不同的方法来记录访问树以优化性能和细节。线程或者线程组以及线程状况可以被所有的视图选择。所有的视图都可以聚集到方法、类、包或J2EE组件等不同层上。
- 访问树 Call Tree:显示一个积累的自顶向下的树,树中包含所有在JVM中已记录的访问队列。JDBC,JMS和JNDI服务请求都被注释在请求树中。请求树可以根据Servlet和JSP对URL的不同需要进行拆分。
- 热点 Hot Spots:显示消耗时间最多的方法的列表。对每个热点都能够显示回溯树。该热点可以按照方法请求,JDBC,JMS和JNDI服务请求以及按照URL请求来进行计算。
- 访问图 Call Graph:显示一个从已选方法、类、包或J2EE组件开始的访问队列的图。
- 方法统计 Method Statistis:显示一段时间内记录的方法的调用时间细节。
关于这种分析都可以从范围大的到范围小的 package -> class -> method
线程视图 Threads
JProfiler通过对线程历史的监控判断其运行状态,并监控是否有线程阻塞产生,还能将一个线程所管理的方法以树状形式呈现。对线程剖析。
- 线程历史 Thread History:显示一个与线程活动和线程状态在一起的活动时间表。
- 线程监控 Thread Monitor:显示一个列表,包括所有的活动线程以及它们目前的活动状况。
- 线程转储 Thread Dumps:显示所有线程的堆栈跟踪。
线程分析主要关心三个方面:
- web容器的线程最大数。比如:Tomcat的线程容量应该略大于最大并发数。
- 线程阻塞
- 线程死锁
查看线程运行状态,可以知道线程执行情况,比如main线程大部分时间在等待,少部分时间在运行
监视器和锁 Monitors & Locks
所有线程持有锁的情况以及锁的信息。观察JVM的内部线程并查看状态:
- 死锁探测图表 Current Locking Graph:显示JVM中的当前死锁图表。
- 目前使用的监测器 Current Monitors:显示目前使用的监测器并且包括它们的关联线程。
- 锁定历史图表 Locking History Graph:显示记录在JVM中的锁定历史。
- 历史检测记录 Monitor History:显示重大的等待事件和阻塞事件的历史记录。
- 监控器使用统计 Monitor Usage Statistics:显示分组监测,线程和监测类的统计监测数据
Arthas
上述工具都必须在服务端项目进程中配置相关的监控参数,然后工具通过远程连接到项目进程,获取相关的数据。这样就会带来一些不便,比如线上环境的网络是隔离的,本地的监控工具根本连不上线上环境。并且类似于Jprofiler这样的商业工具,是需要付费的。那么有没有一款工具不需要远程连接,也不需要配置监控参数,同时也提供了丰富的性能监控数据呢?
阿里巴巴开源的性能分析神器Arthas应运而生。
Arthas是Alibaba开源的Java诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪Java代码;实时监控JVM状态。Arthas 支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:
- 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
- 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
- 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
- 是否有一个全局视角来查看系统的运行状况?
- 有什么办法可以监控到JVM的实时运行状态?
- 怎么快速定位应用的热点,生成火焰图?
下载与使用
下载方式:
1 | curl -O https://arthas.aliyun.com/arthas-boot.jar |
安装方式:
1 | wget https://io/arthas/arthas-boot.jar |
Arthas只是一个java程序,所以可以直接用java -jar运行。
1 | # 启动 |
除了在命令行查看外,Arthas目前还支持 Web Console。在成功启动连接进程之后就已经自动启动,可以直接访问 http://127.0.0.1:8563/ 访问,页面上的操作模式和控制台完全一样。
基础命令
- help——查看命令帮助信息
- cat——打印文件内容,和linux里的cat命令类似
- echo–打印参数,和linux里的echo命令类似
- grep——匹配查找,和linux里的grep命令类似
- base64——base64编码转换,和linux里的base64命令类似
- tee——复制标准输入到标准输出和指定的文件,和linux里的tee命令类似
- pwd——返回当前的工作目录,和linux命令类似
- cls——清空当前屏幕区域
- session——查看当前会话的信息
- reset——重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类
- version——输出当前目标 Java 进程所加载的 Arthas 版本号
- history——打印命令历史
- quit——退出当前 Arthas 客户端,其他 Arthas 客户端不受影响
- stop——关闭 Arthas 服务端,所有 Arthas 客户端全部退出
- keymap——Arthas快捷键列表及自定义快捷键
JVM 相关
- dashboard——当前系统的实时数据面板
- thread——查看当前 JVM 的线程堆栈信息
- jvm——查看当前 JVM 的信息
- sysprop——查看和修改JVM的系统属性
- sysenv——查看JVM的环境变量
- vmoption——查看和修改JVM里诊断相关的option
- perfcounter——查看当前 JVM 的Perf Counter信息
- logger——查看和修改logger
- getstatic——查看类的静态属性
- ognl——执行ognl表达式
- mbean——查看 Mbean 的信息
- heapdump——dump java heap, 类似jmap命令的heap dump功能
- vmtool——从jvm里查询对象,执行forceGc
class/classloader 相关
- sc——查看JVM已加载的类信息
- sm——查看已加载类的方法信息
- jad——反编译指定已加载类的源码
- mc——内存编译器,内存编译
.java
文件为.class
文件 - retransform——加载外部的
.class
文件,retransform到JVM里 - redefine——加载外部的
.class
文件,redefine到JVM里 - dump——dump 已加载类的 byte code 到特定目录
- classloader——查看classloader的继承树,urls,类加载信息,使用classloader去getResource
monitor/watch/trace 相关
请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行
stop
或将增强过的类执行reset
命令。
- monitor——方法执行监控
- watch——方法执行数据观测
- trace——方法内部调用路径,并输出方法路径上的每个节点上耗时
- stack——输出当前方法被调用的调用路径
- tt——方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
Java Misssion Control
在Oracle收购Sun之前,Oracle的JRockit虚拟机提供了一款叫做 JRockit Mission Control 的虚拟机诊断工具。在Oracle收购sun之后,Oracle公司同时拥有了Hotspot和 JRockit 两款虚拟机。根据Oracle对于Java的战略,在今后的发展中,会将JRokit的优秀特性移植到Hotspot上。其中一个重要的改进就是在Sun的JDK中加入了JRockit的支持。
在Oracle JDK 7.0之后,Mission Control这款工具已经绑定在Oracle JDK中发布。
自Java11开始,本节介绍的JFR已经开源。但在之前的Java版本,JFR属于Commercial Feature通过Java虚拟机参数-XX:+UnlockCommercialFeatures
开启。
Java Mission Control(简称JMC) , Java官方提供的性能强劲的工具,是一个用于对 Java应用程序进行管理、监视、概要分析和故障排除的工具套件。它包含一个GUI客户端以及众多用来收集Java虚拟机性能数据的插件如 JMX Console(能够访问用来存放虚拟机齐个于系统运行数据的MXBeans)以及虚拟机内置的高效 profiling 工具 Java Flight Recorder(JFR)。
JMC的另一个优点就是:采用取样,而不是传统的代码植入技术,对应用性能的影响非常非常小,完全可以开着JMC来做压测(唯一影响可能是 full gc 多了)。
官方地址:https://github.com/JDKMissionControl/jmc
Java Flight Recorder
Java Flight Recorder是JMC的其中一个组件,能够以极低的性能开销收集Java虚拟机的性能数据。与其他工具相比,JFR的性能开销很小,在默认配置下平均低于1%。JFR能够直接访问虚拟机内的敌据并且不会影响虚拟机的优化。因此它非常适用于生产环境下满负荷运行的Java程序。
Java Flight Recorder 和 JDK Mission Control共同创建了一个完整的工具链。JDK Mission Control 可对 Java Flight Recorder 连续收集低水平和详细的运行时信息进行高效、详细的分析。
当启用时 JFR将记录运行过程中发生的一系列事件。其中包括Java层面的事件如线程事件、锁事件,以及Java虚拟机内部的事件,如新建对象,垃圾回收和即时编译事件。按照发生时机以及持续时间来划分,JFR的事件共有四种类型,它们分别为以下四种:
- 瞬时事件(Instant Event) ,用户关心的是它们发生与否,例如异常、线程启动事件。
- 持续事件(Duration Event) ,用户关心的是它们的持续时间,例如垃圾回收事件。
- 计时事件(Timed Event) ,是时长超出指定阈值的持续事件。
- 取样事件(Sample Event),是周期性取样的事件。
取样事件的其中一个常见例子便是方法抽样(Method Sampling),即每隔一段时问统计各个线程的栈轨迹。如果在这些抽样取得的栈轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法。
Flame Graphs(火焰图)
在追求极致性能的场景下,了解你的程序运行过程中cpu在干什么很重要,火焰图就是一种非常直观的展示CPU在程序整个生命周期过程中时间分配的工具。火焰图对于现代的程序员不应该陌生,这个工具可以非常直观的显示出调用找中的CPU消耗瓶颈。
网上的关于Java火焰图的讲解大部分来自于Brenden Gregg的博客 http://new.brendangregg.com/flamegraphs.html
火焰图,简单通过x轴横条宽度来度量时间指标,y轴代表线程栈的层次。
Tprofiler
案例: 使用JDK自身提供的工具进行JVM调优可以将下 TPS 由2.5提升到20(提升了7倍),并准确 定位系统瓶颈。
系统瓶颈有:应用里静态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题。
那么,如何在海量业务代码里边准确定位这些性能代码?这里使用阿里开源工具 Tprofiler 来定位 这些性能代码,成功解决掉了GC 过于频繁的性能瓶预,并最终在上次优化的基础上将 TPS 再提升了4倍,即提升到100。
- Tprofiler配置部署、远程操作、 日志阅谈都不太复杂,操作还是很简单的。但是其却是能够起到一针见血、立竿见影的效果,帮我们解决了GC过于频繁的性能瓶预。
- Tprofiler最重要的特性就是能够统汁出你指定时间段内 JVM 的 top method。这些 top method 极有可能就是造成你 JVM 性能瓶颈的元凶。这是其他大多数 JVM 调优工具所不具备的,包括 JRockit Mission Control。JRokit 首席开发者 Marcus Hirt 在其私人博客《 Lom Overhead Method Profiling cith Java Mission Control》下的评论中曾明确指出 JRMC 井不支持 TOP 方法的统计。
官方地址:http://github.com/alibaba/Tprofiler
Btrace
常见的动态追踪工具有BTrace、HouseHD(该项目己经停止开发)、Greys-Anatomy(国人开发 个人开发者)、Byteman(JBoss出品),注意Java运行时追踪工具井不限干这几种,但是这几个是相对比较常用的。
BTrace是SUN Kenai 云计算开发平台下的一个开源项目,旨在为java提供安全可靠的动态跟踪分析工具。先看一卜日Trace的官方定义:
一个 Java 平台的安全的动态追踪工具,可以用来动态地追踪一个运行的 Java 程序。BTrace动态调整目标应用程序的类以注入跟踪代码(“字节码跟踪“)。
其他小众监控工具:
- YourKit
- JProbe
- Spring Insight
JVM 运行时参数
官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html
JVM 的运行时参数可以分为三类:
- 标准参数选项
- -X参数选项
- -XX:参数选项
添加 JVM 参数选项
运行 jar 包
1 | java -Xms100m -Xmx100m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -jar demo.jar |
Tomcat 运行 war 包
1 | # linux下catalina.sh添加 |
程序运行中
1 | # 设置Boolean类型参数 |
类型一:标准参数选项
特点:
- 以
-
开头 - 稳定,后续版本基本不变
- 不常用
1 | java -help |
Server 模式和 Client 模式
Hotspot JVM有两种模式,分别是server和client,分别通过-server
和-client
参数设置
- 32位系统上,默认使用client类型的JVM。要想使用Server模式,机器配置至少有2个以上的CPU和2G以上的物理内存。client模式适用于对内存要求较小的桌面应用程序,默认使用Serial串行垃圾收集器
- 64位系统上,只支持server模式的JVM,适用于需要大内存的应用程序,默认使用并行垃圾收集器
官网地址:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/server-class.html
如何知道系统默认使用的是哪种模式呢:通过java -version
命令:可以看到Server VM字样,代表当前系统使用是Server模式
1 | java -version |
类型二:-X参数选项
特点:
- 以
-X
开头 - 较稳定,后续版本可能改变
1 | java -X |
其中,重点需要记忆的参数有:
1、和解释器相关的参数:
-Xint
:只使用解释器-Xcomp
:只使用编译器-Xmixed
:混用(默认选项),前期解释器解释字节码执行,等程序运行起来后后端编译器缓存热点代码,加速执行效率
2、和堆内存相关的参数:
-Xss
:设置线程(栈)内存大小,等效于-XX:ThreadStackSize
(记忆:Stack Size)-Xms
:设置堆内存初始大小,等效于-XX:InitalHeapSize
(记忆:Memory Size)-Xmx
:设置堆内存最大大小,等效于-XX:MaxHeapSize
(记忆:Memory maX)-Xmn
:设置年轻代内存,等同于-XX:NewSize
+-XX:MaxNewSize
(记忆:Memory New)
3、日志相关:
-Xloggc:<file>
:将 GC 状态记录在文件中 (带时间戳);可以使用 GCeasy 网站分析 GC 记录,GC 日志分析
-Xms
和 -Xmx
两个参数设置相同的值,目的是为了能够在每次GC后不需要再重新计算堆区要分配的大小,从而提高性能。
如果二者设置不相同,则每次GC后,会根据当前GC的效果动态调整堆区的大小:回收效果好 -> 减小堆区大小;回收效果差 -> 增大堆区大小
类型三:-XX:参数选项
特点:
- 以
-XX:
开头 - 不稳定,后续版本会改动
- 常用
Boolean 类型格式
1 | -XX:+<option> # 启用option属性,例如 -XX:+UseParallelGC:使用parallel垃圾收集器 |
非 Boolean 类型格式
1 | -XX:<option>=<number> # 设置option数值,可以带单位如k/K/m/M/g/G,例如 -XX:SurvivorRatio=8 设置新生代中Eden区与survivor区占比为: 8:1:1 |
其中,重点需要记忆的参数有:
-XX:+PrintFlagsFinal
:打印所有-XX选项的实际值-XX:NewRatio=2
:设置老年代与年轻代的比例,默认为2-XX:SurvivorRatio=8
:设置Eden区与Survivor区的比值,默认为8(虽然默认是8,但是因为默认开启了-XX:+UseAdaptiveSizePolicy
自动调整策略,导致实际的新生代内比例可能不是8)-XX:+UseAdaptiveSizePolicy
:设置堆空间大小比例自适应,默认开启
打印设置的-XX选项及值
1 | -XX:+PrintCommandLineFlags # 程序运行时JVM默认设置或用户手动设置的XX选项 |
-XX:+PrintCommandLineFlags
:打印命令行启动时添加的-XX选项或JVM自动设置的-XX选项
-XX:+PrintFlagsInitinal
:打印出所有-XX选项默认值
-XX:+PrintFlagsFinal
:打印出-XX选项最终在程序运行时的值(使用 jinfo -flag 修改参数 pid
进行修改)
-
``-XX:+PrintVMOption` 打印JVM参数
堆、栈、方法区等内存大小设置
1 | # 栈 |
OutOfMemory 相关的选项
1 | -XX:+HeapDumpOnOutMemoryError # 内存出现OOM时生成Heap转储文件,两者互斥 |
垃圾收集器相关选项
首先需了解垃圾收集器之间的搭配使用关系:
- 红色虚线表示在jdk8时被Deprecate,jdk9时被删除
- 绿色虚线表示在jdk14时被Deprecate
- 绿色虚框表示在jdk9时被Deprecate,jdk14时被删除
1 | # Serial回收器 |
其中,并行GC线程的线程数的经验公式:
1 | # CMS回收器 |
1 | # G1回收器 |
注意:使用分代收集器G1时最好不要使用-XX:NewRatio
, -Xmn
这种指定年轻代内存大小的参数,而是应该交给G1自动计算年轻代所占的内存大小以满足其低延迟的暂停时间,否则G1里的 -XX:MaxGCPauseMillis
可能无法实现。
怎么选择垃圾回收器?
- 优先让JVM自适应,调整堆的大小
- 串行收集器:内存小于100M;单核、单机程序,并且没有停顿时间的要求
- 并行收集器:多CPU、高吞吐量、允许停顿时间超过1秒
- 并发收集器:多CPU、追求低停顿时间、快速响应(比如延迟不能超过1秒,如互联网应用)
- 官方推荐G1,性能高。现在互联网的项目,基本都是使用G1
GC 日志相关选项
1 | -XX:+PrintGC <==> -verbose:gc # 打印简要日志信息 |
其他参数
1 | -XX:+DisableExplicitGC # 禁用hotspot执行System.gc(),默认禁用 |
通过 Java 代码获取 JVM 参数
Java提供了java.lang.management
包用于监视和管理Java虚拟机和Java运行时中的其他组件,它允许本地或远程监控和管理运行的Java虚拟机。其中ManagementFactory
类较为常用,另外Runtime
类可获取内存、CPU核数等相关的数据。通过使用这些api,可以监控应用服务器的堆内存使用情况,设置一些阈值进行报警等处理。
1 | public class MemoryMonitor { |
GC 日志分析
GC 分类
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
- 部分收集(Partial GC):不是完整收集整个Java堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC / Young GC):只是新生代(Eden / S0, S1)的垃圾收集
- 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有CMS GC会有单独收集老年代的行为。注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为
- 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。
参数解析
通过阅读GC日志,我们可以了解Java虚拟机内存分配与回收策略。
内存分配与垃圾回收的参数列表
-XX:+PrintGC
:输出GC日志。类似:-verbose:gc
-XX:+PrintGCDetails
:输出GC的详细日志-XX:+PrintGCTimestamps
:输出GC的时间戳(以基准时间的形式)-XX:+PrintGCDatestamps
:输出GC的时间戳(以日期的形式,如2013-05-04T21: 53: 59.234 +0800)-XX:+PrintHeapAtGC
:在进行GC的前后打印出堆的信息-Xloggc:…/logs/gc.log
:日志文件的输出路径
verbose:gc
打开GC日志的 JVM 参数:
1 | -verbose:gc |
这个只会显示总的GC堆的变化,如下:
参数解析:
PrintGCDetails
打开GC日志
1 | -XX:+PrintGCDetails |
输入信息如下:
参数解析:
PrintGCTimestamps 和 PrintGCDatestamps
加上这两个参数后,打印的GC日志就会带上日期时间信息。
GC 日志补充说明
- “[GC”和”[Full GC”说明了这次垃圾收集的停顿类型,如果有”Full”则说明GC发生了”Stop The World”
- 使用Serial收集器在新生代的名字是Default New Generation,因此显示的是”[DefNew”
- 使用ParNew收集器在新生代的名字会变成”[ParNew”,意思是”Parallel New Generation”
- 使用Parallel scavenge收集器在新生代的名字是”[PSYoungGen”
- 老年代的收集和新生代道理一样,名字也是收集器决定的
- 使用G1收集器的话,会显示为”garbage-first heap”
- Allocation Failure表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。
- [ PSYoungGen: 5986K->696K(8704K) ] 5986K->704K (9216K)
- 中括号内:GC回收前年轻代大小,回收后大小,(年轻代总大小)
- 括号外:GC回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)
- user代表用户态回收耗时,sys内核态回收耗时,real实际耗时。由于多核线程切换的原因,时间总和可能会超过real时间
Young GC
Full GC
GC 日志结构剖析
透过日志看垃圾收集器
- Serial收集器:新生代显示 “[DefNew”,即 Default New Generation
- ParNew收集器:新生代显示 “[ParNew”,即 Parallel New Generation
- Parallel Scavenge收集器:新生代显示"[PSYoungGen",JDK1.7使用的即PSYoungGen
- Parallel Old收集器:老年代显示"[ParoldGen"
- G1收集器:显示”garbage-first heap“
透过日志看 GC 原因
- Allocation Failure:表明本次引起GC的原因是因为新生代中没有足够的区域存放需要分配的数据
- Metadata GC Threshold:Metaspace区不够用了
- FErgonomics:JVM自适应调整导致的GC
- System:调用了System.gc()方法
透过日志看 GC 前后情况
通过图示,我们可以发现GC日志格式的规律一般都是:GC前内存占用->GC后内存占用(该区域内存总大小)
1 | [PSYoungGen: 5986K->696K (8704K) ] 5986K->704K (9216K) |
- 中括号内:GC回收前年轻代堆大小,回收后大小,(年轻代堆总大小)
- 括号外:GC回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)
注意:Minor GC堆内存总容量 = 9/10 年轻代 + 老年代。原因是Survivor区只计算from部分,而JVM默认年轻代中Eden区和Survivor区的比例关系,Eden:S0:S1=8:1:1。
面试题:为什么GC日志里显示的堆内存总容量比
-Xms
设置的要小?因为GC日志里只显示了 9/10 年轻代 + 老年代,自然比设置的值要小。补充:这里的 9/10 要根据实际情况来定,可能开了自适应调整策略会导致该值变化。
透过日志看 GC 时间
GC日志中有三个时间:user,sys和real
- user:进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际CPU 时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示GC线程执行所使用的 CPU 总时间。
- sys:进程在内核态消耗的 CPU 时间,即在内核执行系统调用或等待系统事件所使用的CPU 时间
- real:程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。对于并行gc,这个数字应该接近(用户时间+系统时间)除以垃圾收集器使用的线程数。
由于多核的原因,一般的GC事件中,real time是小于sys time+user time的,因为一般是多个线程并发的去做GC,所以real time是要小于sys+user time的。如果real>sys+user的话,则你的应用可能存在下列问题:IO负载非常重或CPU不够用。
GC 回收举例
我们编写一个程序,用来说明GC收集的过程
1 | public class GCUseTest { |
我们设置JVM启动参数
1 | -Xms10m -Xmx10m -XX:+PrintGCDetails |
首先我们会将3个2M的数组存放到Eden区,然后后面4M的数组来了后,将无法存储,因为Eden区只剩下2M的剩余空间了,那么将会进行一次Young GC操作,将原来Eden区的内容,存放到Survivor区,但是Survivor区也存放不下,那么就会直接晋级存入Old 区
然后我们将4M对象存入到Eden区中
常用日志分析工具
保存日志文件
JVM参数:-XLoggc:./logs/gc.log
, ./ 表示当前目录,在 IDEA中程序运行的当前目录是工程的根目录,而不是模块的根目录
可以用一些工具去分析这些GC日志 gc.log。常用的日志分析工具有:GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等
推荐:GCeasy
在线分析网址:gceasy.io
GCViewer
补充:OQL
Visual VM 和 MAT 都支持一种类似于SQL的查询语言OQL(Object Query Language)。OQL使用类SQL语法,可以在堆中进行对象的查找和筛选。
SELECT 子句
在MAT中,Select子句的格式与SQL基本一致,用于指定要显示的列。Select子句中可以使用“*”,查看结果对象的引用实例(相当于outgoing references)。
1 | SELECT * FROM java.util.Vector v |
使用“
OBJECTS”关键字,可以将返回结果集中的项以对象的形式显示。
1 | SELECT objects v.elementData FROM java.util.Vector v |
在Select子句中,使用“AS RETAINED SET”关键字可以得到所得对象的保留集。
1 | SELECT AS RETAINED SET *FROM com.zhao.mat.Student |
“DISTINCT”关键字用于在结果集中去除重复对象。
1 | SELECT DISTINCT OBJECTS classof(s) FROM java.lang.String s |
FROM 子句
From子句用于指定查询范围,它可以指定类名、正则表达式或者对象地址。
1 | SELECT * FROM java.lang.String s |
使用正则表达式,限定搜索范围,输出所有com.zhao包下所有类的实例
1 | SELECT * FROM "com\.zhao\..*" |
使用类的地址进行搜索。使用类的地址的好处是可以区分被不同ClassLoader加载的同一种类型。
1 | select * from 0x37a0b4d |
WHERE 子句
Where子句用于指定OQL的查询条件。OQL查询将只返回满足Where子句指定条件的对象。Where子句的格式与传统SQL极为相似。
返回长度大于10的char数组。
1 | SELECT *FROM Ichar[] s WHERE s.@length>10 |
返回包含“java”子字符串的所有字符串,使用“LIKE”操作符,“LIKE”操作符的操作参数为正则表达式。
1 | SELECT * FROM java.lang.String s WHERE toString(s) LIKE ".*java.*" |
返回所有value域不为null的字符串,使用“=”操作符。
1 | SELECT * FROM java.lang.String s where s.value!=null |
返回数组长度大于15,并且深堆大于1000字节的所有Vector对象。
1 | SELECT * FROM java.util.Vector v WHERE v.elementData.@length>15 AND v.@retainedHeapSize>1000 |
Where子句用于指定OQL的查询条件。OQL查询将只返回满足Where子句指定条件的对象。Where子句的格式与传统SQL极为相似。
返回长度大于10的char数组。
1 | SELECT *FROM Ichar[] s WHERE s.@length>10 |
返回包含“java”子字符串的所有字符串,使用“LIKE”操作符,“LIKE”操作符的操作参数为正则表达式。
1 | SELECT * FROM java.lang.String s WHERE toString(s) LIKE ".*java.*" |
返回所有value域不为null的字符串,使用“=”操作符。
1 | SELECT * FROM java.lang.String s where s.value!=null |
返回数组长度大于15,并且深堆大于1000字节的所有Vector对象。
1 | SELECT * FROM java.util.Vector v WHERE v.elementData.@length>15 AND v.@retainedHeapSize>1000 |
相关面试题
支付宝:
- 三面:JVM性能调优都做了什么?
小米:
- 有做过JVM内存优化吗?从SQL、JVM、架构、数据库四个方面讲讲优化思路
蚂蚁金服:
- JVM的编译优化
- JVM性能调优都做了什么
- JVM诊断调优工具用过哪些?
- 二面:jvm怎样调优,堆内存、栈空间设置多少合适
- 三面:JVM相关的分析工具使用过的有哪些?具体的性能调优步骤如何
阿里:
- 如何进行JVM调优?有哪些方法?
- 如何理解内存泄漏问题?有哪些情况会导致内存泄漏?如何解决?
字节跳动:
- 三面:JVM如何调优、参数怎么调?
拼多多:
- 从SQL、JVM、架构、数据库四个方面讲讲优化思路
京东:
- JVM诊断调优工具用过哪些?
- 每秒几十万并发的秒杀系统为什么会频繁发生GC?
- 日均百万级交易系统如何优化JVM?
- 线上生产系统OOM如何监控及定位与解决?
- 高并发系统如何基于G1垃圾回收器优化性能?