8.1 jvm资源监控工具
8.1.1jconsole监控工具
jmap:此工具在jdk安装目录的bin文件夹里面
jmap [option]<pid>
例如:jmap -heap 6033
MaxHeapSize =573870912(512.0MB)
jconsole【被取代了】:此工具在jdk安装目录的bin文件夹里面,运行后界面
可以查看内存、线程、类、jvm等相关信息
本地进程访问,选择对应的java程序进程即可
远程访问需要服务器开放对应的端口,启动时,添加一下对应的配置
-Dcom.sum.management.jmxremote
-Djava.rmi.server.hostname=192.168.1.15 这个ip写用于访问的ip,要确定你的jconsole客户端能够通过这个ip访问到服务器
-Dcom.sun.management.jmxremote.port=8999 给jconsole连接的端口
-Dcom.sun.management.jmxremote.rmi.port=9999
-Dcom.sun.management.jmxremote.ssl=false 取消ssl加密
-Dcom.sun.management.jmxremote.autheticate=false 取消用户名密码验证
服务器启动项目时,将这些参数加进去,启动命令为:
java -Xmx2G -Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8999
-Dcom.sun.management.jmxremote.rmi.port=9999
-Djava.rmi.server.hostname=192.168.1.5
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-jar -Dspring.datasource.url="jdbc:mysql://192.168.1.7:3306/novel-plus?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true" -Dspring.datasource.username=root -Dspring.datasource.password=zhaoWEILI1314520@ novel-front-3.5.4.jar
在客户端jdk安装目录下的 bin目录,打开jconsole
注意:连不上,可以关闭防火墙,前面介绍别的工具安装,配置防火墙,让防火墙放行单个端口的设置去操作,需要将8998端口和9999端口一起给它配置了
firewall-cmd --add-port=8999/tcp --permannet
firewall-cmd --add-port=9999/tcp --permannet
Firewall-cmd --reload #重载防火墙
建议直接关闭防火墙 systemctl stop firewalld
对服务器进行压测,检测在测试过程数据的变成情况
8.1.2 jvisualvm监控工具(取代了jsconsole)
jvisualvm功能比jconsole强大
官网下载地址:https://visualvm.github.io/releases.html
解压后,在bin目录中 找到.exe文件,打开它,创建远程连接
这个工具和jconsole相关,服务器启动项目时,将jscnsole这些参数加进去;
添加jmx,增加端口号8999
接下来就可以查看远程主机的一些信息
被监控服务器上启动jstatd,jvm jstatd Daemon守护进程,一个RMI(Remote Method Invocation)服务器程序,用于监控本地所有jvm从创建开始,知道销毁整个过程中的资源使用情况,同时 提供接口给监控工具 (如这里的VisualVM)让工具能连接到本地所有的jvm
该命令在jdk的bin目录下:
创建安全策略文件:在jdk的bin目录下新建 jstatd.all.policy的文件
内容如下:
grant codebase "file:${java.home}/../lib/tools.jar" {
permission java.security.AllPermission; };
注意:服务器记得配置JAVA_HOME环境变量
启动:
jstatd -J-Djava.security.policy=/usr/local/jdk/bin/jstatd.all.policy -J-Djava.rmi.server.hostname=192.168.1.5
添加jstatd
注意:jvisualvm此功能要求服务器启动程序时和jconsole一样的请求参数
特别注明:jdk1.8,在bin目录,自带中文版的该工具,在jdk的bin目录下
在测试过程中可以通过抽样器对测试进行过程进行分析,找到最耗费cpu和内存的方法,从而协助开发有针对性的对代码进行优化,而不是优化所有代码。
8.1.3 jvm集群监控体系
基于prometheus+grafana
官网下载地址:/prometheus/jmx_exporter
将jar包传到java应用服务器上,在jar同级目录下创建config.yaml
内容为(通用配置):
---
lowercaseOutputLabelNames: true
lowercaseOutputName: true
whitelistObjectNames: ["java.lang:type=OperatingSystem", "Tomcat:*"]
blacklistObjectNames: []
rules:
- pattern: 'java.lang<type=OperatingSystem><>(committed_virtual_memory|free_physical_memory|free_swap_space|total_physical_memory|total_swap_space)_size:'
name: os_$1_bytes
type: GAUGE
attrNameSnakeCase: true
- pattern: 'java.lang<type=OperatingSystem><>((?!process_cpu_time)\w+):'
name: os_$1
type: GAUGE
attrNameSnakeCase: true
- pattern: 'Tomcat<type=Server><>: (.+)'
name: tomcat_serverinfo
value: 1
labels:
serverInfo: "SpringBoot Tomcat"
type: COUNTER
- pattern: 'Tomcat<type=GlobalRequestProcessor, name=\"(\w+-\w+)-(\d+)\"><>(\w+):'
name: tomcat_$3_total
labels:
port: "$2"
protocol: "$1"
help: Tomcat global $3
type: COUNTER
- pattern: 'Tomcat<j2eeType=Servlet, WebModule=//([-a-zA-Z0-9+&@#/%?=~_|!:.,;]*[-a-zA-Z0-9+&@#/%=~_|]), name=([-a-zA-Z0-9+/$%~_-|!.]*), J2EEApplication=none, J2EEServer=none><>(requestCount|processingTime|errorCount):'
name: tomcat_servlet_$3_total
labels:
module: "$1"
servlet: "$2"
help: Tomcat servlet $3 total
type: COUNTER
- pattern: 'Tomcat<type=ThreadPool, name="(\w+-\w+)-(\d+)"><>(currentThreadCount|currentThreadsBusy|keepAliveCount|connectionCount|acceptCount|acceptorThreadCount|pollerThreadCount|maxThreads|minSpareThreads):'
name: tomcat_threadpool_$3
labels:
port: "$2"
protocol: "$1"
help: Tomcat threadpool $3
type: GAUGE
- pattern: 'Tomcat<type=Manager, host=([-a-zA-Z0-9+&@#/%?=~_|!:.,;]*[-a-zA-Z0-9+&@#/%=~_|]), context=([-a-zA-Z0-9+/$%~_-|!.]*)><>(processingTime|sessionCounter|rejectedSessions|expiredSessions):'
name: tomcat_session_$3_total
labels:
context: "$2"
host: "$1"
help: Tomcat session $3 total
type: COUNTER
程序启动时添加对应的agent和配置:
java -javaagent:./jmx_prometheus_javaagent-0.18.0.jar=12345:config.yaml -jar
-Dspring.datasource.url="jdbc:mysql://192.168.1.7:3306/novel-plus?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true" -Dspring.datasource.username=root -Dspring.datasource.password=zhaoWEILI1314520@ novel-front-3.5.4.jar
java程序启动后,会开放一个12345的端口.通过http去访问这个端口,可以获取jvm的运行状态数据,配置防火墙策略:
firewall-cmd --add-port=12345/tcp --permanent
firewall-cmd reload
接下来:Prometheus配置数据采集,修改yml配置文件,添加监控,配置后重新启动
单独创建了一个任务--jvms,这个工具就会自动的调节前面启动监控数据接口,从而获取jvm运行时的数据
Grafanap配置监控大屏:
在garfana官网下载监控jvm,然后导入json文件,前面有介绍 ,就不写了,
注意点,jvm在导入时,需要主要job名称,要跟prometheus.yml文件中配置的一致,不然会没有数据
对服务器进行压测,使用jvm监控看板,查看jvm资源使用情况
实战演示:启动java程序时,设置堆内存大小为200M,对服务器进行压测
记录测试时间点:-03-21 13:56:23 --03-21 13:58:09
其中吞吐量最大为:3.39k,平均3.7k,最小223.20
在这个时间段内,java应用服务器的cpu使用率最大为56.81%,内存20.45%
数据库服务器cpu的使用率最大为1.61%,内存40.04%
总结:cpu,内存,磁盘,网络资源使用率不高,程序出现瓶颈,最大吞吐量3.39k,应为程序的问题,查看jvm信息监控看板,jvm,cpu最高为45%,堆内存61.4%,jvm不存在明显的
某个资源使用率过高问题,相对并发数量设置过小,加大并发数,继续排查问题。
这里将堆内存设置为50M,对比看一下,设置前
将堆内存调增为50M后,使用同样的脚本对服务器进行压测,堆内存基本上满了
8.2GC垃圾回收
8.2.1自动垃圾收集机制
内存不是无穷无尽的,每一个请求处理过程中,都会创建一些对象,占用一些内存。请求
处理完毕,占用内存应该被释放。JVM内置垃圾回收机制,JVM程序里面有一个模块的垃圾回收器。
java垃圾回收过程:标记垃圾,清除垃圾,整理内存
8.2.2分代回收机制
新生代-eden --Xmn
新生代-survivor 1
新生带-survivor 2
老年代-XX:pretenureSizeTHreshold=1024 升级到老年代的对象定义
两大区域--
小范围GC,大范围Full GC 两种
新生代-新对象新数据
老年代-超级大的对象,经历多次新生代GC,依然存在的对象
特殊--永久代【元数据空间】
8.2.3 STW概念(stop the world)
和性能有关:一个请求--处理实际
业务代码执行时间+ GC垃圾回收时间
JVM在GC的时候,业务代码线程,处于停止等待的一个状态,就好比打扫垃圾的时候,总不能边扔边打扫,如果是这样的情况,那么业务代码在执行的过程,就会出现,代码还没执行完毕,数据被当作垃圾清理掉了,找到数据,程序运行就会出错。
8.2.3 minor gc小范围GC
新生代--时间一般比较短
8.2.4 full gc大范围GC
所有的堆空间、元数据空间
导致程序中断的时间比较长
8.2.5垃圾回收器
串行GC:基本不用
并行GC:PS
并发GC:cms G1
实战1:StackOverflowError -栈内存溢出
线程栈内存默认大小1M:每个线程都会分配一个小内存区块,保存线程执行的方法、局部变量的信息。
建议代码优化,:出现场景--递归调用--递归--方法嵌套调用自己
栈--数据结构--后进先出--每一次执行方法之前,把方法信息,压入栈,从上往下执行
递归:方法调用没完毕,又调用自身……,如果长时间不结束,导致栈内存不够
确实有大量递归场景,尝试修改配置,增加JVM每个线程栈内存的大小
-Xss参数:java每个线程的stack大小
jdk5.0以后,每个线程堆栈大小为1M,以前为256K
在相同的物理内存下,减少这个值能生成更多的线程
物内存是固定,增大栈内存后,可运行的线程数变少
实战2:OutOfMemoryError-内存溢出
持久性压力测试,程序内存占用逐步增高,最终挂掉,突然大量并发,内存不够i用
java.lang.OutOfMemoryError:unable to create new native thread:java程序已经达到了它可以启动的线程数的限制
java.lang.OutOfMemoryError:Metaspace
1.8+java.lang.OutOfMemoryError:permGen space
-XX:MaxPermSize=512m
加大元数据空间的内存,或检查元数据空间占是否合理,class类加载的太多,动态创建了很多class类
java.lang.OutOfMemoryError:java heap space
Java.lang.OutOfMemoryError:GC overhead limit exceeded
加大堆内存,或者检查内存占用是否合理
程序调优-获取内存快照-占到占用比较大的对象,让开发修改
JVM在遇见OOM(OutOfMemoryError)时生成内存快照文件:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp
增加这些JVM启动参数
在启动项目时,调整项目的栈内存为2M,堆内存为200M,对服务器进行压测,内存一旦溢出就会保存文件到reading/test目录下
java -Xss2m -Xmx200m
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/reading/test
-javaagent:./jmx_prometheus_javaagent-0.18.0.jar=12345:config.yaml -jar
-Dspring.datasource.url="jdbc:mysql://192.168.1.7:3306/novel-plus?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true" -Dspring.datasource.username=root -Dspring.datasource.password=zhaoWEILI1314520@ novel-front-3.5.4.jar
此时执行压测脚本,在对应目录下,创建对应的java文件
内存快照分析:
工具MAT -eclipse Memory Analyzer Tools
下载地址:/mat/downloads.php
下载一个高版本的jdk,我这里下载了1个19的
修改配置文件:MemoryAnalyzer.ini,增加一行
-vm
E:\jdk-19\bin\javaw.exe通过mat打开内存快照 #这个为更改版本jdk的javaw,exe的安装位置
使用该工具,打开从服务器上下载的java_pid2818.hprof文件
首先打开Histogram(直方图),查看每个对象占用了多少内存,但具体是那段代码占用不知道,直方图中,右键,list object菜单中,查看引用对象
with incoming references 表示的是当前查看的对象,被外部应用
with outGoing references 表示的是当前对象,引用了外部对象
接下来看dominator tree(支配树)
当在支配树视图中选中某一对象时,我们还可以通过Path To GC Roots 功能,反向列出该对象到 GC Roots 的引用路径
到了这个程度,开发还找不到,在Reports分析中,Leak Suspects,去找可疑点,点击details
8.3性能稳定性调优
实战:性能测试过程中,吞吐量呈现出波浪状,性能稳定性调优
疑因分析:JVM垃圾回收,导致STW产生时间停顿
波浪小:可能是小规模GC比较多,或者每次GC时间补偿,能接收就行
波浪起伏大,GC时间长,可能是产生了多次Full gc,调整垃圾回收器参数,减少停顿时间
最优先的是让开发从编码的角度去优化代码,减少内存占用、复用内存,90%的情况下,不需要做大量所谓垃圾回收的调整
我们对服务器进行压测
8.3.1垃圾回收器优化思路
并发量不变,垃圾不会减少
垃圾不减少,那就提高垃圾回收的次数,每次少清理一些垃圾,时间则会减少
8.3.2GC是否需要优化
响应时间角度:要求:所有用户请求必须在1000ms内完成,对于响应时间的要求,根据经验来说,我们要求GC占用的时间不超过10%,为了方便理解,假定一次请求最多经过一次GC。也就是一个请求要求1000ms完成,则GC导致STW的时间不能超过100ms,只要在这个范围内,就是符合要求的(也就是无需优化)
耗时=业务代码执行时间+GC-STW时间
吞吐量角度:要求:一定时间内处理完多少此请求,例如:1分钟处理完毕6000此请求,则平均每秒处理100次,追求吞吐量的情况下,我们就不去细说一次GC的时间,我们要统计的就是这段时间内GC消耗的总时间,同样,根据经验值,GC占用的总时间不应超过10%
8.3.4GC相关参数
自适应参数:
单次最大暂停时间(STW时间): -XX:MaxGCPauseMillis=200 JVM尽量保证把每次GC的时间控制在这个时间内
应用程序吞吐量目标:-XX:GCTimeRatio=19 如果参数设置为19,则垃圾回收总时间小于应用程序总运行时间的5%,计算公式:GC总时长限值==1/(1+GCtimeRation)*程序总时长
这两个参数配置后,JVM会根据运行情况,自动动态调整堆中分代各区域的大小。在这种动态调整的情况下,JVM自行选择。
同时配置,优选保证的顺序:最大暂停时间目标>吞吐量目标
分代空间大小配置:-Xmn 新生代代大小
-XX:newSize 新生代初始化内存的大(注意:该值需要小于-Xms的值)
-XX:MaxnewSize 新生代可被分配的内存的最大上限(注意:该值需要小于-Xmx的值)
一个参数顶两个细分参数
-XX:NewRatio=2 默认新生代和老年代比例1:2
-XX:SurvivorRatio=8 默认情况下Eden:from:to=8:1:1
大对象的判定标准,通过-XX:+pretenureSizeThreshold控制
切换不同的垃圾回收器,这个是用的最多的GC方面优化手段
-XX:+UseSerialGC :服务器基本不用,单线程去做垃圾回收
--XX:+UseParallelGC -XX:+UseParallelOldGC:大部分默认这种,并行多线程回收
--XX:+UseConcMarkSweepGC:响应时间要求高的选择这一种,并发多线程回收
GC的多个阶段中,有一些阶段业务线程代码依旧可以运行,不像并行多线程,GC 全都是暂停业务线程
G1 GC,全程Garbage-Firest Garbage Collector,通过-XX:+UseG1GC:大内存服务器选择这一种,并发多线程回收
预申请内存:-XX:+AlwaysPreTouch
每个java应用启动参数必加
每个java应用启动之后梦想操作系统把内存全部申请到位
JVM配置了2G内存,不代表马上就有2G内存归属JVM,默认用了多少,操作系统给它多少,当值JVM要用的时候,操作系统没用足够内存,启动之后,提前申请
GC调优--所有jvm核心开发团队,不断努力提升的方向,参数很多,调了某一个参数不一定管用。多次配置修改,反复尝试,大规模落地--几家大厂核心业务系统