04-Java性能优化实战
欲速则不达,欲达则欲速!
—— 佚名
性能优化更多要求我们关注整体效果,兼顾可靠性、扩展性,以及极端的异常场景。
1. 理论分析
1.1 衡量指标
吞吐量和响应速度
吞吐量
:
- QPS 每秒查询数量,TPS 每秒事务数量,HPS 每秒HTTP请求数量
- 并行执行的优化,合理利用计算资源达到目标
响应速度
:
- Time 时间
- 串行执行的优化,优化执行步骤解决问题
- 响应速度提升,吞吐量也就跟着提升了
响应时间衡量
平均响应时间
并发量
同时能为多个用户提供服务的能力。
1.2 常用理论
基准测试
基准测试(Benchmark),测试某个程序的最佳性能。
测试之前,对应用进行预热,消除JIT编译器等因素的影响,java组件 JMH 就可以消除这些差异。
木桶理论
木桶理论:系统整体的性能,取决于系统重最慢的组件
。
比如数据库应用中,制约性能最严重的就是磁盘I/O问题
,硬盘是短板,也就是需要补齐这个短板。
1.3 注意点
- 数字说话而不是猜想
- 根据难度和影响程度:击破影响最大的点,将其他因素逐一击破
- 个体数据不足信
- 小批量数据的场景,需要有响应之间直方图去分析
- 不要过早优化和过度优化
- 项目开发和性能优化,要作为两个独立的步骤进行,
性能优化在项目功能大体进入稳定状态时在进行
- 项目开发和性能优化,要作为两个独立的步骤进行,
- 保持良好编码习惯
- 好的编码习惯、合适的设计模式
1.4 七类手段
复用优化
代码:重复代码提取。
数据:数据复用,缓冲 Buffer - 主要针对写操作、缓存 Cache - 主要针对读操作
计算优化
并行执行:多机、多进程、多线程
同步变异步:涉及编程逻辑的改变
惰性加载:需要时才加载。
结果集优化
数据:传输数据的效率和解析率的提高,如 JSON 、ProtoBuf
压缩:Nginx 和 feign 的 Gzip 压缩,使传输内容保持紧凑
批量:时效要求不高,处理能力有高要求的情况
资源冲突优化
锁:乐观锁效率更高
算法优化
空间换时间:CPU紧张的业务中,空间换时间的方式提高性能
算法本身:采用降低时间负责度的算法,递归、二分、排序、动态规划等
高效实现
组件:使用高性能组件,如长连接 netty 等
JVM 优化
垃圾回收器:广泛使用的是 G1
1.5 性能瓶颈
CPU
top 命令:CPU 占用情况查看。
uptime 命令:查看负载情况,分别显示 1min、5min、15min的数值。
vmstat 命令:CPU 繁忙程度查看。
每个进程上下文切换的具体数量,查看内存映射文件获取:
1
2
3
4
5
#进程id 2788
[root@localhost~]# cat /proc/2788/status
...
voluntary_ctxt_switches: 93950
nonvoluntary_ctxt_switches: 171204
内存
一些程序的默认行为会对性能有所影响,比如JVM的 -XX:+AlwaysPreTouch
参数。默认情况下,JVM虽然配置了Xmx、Xms等参数,指定堆的初始化大小和最大大小。如果加上 AlwaysPreTouch,JVM会在启动的时候,把所有的内存预先分配。
I/O
缓冲区:解决速度差异的唯一工具。
iostat
命令:查看磁盘I/O情况的工具。
零拷贝:非常重要的性能优化手段,如kafka、Nginx都使用了这种手段。
2. 工具支持
2.1 nmon 获取系统性能数据
选择对应的版本,如 ./nmon_x86_64_centos7
- 按 C 可以加入 CPU 面板
- 按 M 可以加入 内存 面板
- 按 N 可以加入 网络 面板
- 按 D 可以加入 磁盘 面板
1 |
|
监控:最流行的组合是 prometheus + grafana + telegraf
搭建功能强大的监控平台。
2.2 jvisualvm 获取JVM性能数据
插件上加入 jmx 参数:
参数的含义:在 14000 端口上开启 jmx,同时不需要 ssl
2.3 jmc 获取java应用详细性能数据
录制1分钟后查看线程的执行情况:
2.4 arthas 获取单个请求的调用链耗时
主要使用 trace
命令,参考:Arthas阿里开源诊断工具
2.5 wrk 获取web接口的性能数据
HTTP 压测工具,和 ab命令类似,命令行工具,参考:https://github.com/wg/wrk
扩展:jmeter 是专业的压测工具,可以生成压测报告。
2.6 java JMH 精准测量方法性能
如上图常用的java代码耗时计算,但并不是一定准确。因为 JVM执行时,会对一些代码或频繁执行的逻辑,进行 JIT编译和内联优化,在得到一个稳定的测试结果之前,需要先循环上万次进行预热。
JMH
(the Java Microbenchmark Harness) 基准测试的工具,测量精读非常高,可达纳秒
级别。它已经在 JDK12 中被默认包含,其他版本需要单独引入。
使用参考:https://www.cnblogs.com/54chensongxia/p/15485421.html
官方示例:https://hg.openjdk.org/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/
使用前需引入依赖:
1 |
|
1 |
|
图形化结果:jmh-result.json
文件
常用注解:
- @Warmup:预热所需要配置的一些基本测试参数,可用于类或者方法上。
- @Measurement:实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和
@Warmup
相同。 - @BenchmarkMode:用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:
@BenchmarkMode({Mode.SampleTime, Mode.AverageTime})
,还可以设置为Mode.All
,即全部执行一遍。 - @OutputTimeUnit:为统计结果的时间单位,可用于类或者方法注解。
- @Fork:进行 fork 的次数,可用于类或者方法上。一般设置为 1 只使用一个进程进行测试;如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
独立的进程进行测试的,环境数据隔离。
它也可以通过 jvmArgsAppend 参数来传入 JVM 参数。
- @Threads:每个进程中的测试线程,可用于类或者方法上。如果配置了 Threads.MAX 则使用和处理机器核数相同的线程数。
- @State:通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。
- @Param:指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。
常用示例 - 输出耗时:
1 |
|
使用@BenchmarkMode
注解,将测试模式设置为Mode.AverageTime
,表示测试方法的耗时为平均时间。然后使用@OutputTimeUnit
注解,将输出时间单位设置为TimeUnit.MILLISECONDS
,表示以毫秒为单位输出耗时。
在运行该示例代码时,控制台输出将显示每个操作的平均耗时(ms/ops)。
2.7 性能深挖工具
3. 代码性能优化
3.1 缓冲 buffer
Buffer 缓冲:数据一般只使用一次,等待缓冲区满了,就执行 flush 操作。
如 JVM 的堆,就是一个缓冲的概念,代码在堆中不停的生成对象,垃圾回收器在堆中回收。
文件读写流:
- 缓冲保存在内存中,显著提升读写速度,折中的值是 8k,也就是 8192 字节。
日志缓冲:
- 高速日志组件 SLF4J 实现的 Logback,缓冲队列实现异步日志
- Logback 异步日志配置:
★★★★★缓冲区优化思路
:
- 同步操作:控制缓冲区大小,把握处理的时机
- 异步操作:多线程,等待线程超时策略,异步回调函数
- StringBuilder 和 StringBuffer:将要处理的字符串缓冲起来,提高拼接性能
- flush函数强制刷新数据:在写入磁盘或网络I/O时
- MySQL的InnoDB中,配置合理的 innodb_buffer_pool_size 来减少换页,增加数据库的性能
注意事项:
内容写入缓冲区前,需要
先预写日志
,断电/异常退出/kill -9时等故障重启时,根据日志进行数据恢复。
3.2 缓存 cache
缓存 Cache:数据被载入之后,可以多次使用,数据将会共享多次。
缓存的指标:命中率。
影响命中率的因素:
- 缓存容量
- 数据集类型
- 缓存失效策略
本地缓存
(堆内缓存)
推荐比如 Guava 的 LoadingCache(LC),是堆内缓存工具。也可以理解为 本地缓存。
1 |
|
具体使用参考:https://www.jianshu.com/p/3d546868a1db
分布式缓存
(Redis)
java的Redis客户端 jedis、redisson、lettuce(Spring默认使用的是lettuce)
- 引入 spring-boot-starter-data-redis 时,使用 redisTemplate.opsXxx 方法操作缓存
- 引入 spring-boot-starter-cache 时,使用注解+AOP方式,可以在堆内缓存和分布式缓存之间切换
- 启动类加 @EnableCaching
- @CacheConfig 注解注入要使用的缓存框架
- @Cacheable 对资源进行缓存
- @Cacheable 缓存里没有,则将方法返回值进行缓存
- @CachePut 每次执行该方法,都将返回值缓存起来
- @CacheEvict 执行方法的时候,清除某些缓存值
示例:秒杀业务处理
- Lua 脚本完成秒杀:解决同步问题
- 秒杀代码
注意事项:
缓存一致性问题:懒加载的方式。
- 读缓存时无缓存数据则执行业务逻辑载入缓存
- 与缓存有关的资源变动时,先删除相应的缓存项,再对资源进行更新(此时即使资源更新失败也没问题)
3.3 池化对象 pool
连接池
公共池化包 Commons Pool 2.0
- Jedis 是在 Commons Pool 2.0 的基础上封装的。
- HikariCP 是数据库连接池中速度最快的。
常见连接池 http连接池、RPC使用连接池技术、Dubbo连接池技术等…
3.4 大对象复用 obj
占用资源多,垃圾回收花费时间递增;网络I/O变大;解析和处理耗时高。
大对象回收:切断与大对象的引用关系,便于让大对象及时回收。
集合大对象扩容:初始化容量的考量关联扩容因子来设计。
保持合适的对象粒度:对象存储缓存的时候,使用 hash 结构代替 JSON 结构,加快字段获取和信息流转速度。
Bitmap 把对象变小:100亿的 Boolean 数据只占128M内存,java虚拟机中对 Boolean 和 int 一样都是 32位。
数据的冷热分离:数据的时间维度划分。数据双写、写入MQ分发冷热库、使用Binlog同步(Canal组件结合MQ)
3.5 设计模式 design
和性能相关的几个设计模式:代理模式、单例模式、享元模式、原型模式…
代理模式:jdk方式 和 cglib 的创建和执行差别不大,spring选择 cglib是因为可以代理普通类。
单例模式:double check 双检锁单例,兼顾安全和效率,不推荐使用。推荐枚举实现懒加载的单例。
享元模式:共享技术最大限度复用对象。对象复用角度属于 享元模式,功能角度属于 策略模式。
原型模式:加快创建对象的一种思想。
对性能帮助最大的是 生产者消费者模式
,比如异步消息、reactor模型等…
3.6 多线程 threads
使用线程池
:ThreadPoolExector
I/O密集型:核心线程数量 = I/O任务的数量,最大线程数 = 核心线程数 × 2,效果是最好的。在充分利用CPU资源的同时,确保有足够的线程来处理业务逻辑。
CPU密集型:核心线程数量 = CPU核数,最大线程数 = 核心线程数,效率是最高的。因为任务之间切换少,可以充分压榨CPU的计算性能。
使用线程池:SpringBoot启动类上加 @EnableAsync 注解,在具体的方法上加 @Async(“线程池bean”)
注意事项:
- 多线程中,如果抛出了异常,该异常没有被 try-catch 则会导致该线程异常中止。
3.7 SpringBoot服务性能优化
- 在 SpringBoot 的配置文件中,通过如下配置开启 gzip,对结果集去除无用的信息和合理的压缩来提高性能。
1 |
|
同时针对 feign 的底层网络工具改为
OkHTTP
,使用 OkHTTP 的透明压缩(默认开启 gzip),即可完成服务间调用的信息压缩。将结果集合并,使用批量的方式,可以显著增加性能
3.8 代码优化法则
使用局部变量可避免在堆上分配
:堆资源是多线程共享的,过多的对象会造成GC压力,局部变量的方式,将变量在栈上分配。减少变量的作用范围
:除了循环中。访问静态变量直接使用类名
:对象操作会增加寻址操作。字符串拼接
:使用 StringBuilder 或 StringBuffer,不要使用 + 号。重写对象的 HashCode,不要简单的返回固定值
:固定返回 0 相当于把 hash 寻址的功能废除了。HashMap 等集合初始化的时候,指定初始值大小
:大对象的复用,减少扩容带来的损耗。遍历 Map 的时候使用 EntrySet 方法
:比 KeySet 步骤更少。不要在多线程下使用同一个 Random
:Random类的seed会在并发访问的情况下发生竞争,造成性能降低。建议在多线程环境下使用ThreaLocalRandom
类。也可以通过 JVM 配置加入-Djava.security.egd=file:/dev/./urandom
使用 urandom 随机生成器,在随机数获取时,速度会更快。自增推荐使用 LongAddr
:也可以使用原子类 AtomicLong。不要使用异常控制程序流程
:异常比条件判断更消耗资源。不要在循环中使用 try-catch
:应该放在最外层。不要捕捉 RuntimeException
:应该在编码层面就要解决掉。合理使用 PreparedStatement
:预编译对 SQL 进行提速。日志打印注意事项-占位符
:使用占位符的方式提高日志的性能,减少事务的作用范围
:事务的隔离性使用锁实现的,锁对性能有损耗。使用位移操作代替乘除法
:位移操作会极大提高性能。不要打印大集合或大集合的toString方法
:非常不好的习惯,占用大量的内存 I/O。程序中少用反射
:反射通过解析字节码实现的,性能不太理想。正则表达式可以预先编译,加快速度
:Pattern 可以作为类的静态变量使用来预编译。- … 参考汇总图
4. JVM虚拟机优化
参考1:JVM参数调优
参考2:4Cpu8G的JVM参数设置方案
5. 性能优化过程方法
- 优化目标
- 核心维度
CPU
- top 命令,注意它的负载 load 和 使用率。比如
top -Hp
便能很容易获取占用 CPU 最高的线程,进行针对性的优化。 - vmstat 命令,也可以看到一些运行状况,如上下文切换和swap交换分区使用情况。
- top 命令,注意它的负载 load 和 使用率。比如
内存
- free 命令,关注剩余内存的大小 free。
- top 命令,RES 列显示的就是进程实际占用的物理内存。
网络I/O
- iotop 命令,可以看到占用 I/O 最多的进程。
- netstat 命令 或 ss 命令,可以看到当前机器上的网络连接汇总。
- iostat 命令,可以查看磁盘 I/O 的使用情况。
通用
- lsof 命令,可以查看当前进程所关联的所有资源。
- sysctl 命令,可以查看当前系统内核的配置参数。
- dmesg 命令,可以显示系统级别的一些信息。
- 基本解决方式
- PDCA 循环方法论