• 作者简介:我们好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴知道我们
  • 系列专栏:Spring原理、JUC原理、Kafka原理、分布式技术原理、数据库技术、JVM原理
  • 假如感觉博主的文章还不错的话,请三连支持一下博主哦
  • 博主正在努力完成2023计划中:源码溯源,一探终究
  • 联系方式:nhs19990716,加我进群,我们一同学习,一同前进,一同对抗互联网隆冬

功用调优

功用优化的进程总共分为四个进程,其间修复部分要具体问题具体剖析且处理方式各不相同。

JVM系列-9.功用调优

功用调优处理的问题

应用程序在运转进程中经常会呈现功用问题,比较常见的功用问题现象是:

1、经过top指令检查CPU占用率高,接近100乃至多核CPU下超过100都是有可能的。

JVM系列-9.功用调优

2、恳求单个服务处理时刻特别长,多服务运用skywalking等监控系统来判断是哪一个环节功用低下。

JVM系列-9.功用调优

3、程序发动之后运转正常,可是在运转一段时刻之后无法处理任何的恳求(内存和GC正常)。

功用调优的办法

线程转储(Thread Dump)供给了对一切运转中的线程当时状况的快照。线程转储能够经过jstack、visualvm等东西获取。其间包含了线程名、优先级、线程ID、线程状况、线程栈信息等等内容,能够用来处理CPU占用率高、死锁等问题。

JVM系列-9.功用调优

先运用jps 检查对应的进程号,然后运用jstack 进程号即可。或许运用visualvm的 thread dump。

线程转储(Thread Dump)中的几个核心内容:

◆ 名称: 线程名称,经过给线程设置适宜的名称更容易“见名知意”

◆ 优先级(prio):线程的优先级

◆ Java ID(tid):JVM中线程的唯一ID

◆ 本地 ID (nid):操作系统分配给线程的唯一ID

◆ 状况:线程的状况,分为:

NEW – 新创立的线程,尚未开端履行

RUNNABLE –正在运转或预备履行

BLOCKED – 等候获取监视器锁以进入或重新进入同步块/办法

WAITING – 等候其他线程履行特定操作,没有时刻限制

TIMED_WAITING – 等候其他线程在指定时刻内履行特定操作

TERMINATED – 已完成履行

◆ 栈追踪: 显现整个办法的栈帧信息

事例1:CPU占用率高问题的处理方案

问题:

监控人员经过prometheus的告警发现CPU占用率一直处于很高的情况,经过top指令看到是由于Java程序引起的,希望能快速定位到是哪一部分代码导致了功用问题。

处理思路:

1、经过top –c 指令找到CPU占用率高的进程,获取它的进程ID。

JVM系列-9.功用调优

2、运用top -p 进程ID独自监控某个进程,按H能够检查到一切的线程以及线程对应的CPU运用率,找到CPU运用率特别高的线程。

JVM系列-9.功用调优

3、运用 jstack 进程ID 指令能够检查到一切线程正在履行的栈信息。运用 jstack 进程ID > 文件名 保存到文件中便利检查。

JVM系列-9.功用调优

4、找到nid线程ID相同的栈信息,需求将之前记录下的十进制线程号转换成16进制。经过 printf ‘%xn’ 线程ID 指令直接取得16进制下的线程ID。

JVM系列-9.功用调优

5、找到栈信息对应的源代码,并剖析问题发生原因。

事例2:接口呼应时刻很长的问题

问题:

在程序运转进程中,发现有几个接口的呼应时刻特别长,需求快速定位到是哪一个办法的代码履行进程中呈现了功用问题。

处理思路:

已经确定是某个接口功用呈现了问题,可是由于办法嵌套比较深,需求借助于arthas定位到具体的办法。

JVM系列-9.功用调优

Arthas的trace指令

运用arthas的trace指令,能够展示出整个办法的调用路径以及每一个办法的履行耗时。

指令: trace 类名 办法名

  • 增加 –skipJDKMethod false 参数能够输出JDK核心包中的办法及耗时。
  • 增加 ‘#cost > 毫秒值’ 参数,只会显现耗时超过该毫秒值的调用。
  • 增加 –n 数值 参数,最多显现该数值条数的数据。
  • 一切监控都完毕之后,输入stop完毕监控,重置arthas增强的目标。

JVM系列-9.功用调优

Arthas的watch指令

在运用trace定位到功用较低的办法之后,运用watch指令监控该办法,能够取得更为具体的办法信息。

指令: watch 类名 办法名 ‘{params, returnObj}’ ‘#cost>毫秒值’ -x 2

  • ‘{params, returnObj}‘ 代表打印参数和返回值。
  • -x 代表打印的成果中假如有嵌套(比如目标里有特点),最多只打开2层。答应设置的最大值为4。

JVM系列-9.功用调优

总结:

1、经过arthas的trace指令,首要找到功用较差的具体办法,假如访问量比较大,建议设置最小的耗时,准确的找到耗时比较高的调用。

2、经过watch指令,检查此调用的参数和返回值,重点是参数,这样就能够在开发环境或许测验环境模拟类似的现象,经过debug找到具体的问题本源。

3、运用stop指令将一切增强的目标康复。(由于arthas底层是运用动态署理的方式去增强这些目标,然后获取调用时刻的。这样就增加了办法调用的开销,降低了功用)

事例3:定位偏底层的功用问题

问题:

有一个接口中运用了for循环向ArrayList中增加数据,可是终究发现履行时刻比较长,需求定位是由于什么原因导致的功用低下。

处理思路:

Arthas供给了功用火焰图的功用,能够非常直观地显现一切办法中哪些办法履行时刻比较长。

Arthas的profile指令

这个指令不支持windows版本,所以需求在linux上运转。

指令1: profiler start 开端监控办法履行功用

指令2: profiler stop –format html 以HTML的方式生成火焰图

火焰图中一般找绿色部分Java中栈顶上比较平的部分,很可能就是功用的瓶颈。

JVM系列-9.功用调优

总结:

偏底层的功用问题,特别是由于JDK中某些办法被很多调用导致的功用低下,能够运用火焰图非常直观的找到原因。

这个事例中是由于创立ArrayList时没有手动指定容量,导致运用默认的容量而在增加目标进程中发生了多次的扩容,扩容需求将本来数组中的元素复制到新的数组中,耗费了很多的时刻。经过火焰图能够看到很多的调用,修复完之后节省了20% ~ 50%的时刻。

事例4:线程被耗尽问题

问题:

程序在发动运转一段时刻之后,就无法接受任何恳求了。将程序重启之后继续运转,依然会呈现相同的情况。

处理思路:

线程耗尽问题,一般是由于履行时刻过长,剖析办法分成两步:

1、检测是否有死锁发生,无法自动免除的死锁会将线程永久堵塞。

2、假如没有死锁,再运用事例1的打印线程栈的办法检测线程正在履行哪个办法,一般这些很多呈现的办法就是慢办法。

死锁:两个或以上的线程由于争夺资源而造成互相等候的现象。

处理方案:

线程死锁能够经过三种办法定位问题:

1、 jstack -l 进程ID > 文件名 将线程栈保存到本地。

在文件中搜索deadlock即可找到死锁方位:

JVM系列-9.功用调优

2、 开发环境中运用visual vm或许Jconsole东西,都能够检测出死锁。运用线程快照生成东西就能够看到死锁的本源。出产环境的服务一般不会答应运用这两种东西衔接。

JVM系列-9.功用调优
3、 运用fastthread自动检测线程问题。

Fastthread和Gceasy类似,是一款在线的AI自动线程问题检测东西,能够供给线程剖析陈述。经过陈述检查是否存在死锁问题。

JVM系列-9.功用调优

更精细化的功用测验

JVM系列-9.功用调优

Java程序在运转进程中,JIT即时编译器会实时对代码进行功用优化,所以仅凭少量的测验是无法真实反响运转系统终究给用户供给的功用。如下图,随着履行次数的增加,程序功用会逐渐优化。

JVM系列-9.功用调优

OpenJDK中供给了一款叫JMH(Java Microbenchmark Harness)的东西,能够准确地对Java代码进行基准测验,量化办法的履行功用。

官网地址:github.com/openjdk/jmh

JMH会首要履行预热进程,确保JIT对代码进行优化之后再进行真正的迭代测验,最终输出测验的成果。

JVM系列-9.功用调优

JMH环境建立:

创立基准测验项目,在CMD窗口中,运用以下指令创立JMH环境项目:

mvn archetype:generate 
-DinteractiveMode=false 
-DarchetypeGroupId=org.openjdk.jmh 
-DarchetypeArtifactId=jmh-java-benchmark-archetype 
-DgroupId=org.sample 
-DartifactId=test 
-Dversion=1.0

修正POM文件中的JDK版本号和JMH版本号,JMH最新版本号参阅Github。

JVM系列-9.功用调优

测验代码

//履行5轮预热,每次继续1秒
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
//履行一次测验
@Fork(value = 1, jvmArgsAppend = {"-Xms1g", "-Xmx1g"})
//显现均匀时刻,单位纳秒
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class HelloWorldBench {
    @Benchmark
    public int test1() {
        int i = 0;
        i++;
        return i;
    }
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(HelloWorldBench.class.getSimpleName())
                .resultFormat(ResultFormatType.JSON)
                .forks(1)
                .build();
        new Runner(opt).run();
    }
}

事例:日期格式化办法功用测验

问题:

在JDK8中,能够运用Date进行日期的格式化,也能够运用LocalDateTime进行格式化,运用JMH比照这两种格式化的功用。

处理思路:

1、建立JMH测验环境。

2、编写JMH测验代码。

3、进行测验。

4、比对测验成果。

//履行5轮预热,每次继续1秒
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
//履行一次测验
@Fork(value = 1, jvmArgsAppend = {"-Xms1g", "-Xmx1g"})
//显现均匀时刻,单位纳秒
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class DateBench {
    private static String sDateFormatString = "yyyy-MM-dd HH:mm:ss";
    private Date date = new Date();
    private LocalDateTime localDateTime = LocalDateTime.now();
    private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal();
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    @Setup
    public void setUp() {
        SimpleDateFormat sdf = new SimpleDateFormat(sDateFormatString);
        simpleDateFormatThreadLocal.set(sdf);
    }
    @Benchmark
    public String date() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(sDateFormatString);
        return simpleDateFormat.format(date);
    }
    @Benchmark
    public String localDateTime() {
        return localDateTime.format(formatter);
    }
    @Benchmark
    public String localDateTimeNotSave() {
        return localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
    @Benchmark
    public String dateThreadLocal() {
        return simpleDateFormatThreadLocal.get().format(date);
    }
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(DateBench.class.getSimpleName())
                .resultFormat(ResultFormatType.JSON)
                .forks(1)
                .build();
        new Runner(opt).run();
    }
}

1、Date目标运用的SimpleDateFormatter是线程不安全的,所以每次需求重新创立目标或许将目标放入ThreadLocal中进行保存。其间每次重新创立目标功用比较差,将目标放入ThreadLocal之后功用相对还是比较好的。

2、LocalDateTime目标运用的DateTimeFormatter线程安全,而且功用较好,假如能将DateTimeFormatter目标保存下来,功用能够得到进一步的提高。

JVM系列-9.功用调优

功用调优综合实战

问题:

小李的项目中有一个获取用户信息的接口功用比较差,他希望能对这个接口在代码中进行彻底的优化,提高功用。

处理思路:

1、运用trace剖析功用瓶颈。

2、优化代码,重复运用trace测验功用提高的情况。

3、运用JMH在SpringBoot环境中进行测验。

4、比对测验成果。