SpringBoot 服务进行[性能优化]之前,需要做一些准备,把 SpringBoot 服务的一些数据暴露出来。 比如,需要把缓存命中率收集;数据库连接池的参数收集,通常需要用到JVM 的分析和诊断工具,比如:arthus、Jprofiler、jvisualvm、jmap\jstat
分析工具:JDK
arthas: 开源的 Java 诊断工具
Alibaba 在 2018 年 9 月开源的 Java 诊断工具。
**快速退出某个命令**:Q或者Ctrl+C
**退出Arthas:** exit或者quit, 退出当前session,Arthas server还在目标进程中运行。
**彻底退出**: stop. 用完一定要stop哦,避免Arthas server依然运行占用系统资源。
- 下载地址 及启动
curl -O https://alibaba.github.io/arthas/arthas-boot.jar
等价于:wget https://alibaba.github.io/arthas/arthas-boot.jar
启动
java -jar arthas-boot.jar
全局视角查看系统运行状况 Dashboard
CPU飙升原因定位
内存泄漏原因定位
直接用heapdump命令:heapdump --live /root/jvm.hprof 把内存快照dump出来,作用和jmap工具一样(jmap -dump:live)
多线程问题(死锁,阻塞) thread
通过thread加线程id输出该线程的栈信息 thread -n 3 查看CPU使用率top n线程的栈: thread -b 找出当前阻塞其他线程的线程
//线程
thread -n 5
jvm
[tomcat@iZ2zeb4tt2ppjt9ok0779rZ soft]$ jstat -gcutil 1 1000 5
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
59.80 0.00 42.26 39.52 96.39 93.89 92 3.766 5 3.758 7.524
59.80 0.00 42.70 39.52 96.39 93.89 92 3.766 5 3.758 7.524
59.80 0.00 43.11 39.52 96.39 93.89 92 3.766 5 3.758 7.524
59.80 0.00 43.13 39.52 96.39 93.89 92 3.766 5 3.758 7.524
59.80 0.00 43.53 39.52 96.39 93.89 92 3.766 5 3.758 7.524
[tomcat@iZ2zeb4tt2ppjt9ok0779rZ soft]$ jstack 1 >./dump01
[tomcat@iZ2zeb4tt2ppjt9ok0779rZ soft]$ jmap -heap 1
Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11
using thread-local object allocation.
Parallel GC with 2 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 2147483648 (2048.0MB)
NewSize = 178782208 (170.5MB)
MaxNewSize = 715653120 (682.5MB)
OldSize = 358088704 (341.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 585105408 (558.0MB)
used = 314888272 (300.3008575439453MB)
free = 270217136 (257.6991424560547MB)
53.8173579827859% used
From Space:
capacity = 63438848 (60.5MB)
used = 37695096 (35.94884490966797MB)
free = 25743752 (24.55115509033203MB)
59.41957836308755% used
To Space:
capacity = 62914560 (60.0MB)
used = 0 (0.0MB)
free = 62914560 (60.0MB)
0.0% used
PS Old Generation
capacity = 1046478848 (998.0MB)
used = 413602744 (394.4423141479492MB)
free = 632876104 (603.5576858520508MB)
39.523277970736395% used
程序运行耗时监测 (没记日志的情况下:定位哪里运行时长最长) trace命令
trace com.xxxx.controller.DubboCaseController getList '#cost > 2'
[arthas@1]$ trace com.better517na.cLReportService.controller.SettlementPublicController [arthas@1]$ trace com.better517na.cLReportService.controller.SettlementPublicController queryPersonConsumptionsFromESApply '#cost > 2'
无法线上DEBUG,缺少日志情况下的问题定位
- trace命令
- watch命令:查看指定方法的调用情况
watch com.xxxx.xxxxController update “{params,returnObj}” -x 3 -b -s,查看xxxxController的update方法的返回值:
-x 3是指定输出结果的属性遍历深度,默认为 1
-b方法调用前观察,用于返回方法入参
-s方法调用后观察,用于返回方法返回值
异常
- monitor命令:监控方法的执行情况
包括:成功次数、失败次数、平均响应时间、失败率
monitor -c 10 com.xxxx.xxxxController update
- TimeTunnel 记录下方法执行数据的时空隧道
tt -i 1003 -p 表示重做Index为1003的那次调用
- stack命令:监控方法的被执行的路径
stack 命令, 主要用于监控方法被谁调用了: stack com.xxxx.xxxxController list
- sm命令:能搜索出所有已经加载了 Class 信息的方法信息
sm -d com.xxxx.xxxxController
- jad命令:反编译指定已加载类的源码
反编译源码到指定文件:
jad --source-only com.xxxx.xxxxController > /tmp/xxxxController.java
- logger命令:实现动态更新logger level
使用sc命令查看你需要改变的类信息,关注classLoaderHash
sc -d com.xxxx.xxxxController logger -c 70ac4376 查看当前的日志级别
将日志级别改为info: logger -c 70ac4376 --name com.xxxx.xxxx --level info
Jprofiler 性能分析神器JProfiler
CPU分析
内存分析
线程分析
I/O分析
生成快照
jvisualvm : JAVA 自带诊断工具
概念
JVisualVM是一个集成了命令行JDK工具和轻量级分析功能的可视化工具,专为开发和生产环境使用而设计。 官方地址:VisualVM.github.io/ JVisualVM已作为Java JVisualVM在Oracle JDK 6~8中分发。它已在Oracle JDK9中停止。 独立的VisualVM可以从官网下载,可以在任何兼容的JDK版本上运行。
VisualVM 是[Netbeans]的profile子项目,已在JDK6.0 update 7 中自带,能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。
VisualVM 提供了一个可视界面,用于查看 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的基于 Java 技术的应用程序的详细信息。 VisualVM 对 Java Development Kit (JDK) 工具所检索的 JVM 软件相关数据进行组织,并通过一种使您可以快速查看有关多个 Java 应用程序的数据的方式提供该信息。 您可以查看本地应用程序或远程主机上运行的应用程序的相关数据
VisualVM非常多的其它功能,可以分析dump的内存快照,
dump出来的线程快照并且进行分析等,还有其它很多的插件大家可以去探索
jmap.exe
jmap命令是Jdk自带的一个,查看jvm内存使用详情的命令
jmap -histo 17 | head -n 30
jmap -histo 17 | sort -k 2 -g -r
当前进程中对象的大小及个数,辅助进行分析
jmap -histo pid|head -n 10 查看前10位
jmap -histo pid | sort -k 2 -g -r 查看对象数最多的对象,按降序输出
jmap -histo pid | sort -k 3 -g -r 查看内存的对象,按降序输出
$ jmap --help
Usage:
jmap [option] <pid>
(to connect to running process)
jmap [option] <executable <core>
(to connect to a core file)
jmap [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server)
where <option> is one of:
<none> to print same info as Solaris pmap
-heap to print java heap summary
-histo[:live] to print histogram of java object heap; if the "live"
suboption is specified, only count live objects
-clstats to print class loader statistics
-finalizerinfo to print information on objects awaiting finalization
-dump:<dump-options> to dump java heap in hprof binary format
dump-options:
live dump only live objects; if not specified,
all objects in the heap are dumped.
format=b binary format
file=<file> dump heap to <file>
Example: jmap -dump:live,format=b,file=heap.bin <pid>
-F force. Use with -dump:<dump-options> <pid> or -histo
to force a heap dump or histogram when <pid> does not
respond. The "live" suboption is not supported
in this mode.
-h | -help to print this help message
-J<flag> to pass <flag> directly to the runtime system
histogram 英 / ˈhɪstəɡræm 美 / [ˈhɪstəɡræm] n.(统计学的)直方图,矩形图
jstat
jstat -gcutil 1 3000 10
jstat -gcutil 垃圾收集统计信息摘要
-
列名 描述
-
S0 幸存区Survior S0利用率占空间当前容量的百分比
-
S1 幸存区Survior S1利用率占空间当前容量的百分比
-
E Eden区利用率占空间当前容量的百分比
-
O Old老年代利用率占空间当前容量的百分比
-
M Metaspace元空间利用率占空间当前容量的百分比
-
CCS 以百分比形式压缩的类空间利用率
-
YGC 年轻代 GC 事件的数量
-
YGCT 年轻代垃圾回收时间
-
FGCT 老年代GC时间
-
GCT 总垃圾回收时间
JVM 调优
-Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m -XX:MaxDirectMemorySize=1g -XX:SurvivorRatio=10
-XX:+UseConcMarkSweepGC -XX:CMSMaxAbortablePrecleanTime=5000
-XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=80
-XX:+UseCMSInitiatingOccupancyOnly -XX:+ExplicitGCInvokesConcurrent
-Dsun.rmi.dgc.server.gcInterval=2592000000 -Dsun.rmi.dgc.client.gcInterval=2592000000
-XX:ParallelGCThreads=2 -Dsun.net.client.defaultConnectTimeout=10000
-Dsun.net.client.defaultReadTimeout=30000
代码优化
Controller @ResponseBody 大结果集不仅会影响解析时间,还会造成内存浪费。
假如结果集在解析成 JSON 之前,占用的内存是 10MB,那么在解析过程中,有可能会使用 20M 或者更多的内存去做这个工作。 很多案例,由于返回对象的嵌套层次太深、引用了不该引用的对象(比如非常大的 byte[] 对象),造成了内存使用的飙升。保持结果集的精简,非常有必要,这是 DTO(data transfer object)存在原因之一。
Service 层
service 层用于处理具体的业务,大部分功能需求都是在这里完成的。service 层一般是使用单例模式,很少会保存状态,而且可以被 controller 复用。
service 层的代码组织,对代码的可读性、性能影响都比较大。设计模式,大多数是针对 service 层。
对象用完置null : clear to let GC do its work
public static void main(String[] args) {
EntClearingInfoVo entClearingInfoBo = new EntClearingInfoVo();
entClearingInfoBo.setEnterpriseNum("test1");
Map<Integer, List<StaffSettingInfoBo>> productStaffLists = new HashMap<>();\
List<StaffSettingInfoBo> list = new ArrayList<>();
StaffSettingInfoBo staffSettingInfoBo = new StaffSettingInfoBo();
staffSettingInfoBo.setStaffID("test2");
list.add(staffSettingInfoBo);
productStaffLists.put(0,list);
entClearingInfoBo.setProductStaffLists(productStaffLists);
System.out.println(GsonUtil.getGson().toJson(entClearingInfoBo));
System.out.println(GsonUtil.getGson().toJson(productStaffLists));
productStaffLists = null;// clear to let GC do its work
System.out.println(GsonUtil.getGson().toJson(productStaffLists));
}
e.printStackTrace()
1)内存占用问题
e.printStackTrace() 将异常打印到控制台时,会将产生错误堆栈信息存入字符串常量池中,如果在常量池空间较小且异常较多时,常量池空间可能会被异常信息占满, 这样其他需要使用或者正在使用此空间的线程就会产生阻塞现象,甚至最终抛出 OOM,导致整个应用挂掉。
2)性能问题
如下代码中的synchronized关键字告诉我们e.printStackTrace()执行时会有并发锁,如果异常代码频繁被调用时,e.printStackTrace()的性能会下降。
3) 实际使用场景
优化:e.printStackTrace() ,存在性能问题;
介绍:打印 Java 异常的调用栈,即Exception + Trace
性能问题:
1. java中,e.printStackTrace() 会产生大量字符串的方法, 占用很多非堆内存;
2. 短时间内大量请求访问异常接口,e.printStackTrace() 导致内存被占满,其他线程处于相互等待,相互等待内存,会产生线程死锁,等线程耗尽,整个服务就会被打挂。
优化方案:
方案一:不使用 e.printStackTrace() ,干掉服务中的这行代码
方案二:通过环境标识打印e.printStackTrace(),生产环境这行代码没有意义,测试开发环境可以输出。
方案三:jvm进行了优化,如果出现非常频繁打印同一个堆栈信息的情况,后续将不会再打印堆栈信息了 (k8s JAVA_OPTS 配置JVM参数 -XX:+OmitStackTraceInFastThrow)。
{"name":"JAVA_OPTS","value":"-Xms6g
-Xmx6g -Xmn2g -XX:MetaspaceSize=256m -XX:MaxDirectMemorySize=1g
-XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC
-XX:CMSMaxAbortablePrecleanTime=5000
-XX:+CMSClassUnloadingEnabled
-XX:CMSInitiatingOccupancyFraction=80
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+ExplicitGCInvokesConcurrent
-Dsun.rmi.dgc.server.gcInterval=2592000000
-Dsun.rmi.dgc.client.gcInterval=2592000000 -XX:ParallelGCThreads=2
-Dsun.net.client.defaultConnectTimeout=10000
-Dsun.net.client.defaultReadTimeout=30000
-Dsentinel.remoteAddress=sentinel.517nacos.com:343434"}
JAVA_OPTS="$JAVA_OPTS
-XX:ParallelGCThreads=4
-XX:MaxTenuringThreshold=9
-XX:+DisableExplicitGC
-XX:+ScavengeBeforeFullGC
-XX:SoftRefLRUPolicyMSPerMB=0
-XX:ParallelGCThreads=2 定义CMS过程并行收集的线程数。
-XX:+CMSClassUnloadingEnabled 相对于并行收集器,CMS收集器默认不会对永久代进行垃圾回收。如果希望对永久代进行垃圾回收,可用设置-XX:+CMSClassUnloadingEnabled。默认关闭
-XX:+ExplicitGCInvokesConcurrent 做System.gc()时会做background模式CMSGC,即并行FULLGC,可提高FULLGC效率,注意:该参数在允许systemGC且使用CMSGC时有效
-XX:+PrintGCDetails 详细的查看 GC 的回收操作,一般会将 GC 的输出,单独单到一个 log 文件当中进行查看
-XX:+HeapDumpOnOutOfMemoryError 发生OOM时,自动生成DUMP 文件
-XX:-OmitStackTraceInFastThrow 省略异常栈信息从而快速抛出
-Duser.timezone=Asia/Shanghai
-Dclient.encoding.override=UTF-8
-Dfile.encoding=UTF-8
-Djava.security.egd=file:/dev/./urandom"
参考:zhuanlan.zhihu.com/p/437612041
4) 源码
private void printStackTrace(PrintStreamOrWriter s) {
// Guard against malicious overrides of Throwable.equals by
// using a Set with identity equality semantics.
Set<Throwable> dejaVu =
Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
dejaVu.add(this);
synchronized (s.lock()) {
// Print our stack trace
s.println(this);
StackTraceElement[] trace = getOurStackTrace();
for (StackTraceElement traceElement : trace)
s.println("\tat " + traceElement);
// Print suppressed exceptions, if any
for (Throwable se : getSuppressed())
se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);
// Print cause, if any
Throwable ourCause = getCause();
if (ourCause != null)
ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
}
}
## select * from table 性能问题定位
现象CUP 100% ,内存也很高,所有请求耗时变慢,应用网络流量接受率很高(从数据库 输出到 应用)
查询数据库慢SQL
thread -n 10
BeanUtil.copyProperties 存在性能消耗
List集合在开发过程中运用的频率相当高
List list1 = new ArrayList(); list1.add(1); list1.add(2); list1.add(3);
Integer[] array1= {1,2,3}; List list2 = Arrays.asList(array1);
springboot中,@Component, @Service,@Controller默认都是单例(singleton),prototype是每次调用都会new一个新的对象。
线程池
public String doSimplePost(String url, String params, String charset) {
if (StringUtils.isBlank(url)) {
return null;
}
CloseableHttpResponse response = null;
try {
HttpEntity httpEntity = null;
if (params != null && !params.isEmpty()) {
httpEntity = new StringEntity(params, charset);
}
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("content-type", "application/json");
if (this.headers != null && this.headers.size() > 0) {
for (Header header : headers) {
httpPost.addHeader(header);
}
}
if (httpEntity != null && httpEntity.getContentLength() > 0) {
httpPost.setEntity(httpEntity);
}
response = httpClinet.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
httpPost.abort();
if (statusCode == 401) {
return "授权错误";
}
throw new RuntimeException("HttpClient,error status code :" + statusCode);
}
HttpEntity entity = response.getEntity();
String result = null;
if (entity != null) {
result = EntityUtils.toString(entity, charset);
}
EntityUtils.consume(entity);
response.close();
return result;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
** if(null != client) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}**
}
return null;
}
## http 通信用完要关闭, CloseableHttpClient 用完就要去关闭。线程用完要shuntdown
```
if(null != client) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
SpringBoot 服务超时配置,尽可能小于60s ,大于这个专门放行改接口
feign中的配置的readTimeout和connectTimeout会覆盖ribbon中的ConnectTimeout和ReadTimeout; ribbon.ConnectTimeout=150000 ribbon.ReadTimeout=150000 feign.client.config.default.connectTimeout=120000 feign.client.config.default.readTimeout=150000
server.tomcat.max-threads=800 ,4核8g内存 核数*200,线程数800; 8c16g ,8*200=1600