关注「Java视界」公众号,获取更多技术干货

【十三】不要问我JVM !—— 性能调优

一、背景说明

1.1、生产环境中的问题

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

1.2、为什么要调优?

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

1.3、不同阶段的考虑

  • 上线前
  • 项目运行阶段
  • 线上出现OOM

二、调优的概述

2.1、监控的依据

  • 运行日志
  • 异常堆栈
  • GC日志
  • 线程快照
  • 堆转储快照

2.2、调优的大方向

  • 合理的编写代码
  • 充分并合理的使用硬件资源
  • 合理的进行JVM调优

三、性能优化的步骤

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

一种以非强行或者入侵方式收集查看应用运行性能数据的活动。监控通常是指一种在生产、质量评估或者开发环境下实施的带有预防或主动性的活动。当应用相关干系人提出性能问题却没有提供足够多的线索时,首先我们需要进行性能监控,随后是性能分析。

  • GC频繁
  • CPU load过高
  • OOM
  • 内存泄露
  • 死锁
  • 程序相应时间较长

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

一种以侵入方式收集运行性能问题的答复结果,它会影响应用的吞吐量或响应性。性能分析是针对性能问题的答复结果,关注的范围通常比性能监控更加集中。性能分析很少在生产环境下进行,通常是在质量评估、系统测试或者开发环境下进行,是性能监控之后的步骤。

  • 打印GC日志,通过GCviewer或者gceasy来分析日志信息
  • 灵活运用命令行工具,jstack、jmap、jinfo等
  • dump出堆文件,使用内存分析工具分析文件
  • 使用阿里Arthas或jconsole,JVisualVM来实时查看JVM状态
  • jstack查看堆栈信息

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

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

性能调优的目的:减少GC的频率,以较少的内存获取更大的吞吐量和更低的延迟

  • 适当增加内存,根据业务背景选择垃圾回收器
  • 优化代码,控制内存使用
  • 增加机器,分散节点压力
  • 合理设置线程池线程数量
  • 使用中间件提高程序效率,比如缓存,消息队列等

四、性能指标/测试指标

停顿时间(或响应时间)
提交请求和返回请求的响应之间使用的时间。

常用操作的响应时间列表:
在这里插入图片描述
在垃圾回收环节中,暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。( XX:MaxGCPauseMills

吞吐量
对单位时间内完成的工作量(请求)的度量
在GC中:运行用户代码的时间占总运行时间的比例总运行时间程序运行时间+内存回收的时间)吞吐量为1-1/(1+n)。(-XX:GCTimeRatio=n)

并发数
同一时刻,对服务器有实际交互的请求数。
1000个人同时在线,估计并发数在5%-15%之间,也就是同时并发量:50-150之间。

内存占用
Java堆区所占的内存大小

相互间的关系: 以高速公路通行状况为例。
吞吐量:每天通过高速公路收费站的数量的数目(也可以理解为收费站收取的高速费)
并发数:高速公路上正在行驶的车辆的数目
响应时间:车速
车少,车速快,收费少 ----> 并发数少,响应时间快,吞吐量小
车适当增多,车速较快,收费较多 ----> 并发数适合,响应时间较快,吞吐量较大
车过多,车速慢,收费少 ----> 并发数过多,响应时间慢,吞吐量小

五、诊断工具 —— 命令行

生产环境是不适合使用GUI交互界面的,一般都使用JVM命令产生纯文本的方式。
在这里插入图片描述

jps

显示指定系统内所有的HotSpot虛拟机进程(查看虛拟机进程信息),用于查询正在运行的虚拟机进程。

说明:
对于本地JVM进程来说,进程的本地虚拟机ID与操作系统的进程ID是一致的,是唯一的。(它是使用频率最高的JDK命令行工具,因为其他的JDK工具大多需要输入他查询到的LVMID来确定要监控的是哪一个虚拟机进程,LVMID对于本地虚拟机而言就是操作系统进程ID)

格式:
jps [ options ] [ hostid ]

options参数

  • -q :仅仅显示LVMID (local virtual machine id),即本地虚拟机唯一id。 不显示主类的名称等
  • -l:输出应用程序主类的全类名或如果进程执行的是jar包,则输出jar完整路径
  • -m:输出虚拟机进程启动时传递给主类main()的参数,参数是值:idea中点击Run有一个edit configuration里面有一个Program argument
  • -v:列出虚拟机进程启动时的JVM参数。 比如: -Xms20m - Xmx50m是启动程序指定的jvm参数。

注意:
如果某Java进程关闭了默认开启的UsePerfData参数( 即使用参数-XX:-UsePerfData),那么jps命令 (以及下面介绍的jstat)将无法探知该Java进程。

hostid参数
RMI注册表中注册的主机名。
如果想要远程监控主机上的java程序,需要安装jstatd

对于具有更严格的安全实践的网络场所而言,可能使用一个自定义的策略文件来显示对特定的可信主机或网络的访问,尽管这种技术容易受到IP地址欺诈攻击。

如果安全问题无法使用一个定制的策略文件来处理,那么最安全的操作是不运行jstatd服务器,而是在本地使用jstat和jps工具。

C:\JavaJDK1.8\Java\jdk1.8.0_281\bin>jps
3088
34752 JpsDemo
40192 Launcher
41316 Jps

C:\JavaJDK1.8\Java\jdk1.8.0_281\bin>jps -m -> 1.txt
1.txt
3088 
42500 Launcher D:/IDEA/IntelliJ IDEA 2017.3.2/lib/jps-builders-6.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/snappy-in-java-0.5.1.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/asm-all.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/aether-dependency-resolver.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/maven-aether-provider-3.3.9-all.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/jdom.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/guava-21.0.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/jps-builders.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/httpclient-4.5.2.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/oromatcher.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/log4j.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/forms_rt.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/httpcore-4.4.5.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/netty-all-4.1.13.Final.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/jna-platform.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/commons-codec-1.9.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/jna.jar;D:/IDEA/IntelliJ IDEA 2017.3.2/lib/jps-model.jar;D:/IDEA/IntelliJ IDEA 2017.3.
33144 JpsDemo hello //idea中的Program argument写了hello
41644 Jps -m -

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

jstat

用于监视JVM各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载内存垃圾收集JIT编译等运行数据。

说明
在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。

格式
jstat [option -t vmid [interval[s|ms] [count] ]

option参数
类装载相关的:

  • -class:显示ClassLoader的相关信息:类的装载、卸载数量、总空间、类装载所消耗的时间等

垃圾回收相关的:

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

JIT相关的

  • -compiler:显示JIT编译器编译过的方法,耗时等信息
  • -printcompilation:输出已经被JIT编译的方法

参数intervalcount代表查询间隔和次数,如果省略这两个参数,说明只查询一次。假设需要每250ms查询一次进程2764垃圾收集情况,一共查询20次,命令如下

jstat -gc 2764 250 20

参数-t表示可以在输出信息前加上一个Timestamp列,显示程序的运行时间,单位:秒

重要:我们可以比较Java进程的启动时间以及总 GC 时间(GCT列),或者两次测量的间隔时间以及总GC时间的增量,来得出 GC时间占运行时间的比例。如果该比例超过20%,则说明目前堆的压力较大;如果该比例超过90%,则说明堆里几乎没有可用空间,随时都可能抛出 OOM异常。如下:
在这里插入图片描述
其他案例:
在这里插入图片描述
在这里插入图片描述
每个3秒出现一个表头
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

jinfo

查看虚拟机配置参数信息,也可用于调整虚拟机的配置参数。

说明
在很多情况下,Java应用程序不会指定所有的Java虚拟机参数。而此时,开发人员可能不知道某一个具体的Java虚拟机参数的默认值。在这种情况下,可能需要通过查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了jinfo工具,开发人员可以很方便地找到Java虚拟机参数的当前值。

格式
jinfo [option] pid

  • jinfo -sysprops PID:可以查看由System.getProperties)取得的参数。
  • jinfo -flags PID:查看曾经赋过值的一些参数
  • jinfo -flag 具体参数 PID:查看某个java进程的具体参数的值
  • 针对boolean类型:jinfo -flag [+I-]具体参数 PID
  • 针对非boolean类型:jinfo -flag 具体参数=具体参数值 PID

jinfo不仅可以查看运行时某一个Java虚拟机参数的实际取值,甚至可以在运行时修改部分参数,并使之立即生效。但是,并非所有参数都支持动态修改。参数只有被标记为manageable的flag可以被实时修改其实,这个修改能力是极其有限的。

可以查看被标记为manageable的参数:

java -XX: +PrintFlagsFinal -version| grep manageable

在这里插入图片描述
实例演示:
boolean类型在这里插入图片描述
非boolean类型
在这里插入图片描述
扩展:
java -XX:+ PrintFlagslnitial 查看所有JVM参数启动的初始值
java -XX:+ PrintFlagsFinal 查看所有JVM参数的最终值
java -XX: + PrintCommandLineFlags 查看那些已经被用户或者JVM设置过的详细的XX参数的名称和值
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个修改是临时的,若重启服务,又会恢复原来的配置

jmap

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

说明
一方面是获取dump文件( 堆转储快照文件,二进制文件),它还可以获取目标Java进程的内存相关信息,包括Java堆各区域的使用情况、堆中对象的统计信息、类加载信息等。

格式
jmap [option] vmid
在这里插入图片描述
重要参数:-dump -heap -histo

jmap的三种使用
使用1:导出内存映像文件

手动方式
jmap -dump:format= b,file= < filename.hprof> <pid>
jmap -dump:live,format= b,file= < filename.hprof> <pid> ẞ
自动的方式
-XX:+ HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath= <filename.hprof>

一般来说, 使用jmap指 令生成dump文件的操作算得上是最常用的jmap命令之一, 将堆中所有存活对象导出至一个文件之中。

Heap Dump 又叫做堆存储文件,指一个Java进程在某个时间点的内存快照。Heap Dump在触发内存快照的时候会保存此刻的信息如下:
在这里插入图片描述
说明:1.通常在写Heap Dump文件前会触发一次Full GC,所以heap dump文件里保存的都是FullGC后留下的对象信息。
在这里插入图片描述
生产环境的dump文件可能有几百M,一般导出dump:live即存活的对象即可。

使用2:显示堆内存相关信息

jmap -heap pid
jmap -histo

由于jmap将访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap需要借助安全点机制,让所有线程停留在不改变堆中数据的状态。也就是说,由jmap导出的堆快照必定是安全点位置的。这可能导致基于该堆快照的分析结果存在偏差。

举个例子,假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么:live选项将无法探知到这些对象。

另外,如果某个线程长时间无法跑到安全点,jmap将一 直等下去。

这里是显示某个时刻的堆内存相关信息,不如jstat那样可以设置时间间隔和次数方便。

使用3:其它作用

jmap -permstat pid 查看系统的ClassLoader信息
jmap -finalizerinfo 查看堆积在finalizer队列中的对象

jhat

jhat命令与jmap命令搭配使用,用于分析jmap生成的heap dump文件(堆转储快照)。jhat内置了一个微型的HTTP/HTML服务器,生成dump 文件的分析结果后,用户可以在浏览器中查看分析结果(分析虚拟机转储快照信息)。

使用了jhat命令,就启动了一个http服务,端口是7000,即http://localhost : 7000/,就可以在浏览器里分析。

说明: jhat 命令在JDK9、JDK10中已经被删除,官方建议用VisualVM代替。

jstack

用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟综)。线程快照就是当前虚拟机内指定进程的每–条线程正在执行的方法堆栈的集合。

生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack显示各个线程调用的堆栈情况。

在thread dump中, 要留意下面几种状态:

  • 死锁,Deadlock (重点关注)
  • 等待资源,Waiting on condition (重点关注)
  • 等待获取监视器,Waiting on monitor entry (重点关注)
  • 阻塞,Blocked (重点关注)
  • 执行中,Runnable
  • 暂停,Suspended

线程死锁演示

public class ThreadDeadLock {
    public static void main(String[] args) {
        StringBuilder s1=new StringBuilder();
        StringBuilder s2=new StringBuilder();
        new Thread(){
            @Override
            public void run(){
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");
                    try {
                        Thread.sleep(100);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run(){
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");
                    try {
                        Thread.sleep(100);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
    }
}

在这里插入图片描述
代码也可以实现线程追踪,但是没有jstat好用,还需要自己分析,且还要集成到要追踪的代码中,有侵入性:
在这里插入图片描述
在这里插入图片描述

jcmd

在 jdk1.7 之后,新增了一个命令行工具jcmd,它是一个多功能工具,用它可以导出堆,查看java进程,导出线程信息,执行GC等。jcmd能将命令发送给正在运行的JVM,诊断运行中的java应用。

JFR.stop # java飞行记录器 停止
JFR.start # java飞行记录器 开始
JFR.dump # java飞行记录器 dump
JFR.check # java飞行记录器 检查
VM.native_memory # 虚拟机本地内存
VM.check_commercial_features #
VM.unlock_commercial_features #
ManagementAgent.stop
ManagementAgent.start_local
ManagementAgent.start
GC.rotate_log
Thread.print # 线程打印
GC.class_stats # 类统计数据
GC.class_histogram # 类直方图
GC.heap_dump # gc堆dump
GC.run_finalization # gc运行结束
GC.run # gc运行
VM.uptime # 虚拟机运行时间单位 秒
VM.flags # 虚拟机信息
VM.system_properties # 虚拟机系统配置
VM.command_line # 虚拟机命令行
VM.version # 虚拟机版本

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六、诊断工具 —— 软件工具

posted @ 2022-06-25 14:01  沙滩de流沙  阅读(166)  评论(0编辑  收藏  举报

关注「Java视界」公众号,获取更多技术干货