一、jvm调优基础
1.jvm堆内存配置
-XX:MetaspaceSize=128m (元空间初始化大小)
-XX:MaxMetaspaceSize=128m (元空间最大大小)
-Xms1024m (堆初始化大小)
-Xmx1024m (堆最大大小)
-Xmn256m (新生代大小)
-Xss256k (棧最大深度大小)
-XX:SurvivorRatio=8 (新生代分区比例 8:2)
-XX:+UseConcMarkSweepGC (指定使用的垃圾收集器,这里使用CMS收集器)
-XX:+PrintGCDetails (打印详细的GC日志)
堆内存调整参数如图所示:

我们可以发现每一个区域都有一个可变的伸缩区,当我们的内存空间不足的时候,会在可变的范围内扩大内存空间,当我们的内存空间变得不紧张的时候我们再释放可变空间。
查看jvm配置可使用下面的命令
ps -ef | grep java
1.堆内存
在堆内存的调优之中我们要特别注意两个参数-Xms初始化内存分配大小,默认为物理内存的1/64,-Xmx 最大的分配内存默认为物理内存的1/4。
即执行Java -jar 命令默认的内存分配策略
其中年轻代和老年代的默认分配比例(NewRatio)是:1:2
在年轻代中,SurvivorRatio默认值:8,即Eden、Survivor From、Survivor To 的比值是 8:1:1
当然我们也可以自己手动指定年轻代和老年代的大小
可以使用-XX:NewSize或者-Xmn来指定新生代的大小,老年代大小将会自动计算并且设置为堆大小减去新生代大小。
另外可以通过-XX:MaxTenuringThreshold调整大对象阀值
垃圾最大年龄.如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率
2.元空间
在Java 在8和之前的版本中,永久代的尺寸是固定的,JVM的启动参数-XX:PermSize和-XX:MaxPermSize配置。默认情况下,-XX:PermSize和-XX:MaxPermSize值均为64MB。
然而,在Java 8.在后续版本中,元空间的大小是动态的,不再局限于固定的大小。
元空间的大小由JVM的启动参数组成-XX:MetaspaceSize和-XX:MaxMetaspaceSize配置。默认情况下,-XX:MetaspaceSize值为21MB,而-XX:MaxMetaspaceSize值是无限大的。
当然你也可以自己自定义去设置初始化大小和最大值,但一般不建议。
如果你真的想这么做可以参考
因为元空间在所有类加载完成后,大小基本不会发生太大变化。而项目启动后,元空间大小大约在100M左右,所以可以配置
-XX:MetaspaceSize=128M,-XX:MaxMetaspaceSize=128m
那么元空间什么条件才能够被当成垃圾被卸载回收了?
条件还是比较严苛的,需同时满足如下三个条件的类才会被卸载:
- 该类所有的实例都已经被回收;
- 加载该类的ClassLoader已经被回收;
- 该类对应的java.lang.Class对象没有任何地方被引用。
2.JVM运行情况预估
用 jstat gc -pid 命令可以计算出如下一些关键数据,有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置一些初始性的JVM参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。
年轻代对象增长的速率
可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。
Young GC的触发频率和每次耗时
知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC 公式算出,根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。
每次Young GC后有多少对象存活和进入老年代
这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden,survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。
基于这个理论我自己认为可以得出这样的经验:比如在高并发的场景,短时间内如果发现年轻代有大量的对象产生,同时又有大量的对象被回收了,此时就要预备好足够大的年轻代空间,让这些朝生夕死的对象在年轻代进行内存的创建和回收,而不要因为年轻代内存不够而跑到老年代进行内存的创建和回收。
Full GC的触发频率和每次耗时
知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。
优化思路其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。
2.jvm垃圾收集配置配置
1.cms
如下面的启动参数
root 8508 1 0 11:51 ? 00:02:08 /bin/java -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -server -Xmx2g -Xms2g -Xmn512m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70
-server:
启用-server时新生代默认采用并行收集,其他情况下,默认不启用。
-XX:+UseAdaptiveSizePolicy:
上文中,因启用-server模式,所以新生代使用并行收集器。
设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,建议使用并行收集器时一直打开。
-XX:+DisableExplicitGC
关闭System.gc()
-XX:+UseConcMarkSweepGC:
设置老年代为并发收集。
-XX:+CMSParallelRemarkEnabled
降低标记停顿
-XX:+UseCMSCompactAtFullCollection
在FULL GC的时候, 对老年代的压缩。
CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。
可能会影响性能,但是可以消除碎片
-XX:LargePageSizeInBytes
内存页的大小不可设置过大, 会影响Perm的大小
-XX:+UseFastAccessorMethods
原始类型的快速优化
XX:+UseCMSInitiatingOccupancyOnly
使用手动定义初始化定义开始CMS收集,目的:禁止hostspot自行触发CMS GC
-XX:CMSInitiatingOccupancyFraction=70
使用cms作为垃圾回收,使用70%后开始CMS收集
其他相关参数:
-XX:PermSize
设置持久代(perm gen)初始值 默认为:物理内存的1/64
-XX:MaxPermSize
设置持久代最大值 默认为:物理内存的1/4
3.GC调优原则
GC是有代价的,因此我们调优的根本原则是每一次GC都回收尽可能多的对象,也就是减少无用功。
因此我 们在做具体调优的时候,针对CMS和G1两种垃圾收集器,分别有一些相应的策略
CMS收集器
对于CMS收集器来说,最重要的是合理地设置年轻代和年老代的大小。年轻代太小的话,会导致频繁的 Minor GC,并且很有可能存活期短的对象也不能被回收,GC的效率就不高。年轻代太大的话首先会导致年老代太小,其次反复存活的对象会长期存在年轻代,减少了新对象在年轻代的存储空间。
而年老代太小的话,容纳不下 从年轻代过来的新对象,会频繁触发单线程Full GC,导致较长时间的GC暂停,影响Web应用的响应时间。
G1收集器
对于G1收集器来说,我不推荐直接设置年轻代的大小,这一点跟CMS收集器不一样,这是因为G1收集器会 根据算法动态决定年轻代和年老代的大小。
因此对于G1收集器,我们需要关心的是Java堆的总大小(- Xmx)。
此外G1还有一个较关键的参数是-XX:MaxGCPauseMillis = n,这个参数是用来限制最大的GC暂停时 间,目的是尽量不影响请求处理的响应时间。
G1将根据先前收集的信息以及检测到的垃圾量,估计它可以 立即收集的最大区域数量,从而尽量保证GC时间不会超出这个限制。因此G1相对来说更加“智能”,使用 起来更加简单。
4.dump文件
服务发生内存溢出,就需要查看服务器上Java服务的jvm堆内存使用情况,可以使用dump命令生成dump文件,然后下载到本地,然后使用jvisualVM工具打开,即可实现可视化分析。
生成dump文件常用的两种方式:
第一种:使用命令直接生成。
第二种:java -jar启动服务的时候添加dump参数,服务发生内存溢出时自动生成。
1、使用命令直接生成堆dump文件
发送内存溢出时,可以先使用命令生成dump文件后再重启服务。
登录虚机,执行以下jamp命令
# 替换<pid>为Java进程的ID,file:输出文件名为heap.hprof,可自定义路径
jmap -dump:format=b,file=heap.hprof <pid>
2、内存溢出发生时自动生成dump文件
java -jar启动服务的时候添加dump参数,服务发生内存溢出时自动生成dump文件。
-XX:+HeapDumpOnOutOfMemoryError 当OutOfMemoryError发生时生成dump文件
-XX:HeapDumpPath=生成dump文件的存储目录,如不指定默认生成在jar所在目录,目录一定要存在,否则生成失败。
如下面所示
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=./
生成完dump文件后,可以下载到本地,然后使用jvm的一些可视化工具如jvisualvm进行内存分析。
二、GC log
1.打印GC日志
对于java应用我们可以通过一些配置把程序运行过程中的gc日志全部打印出来,然后分析gc日志得到关键性指标,分析GC原因,调优JVM参数。
打印GC日志方法,在JVM参数里增加参数,%t 代表时间
-Xloggc:./gc-%t.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintGCCause
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=100M
注意不要包含空格,否则可能出现下面的错误

然后我们解释一下常用参数的含义
-Xloggc:gc.log:指定GC日志路径,默认情况下GC日志直接输出到标准输出,不过使用-Xloggc:filename标志也能修改输出到某个文件。最好配置成每次服务重启后会新生成一个log- -XX:+PrintGCDetails:最简单的 GC 参数开启GC日志打印,默认是关闭的。 会打印 GC 前后堆空间使用情况以及 GC 花费的时间
- -XX:+PrintGCDateStamps:打印GC发生的时刻,所处日期时间信息
- -XX:+PrintGCTimeStamps:打印CG发生的时间戳,从应用启动开始累计的时间戳
- -XX:+PrintGCCause:打印GC原因
- -XX:+UseGCLogFileRotation:打开或关闭GC日志滚动记录功能,要求必须设置 -Xloggc参数。当GC日志文件的内容达到一定的大小或数量时,该功能可以自动滚动到新的日志文件,以避免单个日志文件过大
- -XX:NumberOfGCLogFiles:设置滚动日志文件的个数,必须大于等于1。日志文件命名策略是,.0, .1, ..., .n-1,其中n是该参数的值。
- -XX:GCLogFileSize:设置滚动日志文件的大小,必须大于8k。当前写日志文件大小超过该参数值时,日志将写入下一个文件
- -XX:+PrintCommandLineFlags:JVM 将会打印出所有的命令行参数,然后终止。这对于理解和调试 JVM 启动参数非常有用
更多参数的解释可参考官方文档
2.GC日志分析
运行程序加上对应gc日志
java -jar -Xloggc:./gc-%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M microservice-eureka-server.jar
下图中是我截取的JVM刚启动的一部分GC日志

我们可以看到图中第一行红框,是项目的配置参数。这里不仅配置了打印GC日志,还有相关的VM内存参数。
第二行红框中的是在这个GC时间点发生GC之后相关GC情况。
1、对于2.909: 这是从jvm启动开始计算到这次GC经过的时间,前面还有具体的发生时间日期。
2、Full GC(Metadata GC Threshold)指这是一次full gc,括号里是gc的原因, PSYoungGen是年轻代的GC,ParOldGen是老年代的GC,Metaspace是元空间的GC
3、 6160K->0K(141824K),这三个数字分别对应GC之前占用年轻代的大小,GC之后年轻代占用,以及整个年轻代的大小。
4、112K->6056K(95744K),这三个数字分别对应GC之前占用老年代的大小,GC之后老年代占用,以及整个老年代的大小。
5、6272K->6056K(237568K),这三个数字分别对应GC之前占用堆内存的大小,GC之后堆内存占用,以及整个堆内存的大小。
6、20516K->20516K(1069056K),这三个数字分别对应GC之前占用元空间内存的大小,GC之后元空间内存占用,以及整个元空间内存的大小。
7、0.0209707是该时间点GC总耗费时间。
从日志可以发现几次fullgc都是由于元空间不够导致的,所以我们可以将元空间调大点。
上面的这些参数,能够帮我们查看分析GC的垃圾收集情况。但是如果GC日志很多很多,成千上万行。就算你一目十行,看完了,脑子也是一片空白。所以我们可以借助一些功能来帮助我们分析,如GCview和gceasy等工具。
另外在解释一下-XX:+PrintGCCause 打印出来的常见GC原因
-
GC (Allocation Failure:
表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。 -
Full GC (Metadata GC Threshold):
表明本次引起GC的原因是超过元空间阀值
三、jvm调优常用命令
这里主要介绍如下几个工具:
1、jps:查看本机java进程信息
2、jstack:打印线程的栈信息,制作 线程dump文件
3、jmap:打印内存映射信息,制作 堆dump文件
4、jstat:性能监控工具
5、jhat:内存分析工具,用于解析堆dump文件并以适合人阅读的方式展示出来
6、jconsole:简易的JVM可视化工具
7、jvisualvm:功能更强大的JVM可视化工具
JAVA Dump:
JAVA Dump就是虚拟机运行时的快照,将虚拟机运行时的状态和信息保存到文件中,包括:
线程dump:包含所有线程的运行状态,纯文本格式
堆dump:包含所有堆对象的状态,二进制格式
1.jps
显示当前所有java进程pid的命令,我们可以通过这个命令来查看到底启动了几个java进程(因为每一个java程序都会独占一个java虚拟机实例),不过jps有个缺点是只能显示当前用户的进程id,要显示其他用户的还只能用linux的ps命令。

执行jps命令,会列出所有正在运行的java进程,其中jps命令也是一个java程序。前面的数字就是进程的id,这个id的作用非常大,后面会有相关介绍。
jps -v 输出传递给JVM的参数
2.jstack
主要用于生成指定进程当前时刻的线程快照,线程快照是当前java虚拟机每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致长时间等待。

3.jmap
主要用于打印指定java进程的共享对象内存映射或堆内存细节。
堆Dump是反映堆使用情况的内存镜像,其中主要包括系统信息、虚拟机属性、完整的线程Dump、所有类和对象的状态等。一般在内存不足,GC异常等情况下,我们会去怀疑内存泄漏,这个时候就会去打印堆Dump。
1. jmap -heap pid:查看堆使用情况

jmap 踩坑记录
Windows下jmap命令报错问题 https://www.cnblogs.com/ocean234/p/11525721.html
2. jmap -histo pid:查看堆中对象数量和大小

4.jstat
主要是对java应用程序的资源和性能进行实时的命令行监控,包括了对heap size和垃圾回收状况的监控。
jstat - [-t] [-h] [ []]
option:我们经常使用的选项有gc、gcutil
vmid:java进程id
interval:间隔时间,单位为毫秒
count:打印次数
启动方式为: “jstat -gc -t PID 5s”
jstat -gc -t 1 5s

Timestamp:jstat 连接到 JVM 的时间,即jvm从启动开始算的时间
S0C:年轻代第一个survivor的容量(KB)
S1C:年轻代第二个survivor的容量(KB)
S0U:年轻代第一个survivor已使用的容量(KB)
S1U:年轻代第二个survivor已使用的容量(KB)
EC:年轻代中Eden的空间(KB)
EU:年代代中Eden已使用的空间(KB)
OC:老年代的容量(KB)
OU:老年代中已使用的空间(KB)
MC:当前metaspace的容量(KB)
MU:当前metaspace已使用的空间(KB)CCSC:当前compressed class space的容量(KB)
CCSU:当前compressed class space已使用的空间(KB)YGC:从应用程序启动到采样时年轻代中GC的次数
YGCT:从应用程序启动到采样时年轻代中GC所使用的时间(单位:S)FGC:从应用程序启动到采样时老年代中GC(FULL GC)的次数
FGCT:从应用程序启动到采样时老年代中GC所使用的时间(单位:S)
CGC:从应用程序启动到采样时发生concurrent GC的次数
CGCT:从应用程序启动到采样时concurrent GC所用的时间(秒)
GCT:从应用程序启动到采样时垃圾回收所用的总时间(秒)
通过 jstat 能很快发现对JVM健康极为不利的GC行为。一般来说, 只看 jstat 的输出就能快速发现以下问题:
- 最后一列 “GCT”, 与JVM的总运行时间 “Timestamp” 的比值, 就是GC 的开销。如果每一秒内, “GCT“ 的值都会明显增大, 与总运行时间相比, 就暴露出GC开销过大的问题. 不同系统对GC开销有不同的容忍度, 由性能需求决定, 一般来讲, 超过
10%的GC开销都是有问题的。 - “YGC” 和 “FGC” 列的快速变化往往也是有问题的征兆。频繁的GC暂停会累积,并导致更多的线程停顿(stop-the-world pauses), 进而影响吞吐量。
- 如果看到 “OU” 列中,老年代的使用量约等于老年代的最大容量(OC), 并且不降低的话, 就表示虽然执行了老年代GC, 但基本上属于无效GC。
5.jinfo
jinfo可以用来查看正在运行的java运用程序的扩展参数,甚至支持在运行时动态地更改部分参数。
基本使用语法如下: jinfo -< option > < pid > ,其中option可以为以下信息:
查看当前的应用java参数配置
jinfo -flags pid

6.jcmd
在JDK 1.7之后,新增了一个命令行工具jcmd。它是一个多功能工具,可以用来导出堆,查看java进程,导出线程信息,执行GC等。jcmd拥有jmap的大部分功能,Oracle官方建议使用jcmd代替jmap。
jcmd pid help:针对指定的进程,列出支持的所有命令
如下图所示

子命令含义:
VM.native_memory
VM.commercial_features
GC.rotate_log
ManagementAgent.stop
ManagementAgent.start_local
ManagementAgent.start
Thread.print, 打印线程栈信息
GC.class_histogram, 查看系统中类统计信息
GC.heap_dump, 导出堆信息,与jmap -dump功能一样
GC.run_finalization, 触发finalize()
GC.run, 触发gc()
VM.uptime, VM启动时间
VM.flags, 获取JVM启动参数
VM.system_properties, 获取系统Properties
VM.command_line, 启动时命令行指定的参数
VM.version
help
当然你也可以通过命令提示查看官方对子命令的含义解释

如查看JVM启动参数

思考
2.如何查看内存占用最高的对象
通过jconsole分析dump文件。
3.如何查看进程是否频繁进行fullGC并解决
使用的jvm参数为
-Xms100m -Xmx100m -Xmn30m
代码地址见:
1.使用jstat命令分析是否频繁触发fullGC

可见频繁的进行了full gc
2.使用jmap -histo 1358 查看是否存在大对象

user对象创建了500个实例,每个实例0.1M,并放在了一个list集合中,将是50M,大于新生代的大小30M,将放在老年代中。长次以往将不断的触发full gc.
解决办法:
调大新生代的大小,让list对象通过Minor gc进行回收掉。
4.发生OOM如何排查?
1.首先增加两个参数便于,当 OOM 发生时自动 dump 堆内存信息
到指定目录。
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
2.使用 MAT 等工具载入到 dump 文件, 分析大对象的占用情况, 比如 HashMap 做缓存未清理, 时间长了就会内存溢出, 可以把改为弱引用。
四、实战与演练
1. idea 设置jvm运行参数
-Xms100m -Xmx100m

2.CPU飙升问题
CPU 资源经常会成为系统性能的一个瓶颈,这其中的原因是多方面的,可能是内存泄露导致频繁 GC,进而引起 CPU 使用率过高;又可能是代码中的 Bug 创建了大量的线程,导致 CPU 上下文切换开销。
那么Java 进程 CPU 使用率高”的解决思路是什么?
对于 CPU 的问题,最重要的是要找到是哪些线程在消耗 CPU,通过线程栈定位到问题代码;如果没有找到个别线程的 CPU 使用率特别高,我们要怀疑到是不是线程上下文切换导致了 CPU 使用率过高。
1.如何用jstack找出占用cpu最高的线程堆栈信息?
我们使用jdk自带的jstack来分析。
思路:
先找到高负载的进程ID,然后查看进程中高负载的线程ID。最后使用jstack 线程ID找到详细的堆栈位置
当linux出现cpu被java程序消耗过高时,以下过程说不定可以帮上你的忙:
1、执行:top
查看高负载的进程
2、top -H -p 28973
查看高负载进程下的高负载线程
把线程号 28973 进行换算成16进制编号:print"%x\n" 28973 ->72d6
3、jstack 28973 >> log.txt
将线程堆栈信息保存为txt文档
4、打开log.txt,find一下72d6
导出进程的堆栈日志,找到72d6 这个线程号

@see jstack命令查看占用CPU高的线程堆栈信息https://www.cnblogs.com/kaymi/p/12667708.html
2.排查上下文切换开销导致CPU标高问题?
写一个模拟程序来模拟 CPU 使用率过高的问题,这个程序会在线程池中创建 4096 个线程。
代码如下:

使用 top 命令,我们看到 Java 进程的 CPU 使用率达到了 262.3%,注意到进程 ID 是 4361。

接着我们用更精细化的 top 命令查看这个 Java 进程中各线程使用 CPU 的情况:
#top -H -p 4361
从图上我们可以看到,有个叫“scheduling-1”的线程占用了较多的 CPU,达到了 42.5%。
因此下一步我们要找出这个线程在做什么事情,那么就和上面用 jstack 命令生成线程快照类似,这里就直接贴出结果

从线程栈中我们看到了AbstractExecutorService.submit这个函数调用,说明它是 Spring Boot 启动的周期性任务线程,向线程池中提交任务,这个线程消耗了大量 CPU。
一般来说,通过上面的过程,我们就能定位到大量消耗 CPU 的线程以及有问题的代码,比如死循环。
但是对于这个实例的问题,你是否发现这样一个情况:Java 进程占用的 CPU 是 262.3%, 而“scheduling-1”线程只占用了 42.5% 的 CPU,那还有将近 220% 的 CPU 被谁占用了呢?
不知道你注意到没有,我们在第 4 步用top -H -p 4361命令看到的线程列表中还有许多名为“pool-1-thread-x”的线程,它们单个的 CPU 使用率不高,但是似乎数量比较多。你可能已经猜到,这些就是线程池中干活的线程。那剩下的 220% 的 CPU 是不是被这些线程消耗了呢?
要弄清楚这个问题,我们还需要看 jstack 的输出结果,主要是看这些线程池中的线程是不是真的在干活,还是在“休息”呢?

通过上面的图我们发现这些“pool-1-thread-x”线程基本都处于 WAITING 的状态,那什么是 WAITING 状态呢?
- Waiting 指的是一个线程拿到了锁,但是需要等待其他线程执行某些操作。比如调用了 Object.wait、Thread.join 或者 LockSupport.park 方法时,进入 Waiting 状态。前提是这个线程已经拿到锁了,并且在进入 Waiting 状态前,操作系统层面会自动释放锁,当等待条件满足,外部调用了 Object.notify 或者 LockSupport.unpark 方法,线程会重新竞争锁,成功获得锁后才能进入到 Runnable 状态继续执行。
回到我们的“pool-1-thread-x”线程,这些线程都处在“Waiting”状态,从线程栈我们看到,这些线程“等待”在 getTask 方法调用上,线程尝试从线程池的队列中取任务,但是队列为空,所以通过 LockSupport.park 调用进到了“Waiting”状态。那“pool-1-thread-x”线程有多少个呢?通过下面这个命令来统计一下,结果是 4096,正好跟线程池中的线程数相等。

你可能好奇了,那剩下的 220% 的 CPU 到底被谁消耗了呢?分析到这里,我们应该怀疑 CPU 的上下文切换开销了,因为我们看到 Java 进程中的线程数比较多。
下面我们通过 vmstat 命令来查看一下操作系统层面的线程上下文切换活动:

其中 cs 那一栏表示线程上下文切换次数,in 表示 CPU 中断次数,我们发现这两个数字非常高,基本证实了我们的猜测,线程上下文切切换消耗了大量 CPU。那么问题来了,具体是哪个进程导致的呢?
我们停止 Spring Boot 测试程序,再次运行 vmstat 命令,会看到 in 和 cs 都大幅下降了,这样就证实了引起线程上下文切换开销的 Java 进程正是 4361。

关于具体的案例可参考
极客时间《深入拆解 Tomcat & Jetty》
3.日均百万级系统JVM参数调优
JVM参数大小设置并没有固定标准,需要根据实际项目情况分析,给大家举个例子

结论:通过上面这些内容介 绍,大家应该对JVM优化有些概念了,就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收。
五、jvm调优工具
1.VisualVM
关于下载和安装比较简单,可参考
然后启动打开对应的dump文件

可以看到服务中占有内存比较多的类

查看被引用的位置
查看对应的报错位置


2.GCview
关于安装和启动可参考
然后启动后,打开对应的GC日志文件(线上环境建议从服务器导出来),日志路径可以看服务启动时所配置的-Xloggc路径,比如
-Xloggc:./gc-%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M
打开后可参考下图

我给程序设置的堆的大小为32MB,使用的版本是Java 12,默认的垃圾收集器是G1。
目的是能让我们看到Full GC
- 图中上部的蓝线表示已使用堆的大小,我们看到它周期的上下震荡,当堆大小在32MB左右时,会被较大幅度清理
- 图底部的绿线表示年轻代GC活动,从图上看到当堆的使用率上去了,会触发频繁的GC活动。
- 图中的黑色的竖线表示Full GC,从图上看到,伴随着Full GC,蓝线会下降,这说明Full GC收集了年老代中的对 象。
基于上面的分析,我们可以得出一个结论,那就是Java堆的大小不够。
我来解释一下为什么得出这个结论:
- GC活动频繁:年轻代GC(绿色线)和年老代GC(黑色线)都比较密集。这说明内存空间不够,也就是 Java堆的大小不够。
- Java的堆中对象在GC之后能够被回收,说明不是内存泄漏。
我们通过GCViewer还发现累计GC暂停时间有55.57秒,如下图所示:

因此我们的解决方案是调大Java堆的大小,提高到了2048m
生成的新的GC log分析图如下

你可以看到,没有发生Full GC(无黑色的大竖线,内存下降幅度较小),并且年轻代GC也没有那么频繁了,并且累计GC暂停时间只有3.05秒。

参考资料
1.JDK工具(查看JVM参数、内存使用情况及分析等)
https://www.cnblogs.com/z-sm/p/6745375.html

2245

被折叠的 条评论
为什么被折叠?



