JVM实战笔记-随笔
- 前言
- 字节码
- 如何查看字节码文件
- jclasslib
- Javap
- Arthas
- Arthurs监控面板
- Arthus查看字节码信息
- 内存调优
- 内存溢出的常见场景
- 解决内存溢出
- 发现问题
- Top命令
- VisualVM
- Arthas
- 使用案例
- Prometheus + Grafana
- 案例
- 堆内存情况对比
- 内存泄漏的原因:
- 代码中的内存泄漏
- 并发请求问题
- Jmeter模拟并发请求
- 插件增强(不是必须,可能自带)
- 案例
- 使用
- 诊断问题
- MAT工具使用和内存快照
- Jmap命令
- 案例: 没有溢出,但是持续增长怎么分析——jmap命令
- 分析超大堆的内存快照
- 修复问题
- 案例1:分页查询文章接口导致的内存溢出
- 实践
- 如何查看是什么占用内存的是什么数据?
- 如果是线程的问题,怎么查看是哪个接口呢?
- 如何根据接口确定是否相关对象占用过大?
- 如何模拟重现问题
- 解决案例1问题
- 案例2: Mybatis导致的内存溢出
- 实践
- 分析SQL相关的内存占用
- 解决案例2
- 案例3:导出大文件内存溢出
- 搭建k8s集群
- 案例4:ThreadLocal使用时占用大量内存
- 案例5:文章内容审核接口的内存溢出
- 设计1:
- 设计2:
- 设计3:
- 两种方式的优缺点
- 在线定位
- jmap在线定位步骤
- btrace在线定位步骤
- 小结
- GC调优
- GC调优的核心分成三部分:
- GC调优的指标
- 吞吐量:
- 垃圾回收吞吐量
- 延迟
- 内存的使用量
- GCeasy工具
- GC调优的方法工具
- jstat工具
- VisualVM
- VisualGC的GC插件
- Prometheus + Grafana
- GC 日志
- GC Viewer
- GC easy
- 以上是所有gc调优相关的工具
- 常见的GC模式
- 一、正常情况
- 二、缓存对象过多
- 三、内存泄漏
- 四、持续的FullGC
- 五、元空间不足导致的FULLGC
- 解决GC问题的手段
- 如何通过oracle官网找到jdk的相关参数(重要)
- 优化基础JVM参数
- -Xmx 和 –Xms
- -XX:MaxMetaspaceSize 和 –XX:MetaspaceSize
- -Xss
- 不建议手动设置的参数
- -Xmn
- ‐XX:SurvivorRatio
- ‐XX:MaxTenuringThreshold
- 其他参数 :
- -XX:+DisableExplicitGC
- -XX:+HeapDumpOnOutOfMemoryError:
- 垃圾回收器的选择
- 优化垃圾回收器的参数
- 案例
- 实战: 内存调优+GC调优
- GC调优的核心流程:
- 性能调优
- 线程转储方式
- Jstack命令
- VisualVM
- 案例
- 案例1:CPU占用率高问题的解决方案
- 遗留问题:
- 案例补充:
- 案例2: 接口响应时间很长的问题
- Arthas的trace命令
- Arthas的watch命令
- 案例3: 定位偏底层的性能问题
- 案例4:线程被耗尽问题
- 更精细的性能测试
- JIT对程序性能的影响
- 正确地测试代码性能
- JMH环境搭建:
- 案例:日期格式化方法性能测试
- 性能调优综合实战
- 总结
前言
该文章包含jvm调优工具的学习记录以及常用的jvm调优工具,仅随笔,不完善地方请指出
字节码
如何查看字节码文件
- 本地文件可以使用jclasslib工具查看,开发环境使用jclasslib插件
- 服务器上文件使用javap命令直接查看,也可以通过arthas的dump命令导出字节码文件再查看本地文件。还可以使用jad命令反编译出源代码
javap
dump
jclasslib
——用于本地查看字节码文件
jclasslib工具查看字节码文件
Github地址: https://github.com/ingokegel/jclasslib
jclasslib也有Idea插件版本,建议开发时使用Idea插件版本,可以在代码编译之后实时看到字节码
文件内容
- 安装插件jclasslib
- 左上角View的show…class
Javap
——用于查看服务器字上的节码文件
java自带的反编译工具,可以通过控制台查看字节码文件的内容
- 适合在服务器上面查看字节码文件
- 直接在shell输入javap查参数
- 输入 javap -v 字节码文件名称 查看具体的字节码信息
- 如果是jar包,需要jar -xvf 命令解压
- 进入BOOT-INF的com 目录下找到字节码文件进行展示
案例
Arthas
——用于查看服务器运行中的字节码文件
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。
官网:https://arthas.aliyun.com/doc/
- 查看字节码信息
- 内存监控
- 垃圾回收监控
- 应用热点监控
Arthurs的jar包
链接:https://pan.baidu.com/s/1gzl7MuXeHk1CkH2z4EzkWg?pwd=k76r
提取码:k76r
使用Arthurs的方式
- 直接像运行java的jar包一样在服务器运行
Arthurs监控面板
案例
- 直接运行Arthur-boot.jar展示运行的java程序
- 比如想要进入5的程序,直接键入5
- 执行dashboard -i 2000 -n 3
- 暂停2s,执行三次展示
Arthus查看字节码信息
通过dump命令将类的字节码信息存储到某个文件内(前提是启动Arthurs进入这个进程)
- 如下
通过jad命令通过字节码查看原代码
内存调优
- 什么是内存泄漏
- 监控java内存的工具
- 内存泄漏的常见场景
- 内存泄漏的解决方法
内存溢出的常见场景
- 大型java后端应用,在处理用户请求之后,没有及时删除用户信息。随着用户请求数据量越来越多,内存泄漏的对象沾满了堆内存,导致内存溢出。直接导致用户请求无法处理,重启可以恢复,但一段时间后依旧会溢出。
- 第二种常见场景是分布式任务调度系统如Elastic-job、Quartz等进行任务调度时,被调度的Java应用在调度任务结束中出现了内存泄漏,最终导致多次调度之后内存溢出。这种产生的内存溢出会导致应用执行下次的调度任务执行。同样重启可以恢复应用使用,但是在调度执行一段时间之后依然会出现内存溢出
解决内存溢出
发现问题
Top命令
——用于服务器查看进程CPU,内存占用等信息
- top命令是linux下用来查看系统信息的一个命令,它提供给我们去实时地去查看系统的资源,比如执行时的进程、线程和系统参数等信息,占用内存CPU情况等。
- 进程使用的内存为RES(常驻内存)- SHR(共享内存)
- 简单
VisualVM
——多用于监测分析测试环境的内存问题(生产环境会占用线程资源,慎用)
- VisualVM是多功能合一的Java故障排除工具并且他是一款可视化工具,整合了命令行 JDK 工具和轻量级分析功能,功能非常强大
- 这款软件在Oracle JDK 6~8 中发布,但是在 Oracle JDK 9 之后不在JDK安装目录下需要单独下载。下载地址:https://visualvm.github.io/
- jdk8在bin目录下,双击打开
- idea也有相关插件: visualVM
- visualVM远程访问服务器(不要在生产环境使用,会占用主线程,生产环境禁止使用!!!)
需要按下图进行开发权限端口
- 远程连接开发端口配置
- 远程连接
- 寻找jax连接
Arthas
——监控生产环境的内存占用情况
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、
gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,
包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升
线上问题排查效率。
- 功能强大,不止于监控基础的信息,还能监控单个方法的执行耗时等细节内,应用于集群管理
- 部分高级功能使用门槛较高
使用案例
- 在Spring Boot程序中添加arthas的依赖(支持Spring Boot2),在配置文件中添加tunnel服务端的地址,便于tunnel去监控所有的程序。
<dependency>
<groupId>com.taobao.arthas</groupId>
<artifactId>arthas-spring-boot-starter</artifactId>
<version>3.7.1</version>
</dependency>
//配置相关
arthas:
#tunnel地址,目前是部署在同一台服务器,正式环境需要拆分
tunnel-server: ws://localhost:7777/ws
#tunnel显示的应用名称,直接使用应用名
app-name: ${spring.application.name}
#arthas http访问的端口和远程连接的端口
http-port: 8888
telnet-port: 9999
- 将tunnel服务端程序部署在某台服务器上并启动。
nohup java -jar -Darthas.enable-detail-pages=true
arthas-tunnel-server-3.7.1-fatjar.jar &
也可以手动指定端口,分布式部署
- 启动java程序
- 打开tunnel的服务端页面,查看所有的进程列表,并选择进程进行arthas的操
输入: IP:端口/apps.html 打开控制台
Prometheus + Grafana
——专门的运维来搭建的监控系统,用于监控微服务集群
Prometheus+Grafana是企业中运维常用的监控方案,其中Prometheus用来采集
系统或者应用的相关数据,同时具备告警功能。Grafana可以将Prometheus采集
到的数据以可视化的方式进行展示。
- Java程序员要学会如何读懂Grafana展示的Java虚拟机相关的参数
- 支持系统级别和应用级别的监控,比如linux操作系统、Redis、MySQL、Java进程。支持告警并允许自定义告警指标,通过邮件、短信等方式尽早通知相关人员进行处理
- 环境搭建较为复杂,一般由运维人员完成
案例
阿里云搭建可观测监控
原视频
- 引入依赖(用于暴露指标)
//暴露springboot信息
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<exclusions><!-- 去掉springboot默认配置 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
// 暴露java虚拟机信息
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
- 配置
management:
endpoint:
metrics:
enabled: true #支持metrics
prometheus:
enabled: true #支持Prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: jvm-test #实例名采集
endpoints:
web:
exposure:
include: '*' #开放所有端口
- 就可以actuator暴露服务bean等信息
堆内存情况对比
内存泄漏的原因:
代码中的内存泄漏
——可以通过压力测试发现,开发中其实并不常见
- equals() 和 hashCode() (发生概率小)
在定义新类时没有重写正确的equals()和hashCode()方法。在使用HashMap的场景下,
如果使用这个类对象作为key,HashMap在判断key是否已经存在时会使用这些方法,如
果重写方式不正确,会导致相同的数据被保存多份
public class Demo2 {
public static long count = 0;
public static Map<Student,Long> map = new HashMap<>();
public static void main(String[] args) throws InterruptedException {
while (true){
if(count++ % 100 == 0){
Thread.sleep(10);
}
Student student = new Student();
student.setId(1);
student.setName("张三");
map.put(student,1L);
}
}
}
解决方法
-
在定义新实体时,始终重写equals()和hashCode()方法。
-
重写时一定要确定使用了唯一标识去区分不同的对象,比如用户的id等。
-
hashmap使用时尽量使用编号id等数据作为key,不要将整个实体类对象作为key存放。
-
内部类引用外部类(发生概率小)
- 非静态的内部类默认会持有外部类,尽管代码上不再使用外部类,所以如果有地方引用了这个非静态内部类,会导致外部类也被引用,垃圾回收时无法回收这个外部类。
- 匿名内部类对象如果在非静态方法中被创建,会持有调用者对象,垃圾回收时无法回收调用者
public class Outer{
private byte[] bytes = new byte[1024 * 1024]; //外部类持有数据
private static String name = "测试";
static class Inner{
private String name;
public Inner() {
this.name = Outer.name;
}
}
public static void main(String[] args) throws IOException, InterruptedException {
// System.in.read();
int count = 0;
ArrayList<Inner> inners = new ArrayList<>();
while (true){
if(count++ % 100 == 0){
Thread.sleep(10);
}
inners.add(new Inner());
}
}
}
解决方法
- 如果不想持有外部类对象,应该使用静态内部类
- 使用静态方法,可以避免匿名内部类持有调用者对象
- ThreadLocal的使用(出现频率较高)
- 如果仅仅使用手动创建的线程,就算没有调用ThreadLocal的remove方法清理数据,也不会产生内存泄漏。因为当线程被回收时,ThreadLocal也同样被回收。但是如果使用线程池就不一定了
//没有remove就会内存溢出
public class Demo5 {
public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(Integer.MAX_VALUE, Integer.MAX_VALUE,
0, TimeUnit.DAYS, new SynchronousQueue<>());
int count = 0;
while (true) {
System.out.println(++count);
threadPoolExecutor.execute(() -> {
threadLocal.set(new byte[1024 * 1024]);
threadLocal.remove();
});
Thread.sleep(10);
}
}
}
- 线程方法执行完,一定要调用ThreadLocal中的remove方法清理对象
- String的intern方法(出现极少,基本不用intern方法)
- JDK6中字符串常量池位于堆内存中的Perm Gen永久代中,如果不同字符串的intern方法被大量调用,字符串常量池会不停的变大超过永久代内存上限之后就会产生内存溢出问题
public class Demo6 {
public static void main(String[] args) {
while (true){
List<String> list = new ArrayList<String>();
int i = 0;
while (true) {
//String.valueOf(i++).intern(); //JDK1.6 perm gen 不会溢出
list.add(String.valueOf(i++).intern()); //溢出
}
}
}
}
- 不用就行了,
- 通过静态字段保存对象(高频)
- 如果大量的数据在静态变量中被长期引用,数据就不会被释放,如果这些数据不再使用,就成为了内存泄漏
//懒加载案例
@Lazy //懒加载
@Component
public class TestLazy {
private byte[] bytes = new byte[1024 * 1024 * 1024];
}
//缓存案例
public class CaffineDemo {
public static void main(String[] args) throws InterruptedException {
Cache<Object, Object> build = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(100))
.build();
int count = 0;
while (true){
build.put(count++,new byte[1024 * 1024 * 10]);
Thread.sleep(100L);
}
}
}
解决方法
- 尽量减少将对象长时间的保存在静态变量中,如果不再使用,必须将对象删除(比如在集合中)或者将静态变量设置为null。
- 使用单例模式时,尽量使用懒加载,而不是立即加载。
- Spring的Bean中不要长期存放大对象,如果是缓存用于提升性能,尽量设置过期时间定期失效
- 资源没有正常关闭
并发请求问题
并发请求问题指的是用户通过发送请求向Java应用获取数据,正常情况下Java应用将数据返回之后,这部分数据就可以在内存中被释放掉。但是由于用户的并发请求量有可能很大,同时处理数据的时间很长,导致大量的数据存在于内存中,最终超过了内存的上限,导致内存溢出。这类问题的处理思路和内存泄漏类似,首先要定位到对象产生的根源。
tips: 默认springboot的tomcat线程池为200,只能接收200并发
Jmeter模拟并发请求
——模拟并发,压测接口
Apache Jmeter是一款开源的测试软件,使用Java语言编写,最初是为了测试Web程序,目前已经发展成支持数据库、消息队列、邮件协议等不同类型内容的测试工具。
- 使用Apache Jmeter软件可以进行并发请求测试。
- Apache Jmeter是一款开源的测试软件,使用Java语言编写,最初是为了测试Web程序,目前已经发展成支
持数据库、消息队列、邮件协议等不同类型内容的测试工具。
- Apache Jmeter支持插件扩展,生成多样化的测试结果
插件增强(不是必须,可能自带)
作用显示:
- 显示活跃线程数
- 响应时长
- 每分钟的事务数
插件增加3个工具
- 将插件压缩包的lib目录下jar包放入jmeter下的lib包下
- 插件压缩包ext下的jar包放入jemeter下的ext包下
案例
背景:
小李的团队发现有一个微服务在晚上8点左右用户使用的高峰期会出现内存溢出的问题,于是他们希望在自己的开发环境能重现类似的问题
步骤:
- 安装Jmeter软件,添加线程组
- 在线程组中增加Http请求,添加随机参数
- 在线程组中添加监听器 – 聚合报告,用来展示最终结果
- 启动程序,运行线程组并观察程序是否出现内存溢出
使用
- 创建线程组,新减http请求
设置合适的线程数
- 建聚合报告
- 填写相关配置
4. 选出并创建合适的随机函数生成参数
- 设置参数
诊断问题
MAT工具使用和内存快照
——MAT是eclipse自带的一款工具,通过配置VM option, 内存异出时才能生成异常数据的分析文件,配合MAT分析
MAT工具单独下载:(装个jdk11及以上的jdk就行了,指定一下路径)
https://blog.csdn.net/wts563540/article/details/132380827
- 当堆内存溢出时,需要在堆内存溢出时将整个堆内存保存下来,生成内存快照(Heap Profile )文件。
- 生成内存快照的Java虚拟机参数:
-XX:+HeapDumpOnOutOfMemoryError //发生OutOfMemoryError错误时,自动生成hprof内存快照件。
-XX:HeapDumpPath=<path> //指定hprof文件的输出路径。
- 使用MAT打开hprof文件,并选择内存泄漏检测功能,MAT会自行根据内存快照中保存的数据分析内存泄漏的根源
Jmap命令
案例: 没有溢出,但是持续增长怎么分析——jmap命令
- jmap生成快照是会占用资源的,会影响用户的使用
背景:
小李的团队通过监控系统发现有一个服务内存在持续增长,希望尽快通过内存快照分析增长的
原因,由于并未产生内存溢出所以不能通过HeapDumpOnOutOfMemoryError参数生成内存快照
思路:
导出运行中系统的内存快照,比较简单的方式有两种,注意只需要导出标记为存活的对象:
- 通过JDK自带的jmap命令导出,格式为:
jmap -dump:live,format=b,file=文件路径和文件名 进程ID
- 通过arthas的heapdump命令导出,格式为:
heapdump --live 文件路径和文件名
分析超大堆的内存快照
——服务器内存有限,内存过大分析撑不住的情况下使用
- 在程序员开发用的机器内存范围之内的快照文件,直接使用MAT打开分析即可。但是经常会遇到服务器上的程序占用的内存达到10G以上,开发机无法正常打开此类内存快照,此时需要下载服务器操作系统对应的MAT。下载地址:https://eclipse.dev/mat/downloads.php
- 通过MAT中的脚本生成分析报告
./ParseHeapDump.sh 快照文件路径 org.eclipse.mat.api:suspects
org.eclipse.mat.api:overview org.eclipse.mat.api:top_components
不想官网(官网下载最新只支持jdk11以上)下直接——>
链接:https://pan.baidu.com/s/1b4jw1eydPI7srlcXd7IAbA?pwd=9y4z
提取码:9y4z
使用
- 上传到服务器,打开mat的memoryAnalyzer.ini修改需要的内存大小(默认只有1g,撑不住大内存分析)
- 将文件的权限全部打开
- 在mat目录下执行导出
./ParseHeapDump.sh 快照文件路径 org.eclipse.mat.api:suspects
org.eclipse.mat.api:overview org.eclipse.mat.api:top_components
- 下载三份报告
- 解压打开静态页面
修复问题
内存溢出的问题分为3类
- 代码中的内存溢出
代码中的内存泄漏在前面的篇章中已经介绍并提供了解决方案
- 并发引起的内存溢出——参数不当
由于参数设置不当,比如堆内存设置过小,导致并发量增加之后超过堆内存的上限。
解决方案:调整参数,详细往下看
- 并发引起内存溢出——设计不当
系统的方案设计不当,比如:
- 从数据库获取超大数据量的数据
- 线程池设计不当
- 生产者-消费者模型,消费者消费性能问题
解决方案:优化设计方案
案例1:分页查询文章接口导致的内存溢出
小李负责的新闻资讯类项目采用了微服务架构,其中有一个文章微服务,这个微服务在业务高峰期出现了内存溢出的现象
背景:
小李负责的新闻资讯类项目采用了微服务架构,其中有一个文章微服务,这个微服务在业务高峰期出现了内存溢出的现象。
解决思路:
1、服务出现OOM内存溢出时,生成内存快照。
2、使用MAT分析内存快照,找到内存溢出的对象。
3、尝试在开发环境中重现问题,分析代码中问题产生的原因。
4、修改代码。
5、测试并验证结果。
实践
- 解决思路的12不再赘述
//给需要导出hprof文件的项目启动时的参考参数
java -jar -Dserver.port=8081 -Xmx512m -Xms256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/testproject/learnJvm/jvm.hprof jvm-optimize-0.0.1-SNAPSHOT.jar
- 如何分析hprof文件找到对应的问题接口?
练习文件:
解决思路:
打开hprof文件
图中有两个地方占用大量内存
第一个怀疑对象
- tomcat线程池的大量线程导致的内存占用(但是这里看不出太多问题)
第二个怀疑对象
打开支配树,点击进行深度排序
如何查看是什么占用内存的是什么数据?
- 按树点开,还可以看到来跟数据表相关的内容标识
如果是线程的问题,怎么查看是哪个接口呢?
右键查看当前对象引用的对象:
- outgoing: 查看当前对象引用的对象
- ingoing: 查看当前对象被哪些对象引用
然后跳转如下就能找到对应的接口方法
如何根据接口确定是否相关对象占用过大?
假设定位到此接口问题
点击直方图,输入该对象的包前缀
如何模拟重现问题
- 定位到问题,就在开放环境重现问题
- 比如通过Jemter进行模拟
解决案例1问题
问题根源:
文章微服务中的分页接口没有限制最大单次访问条数,并且单个文章对象占用的内存量较大,在业务高峰期并发量较大时这部分从数据库获取到内存之后会占用大量的内存空间
解决思路:
- 与产品设计人员沟通,限制最大的单次访问条数。
- 分页接口如果只是为了展示文章列表,不需要获取文章内容,可以大大减少对象的大小。
- 在高峰期对微服务进行限流保护
案例2: Mybatis导致的内存溢出
背景:
小李负责的文章微服务进行了升级,新增加了一个判断id是否存在的接口,第二天业务高峰期再次出现了内存溢出,小李觉得应该和新增加的接口有关系。
解决思路:
1、服务出现OOM内存溢出时,生成内存快照。
2、使用MAT分析内存快照,找到内存溢出的对象。
3、尝试在开发环境中重现问题,分析代码中问题产生的原因。
4、修改代码。
5、测试并验证结果
实践
打开mat分析hprof文件
打开第二个:柱状图分析
与案例1同理找到溢出接口
树状图——>handler——>ListObject——>outgoing
分析SQL相关的内存占用
- 如图左下角说明mybatis有大量foreach拼接
代码:
通过jemeter进行测试
解决案例2
问题根源:
Mybatis在使用foreach进行sql拼接时,会在内存中创建对象,如果foreach处理的数组或者集合元素个数过多,会占用大量的内存空间。
解决思路:
1、限制参数中最大的id个数。
2、将id缓存到redis或者内存缓存中,通过缓存进行校验
案例3:导出大文件内存溢出
背景:
小李负责了一个管理系统,这个管理系统支持几十万条数据的excel文件导出。他发现系统在运行时如果有几十个人同时进行大数据量的导出,会出现内存溢出。
小李团队使用的是k8s将管理系统部署到了容器中,所以这一次我们使用阿里云的k8s环境还原场景,并解决问题。阿里云的k8s整体规划如下:
搭建k8s集群
链接
问题根源:
Excel文件导出如果使用POI的XSSFWorkbook,在大数据量(几十万)的情况下会占用大量的内存。
解决思路:
1、使用poi的SXSSFWorkbook。(最快,内存消耗大,比原来优化一点点)
2、hutool提供的BigExcelWriter减少内存开销。(慢一点,内存消耗较小)
3、使用easy excel,对内存进行了大量的优化(更慢一点,分批处理,内存优化最好)
案例4:ThreadLocal使用时占用大量内存
背景:
小李负责了一个微服务,但是他发现系统在没有任何用户使用时,也占用了大量的内存。导致可以使用的内存大大减少
模拟思路
- 创建项目部署的服务器
- jemeter模拟并发请求
- 生成hprof文件
- 下载本地
- 通过MAT分析
问题根源和解决思路:
很多微服务会选择在拦截器preHandle方法中去解析请求头中的数据,并放入一些数据到ThreadLocal中方便后续使用。在拦截器的afterCompletion方法中,必须要将ThreadLocal中的数据清理掉。
案例5:文章内容审核接口的内存溢出
背景:
文章微服务中提供了文章审核接口,会调用阿里云的内容安全接口进行文章中文字和图片的审核,在自测过程中出现内存占用较大的问题
设计1:
使用SpringBoot中的@Async注解进行异步的审核
存在问题:
1、线程池参数设置不当,会导致大量线程的创建或者队列中保存大量的数据。
2、任务没有持久化,一旦走线程池的拒绝策略或者服务宕机、服务器掉电等情况很有可能会丢失任务。
设计2:
使用生产者和消费者模式进行处理,队列数据可以实现持久化到数据库
存在问题:
1、队列参数设置不正确,会保存大量的数据。
2、实现复杂,需要自行实现持久化的机制,否则数据会丢失
设计3:
使用mq消息队列进行处理,由mq来保存文章的数据。发送消息的服务和拉取消息的服务可以是同一个,也可以不是同一个。
具体实现按项目需求分析:
问题根源和解决思路:
- 在项目中如果要使用异步进行业务处理,或者实现生产者 – 消费者的模型,如果在Java代码中实现,会占用大量的内存去保存中间数据。
- 尽量使用Mq消息队列,可以很好地将中间数据单独进行保存,不会占用Java的内存。同时也可以将生产者和消费者拆分成不同的微服务
两种方式的优缺点
在线定位
如果不想对用户访问产生过多影响,采用在线定位,但是信息没有快照详细
jmap在线定位步骤
- 使用jmap -histo:live 进程ID > 文件名 命令将内存中存活对象以直方图的形式保存到文件中,这个过程会影响用户的时间,但是时间比较短暂。
jmap -histo:live 进程ID > 文件名
- 分析内存占用最多的对象,一般这些对象可能就是造成内存泄漏的原因。
- 启动服务器上的arthas
- 使用arthas的stack命令,追踪对象创建的方法被调用的调用路径,找到对象创建的根源。
- 即可定位到哪一行进行的创建
btrace在线定位步骤
参考
——也可以使用btrace工具编写脚本追踪方法执行的过程。
BTrace 是一个在Java 平台上执行的追踪工具,可以有效地用于线上运行系统的方法追踪,具有侵入性小、对性能的影响微乎其微等特点。
- 项目中可以使用btrace工具,打印出方法被调用的栈信息。
- 这种方式需要有一定经验进行定制化脚本编写,能够获取更多的信息。
使用方法:
1、下载btrace工具, 官方地址:https://github.com/btraceio/btrace/releases/latest
2、编写btrace脚本,通常是一个java文件。
3、将btrace工具和脚本上传到服务器,在服务器上运行 btrace 进程ID 脚本文件名 。
4、观察执行结果。
小结
解决内存泄漏问题的方法是什么?
- 发现问题,通过监控工具尽可能尽早地发现内存慢慢变大的现象。
- 诊断原因,通过分析内存快照或者在线分析方法调用过程,诊断问题产生的根源,定位到出现问题的源代码。
- 修复源代码中的问题,如代码bug、技术方案不合理、业务设计不合理等等。
- 在测试环境验证问题是否已经解决,最后发布上线
GC调优
——最后总结gc调优调什么?怎么调?
- 学习如何分析GC日志
- 解决生产环境由于频繁Full GC导致的系统假死的问题
GC调优指的是对垃圾回收(Garbage Collection)进行调优。GC调优的主要目标是避免由垃圾回收引起程序性能下降。
GC调优的核心分成三部分:
- 通用Jvm参数的设置。
- 特定垃圾回收器的Jvm参数的设置。
- 解决由频繁的FULLGC引起的程序性能问题。
GC调优没有唯一的标准答案,如何调优与硬件、程序本身、使用情况均有关系,重点学习调优的工具和方法
GC调优的指标
吞吐量:
吞吐量分为业务吞吐量和垃圾回收吞吐量——业务吞吐量指的在一段时间内,程序需要完成的业务数量。比如企业中对于吞吐量的要求可能会是这样的:
- 支持用户每天生成10000笔订单
- 在晚上8点到10点,支持用户查询50000条商品信息
保证高吞吐量的常规手段有两条:
- 优化业务执行性能,减少单次业务的执行时间
- 优化垃圾回收吞吐量
垃圾回收吞吐量
垃圾回收吞吐量指:
- CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,
- 即吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间)。
- 吞吐量数值越高,垃圾回收的效率就越高,允许更多的CPU时间去处理用户的业务,相应的业务吞吐量也就越高
延迟
——响应时间
延迟指的是从用户发起一个请求到收到响应这其中经历的时间。
- 比如企业中对于延迟的要求可能会是这样的:
- 所有的请求必须在5秒内返回给用户结果
延迟 = GC延迟 + 业务执行时间,所以如果GC时间过长,会影响到用户的使用。
内存的使用量
内存使用量指的是Java应用占用系统内存的最大值,一般通过Jvm参数调整,在满足上述两个指标的前提下,这个值越小越好。
GCeasy工具
——用于分析垃圾回收的吞吐量等信息
GC调优的方法工具
GC调优的四个步骤
jstat工具
——重点关注fullgc的次数和时间 FGC、FGCT
Jstat工具是JDK自带的一款监控工具,可以提供各种垃圾回收、类加载、编译信息等不同的数据。
- 使用方法为:jstat -gc 进程ID 每次统计的间隔(毫秒) 统计次数
jstat -gc <进程ID> <每次统计的间隔(毫秒)> <统计次数>
组合下面的字母标识上图的指标
- C代表Capacity容量,U代表Used使用量
- S – 幸存者区,E – 伊甸园区,O – 老年代,M – 元空间
- YGC、YGT:年轻代GC次数和GC耗时(单位:秒)
- FGC、FGCT:Full GC次数和Full GC耗时
- GCT:GC总耗时
优点
- 简单
缺点
- 无法精确到gc产生的时间
- 只能判断gc是否有问题
VisualVM
——只能用于开发环境,生产环境会占用CPU资源
VisualVm中提供了一款Visual Tool插件,实时监控Java进程的堆内存结构、堆内存变化趋势以及垃圾回收时间的变化趋势。同时还可以监控对象晋升的直方图
- 适合开发使用,能直观的看到堆内存和GC的变化趋势
- 对程序运行性能有一定影响生产环境程序员一般没有权限进行操作
VisualGC的GC插件
查看某个进程的堆内存状态
Prometheus + Grafana
Prometheus+Grafana是企业中运维常用的监控方案,其中Prometheus用来采集系统或者应用的相关数据,同时具备告警功能。Grafana可以将Prometheus采集到的数据以可视化的方式进行展示。
- Java程序员要学会如何读懂Grafana展示的Java虚拟机相关的参数
优点
- 支持系统级别和应用级别的监控,比如linux操作系统、Redis、MySQL、Java进程。
- 支持告警并允许自定义告警指标,通过邮件、短信等方式尽早通知相关人员进行处理
缺点
- 环境搭建较为复杂,一般由运维人员完成
GC 日志
——比上述工具更详细的报告,用于定位问题
通过GC日志,可以更好的看到垃圾回收细节上的数据,同时也可以根据每款垃圾回收器的不同特点更好地发
现存在的问题。
- 直接在控制台打印信息
-verbose:gc
- 使用方法(JDK 8及以下):-XX:+PrintGCDetails -Xloggc:文件名
//不能和-verbose:gc 同时使用,只能单独使用,不指定路径,只指定文件名,默认保存项目目录下
-XX:+PrintGCDetails -Xloggc:保存文件的位置
- 使用方法(JDK 9+):-Xlog:gc*:file=文件名
-Xlog:gc*:file=文件名
效果
GC Viewer
——用于可视化上述的gc日志
- GCViewer是一个将GC日志转换成可视化图表的小工具,github地址:https://github.com/chewiebug/GCViewer
- 使用方法:java -jar gcviewer_1.3.4.jar 日志文件.log
GC easy
——使用简单的ai分析平台,分析gc日志并提供建议,需要翻墙
- GCeasy是业界首款使用AI机器学习技术在线进行GC分析和诊断的工具。定位内存泄漏、GC延迟高的问题,提供JVM参数优化建议,支持在线的可视化工具图表展示。
- 官方网站:https://gceasy.io/
以上是所有gc调优相关的工具
常见的GC模式
一、正常情况
特点:
呈现锯齿状,对象创建之后内存上升,一旦发生垃圾回收之后下降到底部,并且每次下降之后的内存大小接近,存留的对象较少。
二、缓存对象过多
特点:
呈现锯齿状,对象创建之后内存上升,一旦发生垃圾回收之后下降到底部,并且每次下降之后的内存大小接近,处于比较高的位置。
问题产生原因:
程序中保存了大量的缓存对象,导致GC之后无法释放,可以使用MAT或者HeapHero等工具进行
分析内存占用的原因。
三、内存泄漏
特点:
呈现锯齿状,每次垃圾回收之后下降到的内存位置越来越高,最后由于垃圾回收无法释放空间导致对象无法分配产生OutOfMemory的错误。
问题产生原因:
程序中保存了大量的内存泄漏对象,导致GC之后无法释放,可以使用MAT或者HeapHero等工具进行分析是哪些对象产生了内存泄漏
四、持续的FullGC
特点:
在某个时间点产生多次Full GC,CPU使用率同时飙高,用户请求基本无法处理。一段时间之后恢复正常。
问题产生原因:
在该时间范围请求量激增,程序开始生成更多对象,同时垃圾收集无法跟上对象创建速率,导致持续地在进行FULL GC。GC分析报告
五、元空间不足导致的FULLGC
特点:堆内存的大小并不是特别大,但是持续发生FULLGC。
问题产生原因: 元空间大小不足,导致持续FULLGC回收元空间的数据。GC分析报告
解决GC问题的手段
解决GC问题的手段中,前三种是比较推荐的手段,第四种仅在前三种无法解决时选用:
如何通过oracle官网找到jdk的相关参数(重要)
https://www.oracle.com/
products拉倒最下面
找到resources获取文档选择对应的版本
点击对应版本,选择jdk tool
找到所以命令,选择java
当前页面就能找到所有jvm参数(全局搜索想要的参数)
优化基础JVM参数
-Xmx 和 –Xms
-Xmx参数设置的是最大堆内存,但是由于程序是运行在服务器或者容器上,计算可用内存时,要将元空间、操作系统、其它软件占用的内存排除掉。
案例: 服务器内存4G,操作系统+元空间最大值+其它软件占用1.5G,-Xmx可以设置为2g。
最合理的设置方式应该是根据最大并发量估算服务器的配置,然后再根据服务器配置计算最大堆内存的值
-Xms用来设置初始堆大小,建议将-Xms设置的和-Xmx一样大,有以下几点好处:
- 运行时性能更好,堆的扩容是需要向操作系统申请内存的,这样会导致程序性能短期下降。
- 可用性问题,如果在扩容时其他程序正在使用大量内存,很容易因为操作系统内存不足分配失败。
- 启动速度更快,Oracle官方文档的原话:如果初始堆太小,Java 应用程序启动会变得很慢,因为 JVM 被迫频繁执行垃圾收集,直到堆增长到更合理的大小。为了获得最佳启动性能,请将初始堆大小设置为与
-XX:MaxMetaspaceSize 和 –XX:MetaspaceSize
-XX:MaxMetaspaceSize=值
- 参数指的是最大元空间大小,默认值比较大,
- 如果出现元空间内存泄漏会让操作系统可用内存不可控,建议根据测试情况设置最大值,一般设置为256m。
- 设置最大值,避免扩容,提前抛出内存不足的问题
-XX:MetaspaceSize=值
- 参数指的是到达这个值之后会触发FULLGC(网上很多文章的初始元空间大小是错误的),
- 后续什么时候再触发JVM会自行计算。如果设置为和MaxMetaspaceSize一样大,就不会FULLGC,但是对象也无法回收。
-Xss
如果我们不指定栈的大小,JVM 将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构。
比如Linux x86 64位 : 1MB,如果不需要用到这么大的栈内存,完全可以将此值调小节省内存空间,合理值为
256k – 1m之间。
使用:-Xss256k
不建议手动设置的参数
由于JVM底层设计极为复杂,一个参数的调整也许让某个接口得益,但同样有可能影响其他更多接口。
-Xmn
年轻代的大小,默认值为整个堆的1/3,可以根据峰值流量计算最大的年轻代大小,尽量让对象只存放在年轻代,不进入老年代。
但是实际的场景中,接口的响应时间、创建对象的大小、程序内部还会有一些定时任务等不确定因素都会导致这个值的大小并不能仅凭计算得出,如果设置该值要进行大量的测试。G1垃圾回收器尽量不要设置该值,G1会动态调整年轻代的大小。
‐XX:SurvivorRatio
伊甸园区和幸存者区的大小比例,默认值为8。
‐XX:MaxTenuringThreshold
最大晋升阈值(默认15),年龄大于此值之后,会进入老年代。
另外JVM有动态年龄判断机制:将年龄从小到大的对象占据的空间加起来,如果大于survivor区域的50%,然后把等于或大于该年龄的对象,放入到老年代。
幸存者区: s0和s1
其他参数 :
-XX:+DisableExplicitGC
禁止在代码中使用System.gc(), System.gc()可能会引起FULLGC,在代码中尽量不要使用。使用DisableExplicitGC参数可以禁止使用System.gc()方法调用。
-XX:+HeapDumpOnOutOfMemoryError:
发生OutOfMemoryError错误时,自动生成hprof内存快照文件。
-XX:HeapDumpPath=<path>:指定hprof文件的输出路径。
打印GC日志
JDK8及之前 : -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:文件路径
JDK9及之后 : -Xlog:gc*:file=文件路径
JVM参数模板:
-Xms1g
-Xmx1g
-Xss256k
-XX:MaxMetaspaceSize=512m
-XX:+DisableExplicitGC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/logs/my-service.hprof
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:文件路径
注意:
JDK9及之后gc日志输出修改为 -Xlog:gc*:file=文件名
堆内存大小和栈内存大小根据实际情况灵活调整
垃圾回收器的选择
背景:
小李负责的程序在高峰期遇到了性能瓶颈,团队从业务代码入手优化了多次也取得了不错的效果,这次他希望能采用更合理的垃圾回收器优化性能。
思路:
- 编写Jmeter脚本对程序进行压测,同时添加RT响应时间、每秒钟的事务数
等指标进行监控。 - 选择不同的垃圾回收器进行测试,并发量分别设置50、100、200,观察
数据的变化情况。
思路:
- 编写Jmeter脚本对程序进行压测,同时添加RT响应时间、每秒钟的事务数
等指标进行监控。 - 选择不同的垃圾回收器进行测试,并发量分别设置50、100、200,观察
数据的变化情况。 - JDK8 下 ParNew + CMS 组合(注重吞吐量) : -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
默认组合(注重响应时间) : PS + PO
JDK8使用g1 : -XX:+UseG1GC
JDK11 默认 g1
优化垃圾回收器的参数
这部分优化效果未必出色,仅当前边的一些手动无效时才考虑
一个优化的案例:
CMS的并发模式失败(concurrent mode failure)现象。由于CMS的垃圾清理线程和用户线程是并行进行的,如
果在并发清理的过程中老年代的空间不足以容纳放入老年代的对象,会产生并发模式失败。
案例
一个优化的案例:
CMS的并发模式失败(concurrent mode failure)现象。由于CMS的垃圾清理线程和用户线程是并行进行的,如果在并发清理的过程中老年代的空间不足以容纳放入老年代的对象,会产生并发模式失败。
并发模式失败会导致Java虚拟机使用Serial Old单线程进行FULLGC回收老年代,出现长时间的停顿。
解决方案:
1.减少对象的产生以及对象的晋升。
2.增加堆内存大小
3.优化垃圾回收器的参数,比如-XX:CMSInitiatingOccupancyFraction=值,当老年代大小到达该阈值时,会自动进行CMS垃圾回收,通过控制这个参数提前进行老年代的垃圾回收,减少其大小。
JDK8中默认这个参数值为 -1,根据其他几个参数计算出阈值:
((100 - MinHeapFreeRatio) + (double)(CMSTriggerRatio * MinHeapFreeRatio) / 100.0)
该参数设置完是不会生效的,必须开启-XX:+UseCMSInitiatingOccupancyOnly参数。
full gc数量并没有减少,只是降低了频率
- 属于锦上添花,治标不治本
实战: 内存调优+GC调优
背景:
小李负责的程序在高峰期经常会出现接口调用时间特别长的现象,他希望能优化程序的性能。
思路:
- 生成GC报告,通过Gceasy工具进行分析,判断是否存在GC问题或者内存问题。
- 存在内存问题,通过jmap或者arthas将堆内存快照保存下来。
- 通过MAT或者在线的heaphero工具分析内存问题的原因。
- 修复问题,并发布上线进行测试。
案例参数
java -jar
-Xms1g -Xmx1g -Xss256k -XX:MaxMetaspaceSize=256m
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=test.hprof
-XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+DisableExplicitGC
-Xloggc:test.log perform.jar
-Xms1g: 设置JVM初始堆内存大小为1GB。
-Xmx1g: 设置JVM最大堆内存大小为1GB。
-Xss256k: 设置每个线程的堆栈大小为256KB。
-XX:MaxMetaspaceSize=256m: 设置元空间(用于存放类元数据)的最大大小为256MB。
-XX:+UseParNewGC: 使用并行新生代垃圾回收器(ParNew收集器)。
-XX:+UseConcMarkSweepGC: 使用并发标记清除垃圾回收器(CMS收集器)。
-XX:+HeapDumpOnOutOfMemoryError: 当出现内存溢出错误时生成堆转储文件。
-XX:HeapDumpPath=test.hprof: 指定堆转储文件的路径为test.hprof。
-XX:+PrintGCDateStamps: 在垃圾回收日志中打印GC的日期和时间戳。
-XX:+PrintGCDetails: 打印详细的垃圾回收日志信息。
-XX:+DisableExplicitGC: 禁用显示的GC调用(如System.gc())。
-Xloggc:test.log: 将垃圾回收日志输出到文件test.log。
perform.jar: 要运行的Java程序包(JAR文件)
问题1:
发生了连续的FULL GC,堆内存1g如果没有请求的情况下,内存大小在200-300mb之间。
分析:
没有请求的情况下,内存大小并没有处于很低的情况,满足缓存对象过多的情况,怀疑内存种缓存了很多数据。需要将堆内存快照保存下来进行分析。
问题2:
堆内存快照保存到本地之后,使用MAT打开,发现只有几十兆的内存。
分析:
有大量的对象不在GC Root引用链上,可以被回收,使用MAT查看这些对象。
问题3:
由于这些对象已经不在引用链上,无法通过支配树等手段分析创建的位置。
分析:
在不可达对象列表中,除了发现大量的byte[]还发现了大量的线程,可以考虑跟踪线程的栈信息来判断对象在哪里创建。
问题产生原因:
在定时任务中通过线程创建了大量的对象,导致堆内存一直处于比较高的位置。
解决方案:
暂时先将这段代码注释掉,测试效果,由于这个服务本身的内存压力比较大,将这段定时任务移动到别的服务中。
问题4:
修复之后内存基本上处于100m左右,但是当请求发生时,依然有频繁FULL GC的发生。
分析:
请求产生的内存大小比当前最大堆内存大,尝试选择配置更高的服务器,将-Xmx和-Xms参数
调大一些。
案例总结:
1、压力比较大的服务中,尽量不要存放大量的缓存或者定时任务,会影响到服务的内存使用。
2、内存分析发现有大量线程创建时,可以使用导出线程栈来查看线程的运行情况。
3、如果请求确实创建了大量的内存超过了内存上限,只能考虑减少请求时创建的对象,或者使用更大的内存。
4、推荐使用g1垃圾回收器,并且使用较新的JDK可以获得更好的性能。
GC调优的核心流程:
1、监控是否出现连续的FULL GC或者单次GC时间过长。
2、诊断并解决,一般通过四种途径解决:
- 优化基础JVM参数
- 减少对象的产生
- 更换垃圾回收器
- 优化垃圾回收参数
3、在测试环境验证问题是否已经解决,最后发布上线
性能调优
性能优化的步骤总共分为四个步骤,其中修复部分要具体问题具体分析且处理方式各不相同。本章中着重学习发现问题和诊断问题的方法,目标是准确定位到性能问题的根源
应用程序在运行过程中经常会出现性能问题,比较常见的性能问题现象是:
1、通过top命令查看CPU占用率高,接近100甚至多核CPU下超过100都是有可能的。
应用程序在运行过程中经常会出现性能问题,比较常见的性能问题现象是:
1、通过top命令查看CPU占用率高,接近100甚至多核CPU下超过100都是有可能的。
2、请求单个服务处理时间特别长,多服务使用skywalking等监控系统来判断是哪一个环节性能低下。
应用程序在运行过程中经常会出现性能问题,比较常见的性能问题现象是:
1、通过top命令查看CPU占用率高,接近100甚至多核CPU下超过100都是有可能的。
2、请求单个服务处理时间特别长,多服务使用skywalking等监控系统来判断是哪一个环节性能低下。
3、程序启动之后运行正常,但是在运行一段时间之后无法处理任何的请求(内存和GC正常)。
线程转储方式
线程转储(Thread Dump)提供了对所有运行中的线程当前状态的快照。线程转储可以通过jstack、visualvm等工具获取。其中包含了线程名、优先级、线程ID、线程状态、线程栈信息等等内容,可以用来解决CPU占用率高、死锁等问题。
线程转储(Thread Dump)中的几个核心内容:
- 名称: 线程名称,通过给线程设置合适的名称更容易“见名知意”
- 优先级(prio):线程的优先级
- Java ID(tid):JVM中线程的唯一ID
- 本地 ID (nid):操作系统分配给线程的唯一ID
- 状态:线程的状态,分为:
NEW – 新创建的线程,尚未开始执行
RUNNABLE –正在运行或准备执行
BLOCKED – 等待获取监视器锁以进入或重新进入同步块/方法
WAITING – 等待其他线程执行特定操作,没有时间限制
TIMED_WAITING – 等待其他线程在指定时间内执行特定操作
TERMINATED – 已完成执行
- 栈追踪: 显示整个方法的栈帧信息
线程转储的可视化在线分析平台:
1、 https://jstack.review/
2、 https://fastthread.io/
Jstack命令
//查看java进程
jps
//查看线程信息保存到xxx.dump
jstack 进程号 > xxx.tdump
VisualVM
通过可视化按键生成tdump文件
案例
案例1:CPU占用率高问题的解决方案
问题:
监控人员通过prometheus的告警发现CPU占用率一直处于很高的情况,通过top命令看到是由于Java程序引起的,希望能快速定位到是哪一部分代码导致了性能问题
解决思路:
1。 通过top –c 命令找到CPU占用率高的进程,获取它的进程ID
- 使用top -p 进程ID 单独监控某个进程,按H可以查看到所有的线程以及线程对应
的CPU使用率,找到CPU使用率特别高的线程。
- 使用 jstack 进程ID
命令可以查看到所有线程正在执行的栈信息。
使用 jstack 进程ID > 文件名 保存到文件中方便查看
- 找到nid线程ID相同的栈信息,需要将之前记录下的十进制线程号转换成16进制。通过 printf ‘%x\n’ 线程ID 命令直接获得16进制下的线程ID。
- 找到栈信息对应的源代码,并分析问题产生原因
遗留问题:
如果方法中嵌套方法比较多,如何确定栈信息中哪一个方法性能较差?
案例补充:
在定位CPU占用率高的问题时,比较需要关注的是状态为RUNNABLE的线程。但实
际上,有一些线程执行本地方法时并不会消耗CPU,而只是在等待。但 JVM 仍然会
将它们标识成“RUNNABLE”状态。
案例2: 接口响应时间很长的问题
问题:
在程序运行过程中,发现有几个接口的响应时间特别长,需要快速定位到是哪一个方法的代码执行过程中出现了性能问题。
解决思路:
已经确定是某个接口性能出现了问题,但是由于方法嵌套比较深,需要借助于arthas定位到具体的方法。
Arthas的trace命令
使用arthas的trace命令,可以展示出整个方法的调用路径以及每一个方法的执行耗时。
命令: trace 类名 方法名
- 添加 --skipJDKMethod false 参数可以输出JDK核心包中的方法及耗时。
- 添加 ‘#cost > 毫秒值’ 参数,只会显示耗时超过该毫秒值的调用。
- 添加 –n 数值 参数,最多显示该数值条数的数据。
- 所有监控都结束之后,输入stop结束监控,重置arthas增强的对象。
Arthas的watch命令
在使用trace定位到性能较低的方法之后,使用watch命令监控该方法,可以获得更为详细的方法信息。
命令: watch 类名 方法名 ‘{params, returnObj}’ ‘#cost>毫秒值’ -x 2
- ‘{params, returnObj}‘ 代表打印参数和返回值。
- -x 代表打印的结果中如果有嵌套(比如对象里有属性),最多只展开2层。允许设置的最大值为4。
总结:
-
通过arthas的trace命令,首先找到性能较差的具体方法,如果访问量比较大,建议设置最小的耗时,精确的找到耗时比较高的调用。
-
通过watch命令,查看此调用的参数和返回值,重点是参数,这样就可以在开发环境或者测试环境模拟类似的现象,通过debug找到具体的问题根源。
-
使用stop命令将所有增强的对象恢复。
案例3: 定位偏底层的性能问题
问题:
有一个接口中使用了for循环向ArrayList中添加数据,但是最终发现执行时间比较长,需要定位是由于什么原因导致的性能低下。
解决思路:
Arthas提供了性能火焰图的功能,可以非常直观地显示所有方法中哪些方法执行时间比较长。
Arthas的profile命令
使用arthas的profile命令,生成性能监控的火焰图。
命令1: profiler start 开始监控方法执行性能
命令2: profiler stop --format html 以HTML的方式生成火焰图
火焰图中一般找绿色部分Java中栈顶上比较平的部分,很可能就是性能的瓶颈。
总结:
- 偏底层的性能问题,特别是由于JDK中某些方法被大量调用导致的性能低下,可以使用火焰图非常直观的找到原因。
- 这个案例中是由于创建ArrayList时没有手动指定容量,导致使用默认的容量而在添加对象过程中发生了多次的扩容,扩容需要将原来数组中的元素复制到新的数组中,消耗了大量的时间。
- 通过火焰图可以看到大量的调用,修复完之后节省了20% ~ 50%的时间。
案例4:线程被耗尽问题
问题:
程序在启动运行一段时间之后,就无法接受任何请求了。将程序重启之后继续运行,依然会出现相同的情况。
解决思路:
线程耗尽问题,一般是由于执行时间过长,分析方法分成两步:
- 检测是否有死锁产生,无法自动解除的死锁会将线程永远阻塞。
- 如果没有死锁,再使用案例1的打印线程栈的方法检测线程正在执行哪个方法,一般这些大
量出现的方法就是慢方法。
死锁:两个或以上的线程因为争夺资源而造成互相等待的现象
死锁问题,黑马《JUC并发编程》相关章节。
地址 : https://www.bilibili.com/video/BV16J411h7Rd?p=115
解决方案:
线程死锁可以通过三种方法定位问题:
- jstack -l 进程ID > 文件名 将线程栈保存到本地。
在文件中搜索deadlock即可找到死锁位置:
- 开发环境中使用visual vm或者Jconsole工具,都可以检测出死锁。使用线程快照生成工具
就可以看到死锁的根源。生产环境的服务一般不会允许使用这两种工具连接。
- 使用fastthread自动检测线程问题。 https://fastthread.io/
Fastthread和Gceasy类似,是一款在线的AI自动线程问题检测工具,可以提供线程分析报告。
通过报告查看是否存在死锁问题。
更精细的性能测试
你是如何判断一个方法需要耗时多少时间的?
- 我会在方法上打印开始时间和结束时间,他们的差值就是方法的执行耗时。手动通过postman或者jmeter发起一笔请求,在控制台上看输出的时间。
- 这样做是不准确的,第一测试时有些对象创建是懒加载的,所以会影响第一次的请求时间,第二因为虚拟机中JIT即时编译器会优化你的代码,所以你这个测试得出的时间并不一定是最终用户处理的时间。
JIT对程序性能的影响
Java程序在运行过程中,JIT即时编译器会实时对代码进行性能优化,所以仅凭少量的测试是无法真实反应运行系统最终给用户提供的性能。如下图,随着执行次数的增加,程序性能会逐渐优化。
正确地测试代码性能
OpenJDK中提供了一款叫JMH(Java Microbenchmark Harness)的工具,可以准确地对Java代码进行基准测试,量化方法的执行性能。
官网地址:https://github.com/openjdk/jmh
JMH会首先执行预热过程,确保JIT对代码进行优化之后再进行真正的迭代测试,最后输出测试的结果。
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。
- 编写测试方法,几个需要注意的点:
- 死代码问题
- 黑洞的用法
- 通过maven的verify命令,检测代码问题并打包成jar包。
通过java -jar target/benchmarks.jar 命令执行基准测试
java -jar target/benchmarks.jar
- 测试结果通过https://jmh.morethan.io/生成可视化的结果
案例:日期格式化方法性能测试
问题:
在JDK8中,可以使用Date进行日期的格式化,也可以使用LocalDateTime进行格式
化,使用JMH对比这两种格式化的性能
解决思路:
1、搭建JMH测试环境。
2、编写JMH测试代码。
3、进行测试。
4、比对测试结果。
总结:日期格式化方法性能测试
1、Date对象使用的SimpleDateFormatter是线程不安全的,所以每次需要重新创建对象或者将对象放入ThreadLocal中进行保存。其中每次重新创建对象性能比较差,将对象放入ThreadLocal之后性能相对还是比较好的。
2、LocalDateTime对象使用的DateTimeFormatter线程安全,并且性能较好,如果能将DateTimeFormatter对象保存下来,性能可以得到进一步的提升。
性能调优综合实战
问题:
小李的项目中有一个获取用户信息的接口性能比较差,他希望能对这个接口在代码中进行彻底的优化,提升性能。
解决思路:
1、使用trace分析性能瓶颈。
2、优化代码,反复使用trace测试性能提升的情况。
3、使用JMH在SpringBoot环境中进行测试。
4、比对测试结果。
10000数据量下,平均执行时间从原来的217ms优化至2ms。
100000数据量下,平均执行时间从原来的57秒优化至18ms。
总结
-
本案例中性能问题产生的原因是两层for循环导致的循环次数过多,处理时间在循环次数变大的情况下变得非常长,考虑将一层循环拆出去,创建HashMap用来查询提升性能。
-
使用LocalDateTime替代SimpleDateFormat进行日期的格式化。
-
使用stream流改造代码,这一步可能会导致性能下降,主要是为了第四次优化准备。
-
使用并行流利用多核CPU的优势并行执行提升性能。
总结
问题1:CPU占用率高的问题
解决方案:
通过top命令找到进程和线程 -> 使用jstack打印线程快照 -> 找到线程快照正在执行的方法 ,并优化性能。
问题2:接口响应时间特别长
解决方案1:
通过arthas的trace和watch命令,监控方法的执行耗时和参数、返回值等信息,定位性能瓶颈,并优化性能。
解决方案2:
通过arthas的profile火焰图功能,找到火焰图中顶部较平的方法,一般就是性能问题产生的根源,并优化性能。
问题3:线程不可用问题
解决方案:
通过jstack 、visualvm 、fastthread.io等工具,找到线程死锁的原因,解决死锁问题。
如何判断一个方法需要耗时多少时间的?
我们使用了OpenJDK中的jmh基准测试框架对某些特定的方法比如加密算法进行基准测试,jmh
可以完全模拟运行环境中的Java虚拟机参数,同时支持预热能通过JIT执行优化后的代码获得更为
准确的数据。