JVM调优

一、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 

那么元空间什么条件才能够被当成垃圾被卸载回收了?

条件还是比较严苛的,需同时满足如下三个条件的类才会被卸载:

  1. 该类所有的实例都已经被回收;
  2. 加载该类的ClassLoader已经被回收;
  3. 该类对应的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所在目录,目录一定要存在,否则生成失败。

如下面所示

  1. -XX:+HeapDumpOnOutOfMemoryError
  2. -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 启动参数非常有用

更多参数的解释可参考官方文档

javaThis document contains reference information for the tools that are installed with Java Development Kit (JDK).icon-default.png?t=O83Ahttps://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html

 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

关于下载和安装比较简单,可参考

下载安装 VisualVM_visualvm下载-优快云博客文章浏览阅读1.6k次,点赞5次,收藏4次。文件,设置jdk路径。_visualvm下载https://blog.youkuaiyun.com/weixin_37646636/article/details/138389655

然后启动打开对应的dump文件

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

查看被引用的位置

 查看对应的报错位置

2.GCview

关于安装和启动可参考

【JVM】GC日志分析工具一GCview使用介绍_gcviewer-优快云博客文章浏览阅读7.5k次,点赞2次,收藏16次。GCViewer介绍业界较为流行分析GC日志的两个工具——GCViewer、GCEasy。GCEasy部分功能还是要收费的,今天笔者给大家介绍一下GCViewer的使用与功能点。二、GCViewer 使用。_gcviewericon-default.png?t=O83Ahttps://blog.youkuaiyun.com/qq_35995514/article/details/130207816

 

然后启动后,打开对应的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

2.介绍 - 《GC参考手册-Java版》 - 书栈网 · BookStackJava垃圾收集必备手册目录下载相关链接 顾名思义,垃圾收集(Garbage Collection)的意思就是 —— 找到垃圾并进行清理。但现有的垃圾收集实现却恰恰相反: 垃圾收集器跟踪所有正在使用的对象,并把其余部分当做垃圾。记住这一点以后, 我们再深入讲解内存自动回收的原理,探究 JVM 中垃圾收集的具体实现, 。icon-default.png?t=O83Ahttps://www.bookstack.cn/read/gc-handbook/README.md

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值