导航:
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/黑马旅游/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码
目录
一、大厂面试提问方式
二、JVM调优步骤
三、监控发现问题
四、工具定位问题
4.1 调优依据
4.2 JDK自带的命令行调优工具
4.2.3 常用命令总结
4.2.4 jps:查看正在运行的 Java 进程
4.2.5 jstat:查看 JVM 统计信息
概述
【OOM案例】jstat判断内存溢出(OOM):比较GC时长占运行时长的比例
【内存泄漏案例】比较老年代内存量上涨速度
4.2.6 jstack:打印指定进程此刻的线程快照
4.3 JDK自带的可视化监控工具
4.4 MAT分析堆转储文件
4.4.1 简介
4.4.2 生成dump文件方式
方法一:jmap
方法二:Visual VM
方法三:MAT直接从Java进程导出dump文件
五、JVM性能调优
5.1 调优JVM参数
5.1.0 JVM常用调优参数汇总
5.1.1 减少停顿时间:MaxGCPauseMillis
5.1.2 提高吞吐量:GCTimeRatio
5.1.3 调整堆内存大小
5.1.4 调整堆内存比例
5.1.5 调整升老年代年龄
扩展:一次完整的GC流程
5.1.6 调整大对象阈值
5.1.7 调整GC的触发条件
CMS调整老年代触发回收比例
G1调整存活阈值
5.1.8 【最有效】选择合适的垃圾回收器
5.1 排查大对象
5.1.1 内存溢出
概念
OOM的排查和解决
5.1.2 内存泄漏
概念
内存泄漏的排查和解决
5.2 CPU飙升和GC频繁的调优方案
5.2.1 CPU飙升
原因
定位步骤
5.2.2 GC调优
GC频率的合理范围
监控发现问题
命令行分析问题
解决方案
调优效果
5.3 其他优化方案
5.3.1 优化业务代码
5.3.2 增加机器
5.3.3 调整线程池参数
5.3.4 缓存、MQ等中间件优化
诊断分析工具:
调优:
生产环境调优:
内存泄漏:
在项目开发过程中、生产环境中,任何问题的解决、性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题,本文将从这个步骤入手,详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优。
通过监控工具例如Prometheus+Grafana,监控服务器有没有以下情况,有的话需要调优:
使用分析工具定位oom、内存泄漏等问题。
JVM调优时,吞吐量和停顿时长无法兼顾,吞吐量提高的代价是停顿时间拉长。
所以,如果应用程序跟用户基本不交互,就优先提升吞吐量。如果应用程序和用户频繁交互,就优先缩短停顿时间。
jps:查看正在运行的 Java 进程。jps -v查看进程启动时的JVM参数;
jstat:查看指定进程的 JVM 统计信息。jstat -gc查看堆各分区大小、YGC,FGC次数和时长。如果服务器没有 GUI 图形界面,只提供了纯文本控制台环境,它是运行期定位虚拟机性能问题的首选工具。
jinfo:实时查看和修改指定进程的 JVM 配置参数。jinfo -flag查看和修改具体参数。
jstack:打印指定进程此刻的线程快照。定位线程长时间停顿的原因,例如死锁、等待资源、阻塞。如果有死锁会打印线程的互相占用资源情况。
jps(Java Process Status):显示指定系统内所有的 HotSpot 虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。
说明:对于本地虚拟机进程来说,进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的,是唯一的。
基本使用语法为:
jps [options参数] [hostid参数]
代码示例
一个阻塞状态的线程,等待用户输入:
public class ScannerTest { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String info = scanner.next(); } }
运行后,在命令行输入 jps 查看进程:
我们还可以通过追加参数,来打印额外的信息。
options 参数:
-q:仅仅显示 LVMID(local virtual machine id),即本地虚拟机唯一 id。不显示主类的名称等
-l:输出应用程序主类的全类名或如果进程执行的是 jar 包,则输出 jar 完整路径
-m:输出虚拟机进程启动时传递给主类 main() 的参数
-v:列出虚拟机进程启动时的 JVM 参数。比如:-Xms20m -Xmx50m
是启动程序指定的 jvm 参数
说明:以上参数可以综合使用。
案例:
补充:如果某 Java 进程关闭了默认开启的 UsePerfData 参数(即使用参数 -XX:-UsePerfData),那么 jps 命令(以及下面介绍的 jstat)将无法探知该 Java 进程。
hostid 参数:
RMI 注册表中注册的主机名。如果想要远程监控主机上的 java 程序,需要安装 jstatd。
对于具有更严格的安全实践的网络场所而言,可能使用一个自定义的策略文件来显示对特定的可信主机或网络的访问,尽管这种技术容易受到 IP 地址欺诈攻击。
如果安全问题无法使用一个定制的策略文件来处理,那么最安全的操作是不运行 jstatd 服务器,而是在本地使用 jstat 和 jps 工具。
jstat(JVM Statistics Monitoring Tool):用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据。在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。
基本使用语法为:
jstat -
查看命令相关参数:jstat-h
或 jstat-help
。
其中 vmid 是进程 id 号,也就是 jps 之后看到的前面的号码,如下:
option参数:
选项 option 可以由以下值构成:
类装载相关的:
垃圾回收相关的:
-gc
基本相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间-gc
基本相同,但输出主要关注已使用空间占总空间的百分比-gcutil
功能一样,但是会额外输出导致最后一次或当前正在发生的 GC 产生的原因-gcnew
基本相同,输出主要关注使用到的最大、最小空间-gcold
基本相同,输出主要关注使用到的最大、最小空间JIT 相关的:
-compiler:显示 JIT 编译器编译过的方法、耗时等信息
-printcompilation:输出已经被 JIT 编译的方法
jstat -class
jstat -class 86517
Loaded Bytes Unloaded Bytes Time
18051 32345.3 0 0.0 112.14
显示列名 | 具体描述 |
---|---|
Loaded | 装载的类的数量 |
Bytes | 装载的字节数 |
Unloaded | 卸载的类的数量 |
Bytes | 卸载的类数量 |
Time | 装载和卸载的使用时间 |
jstat -compiler
显示 JIT 编译器编译过的方法、耗时等信息。
jstat -printcompilation
输出已经被 JIT 编译的方法。
jstat -gc
显示与 GC 相关的堆信息。包括 Eden 区、两个 Survivor 区、老年代、永久代等的容量、已用空间、GC 时间合计等信息。
执行代码:
public class GCTest {
public static void main(String[] args) {
ArrayList list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
byte[] arr = new byte[1024 * 100];//100KB
list.add(arr);
try {
Thread.sleep(120);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
JVM 参数:
-Xms60m -Xmx60m -XX:SurvivorRatio=8
运行后利用命令查询:
表头 | 含义(字节) |
---|---|
S0C | 幸存者 0 区的大小 |
S1C | 幸存者 1 区的大小 |
S0U | 幸存者 0 区已使用的大小 |
S1U | 幸存者 1 区已使用的大小 |
EC | Eden 区的大小 |
EU | Eden 区已使用的大小 |
OC | 老年代的大小 |
OU | 老年代已使用的大小 |
MC | 元空间的大小 |
MU | 元空间已使用的大小 |
CCSC | 压缩类空间的大小 |
CCSU | 压缩类空间已使用的大小 |
YGC | 从应用程序启动到采样时 Young GC 的次数 |
YGCT | 从应用程序启动到采样时 Young GC 消耗时间(秒) |
FGC | 从应用程序启动到采样时 Full GC 的次数 |
FGCT | 从应用程序启动到采样时的 Full GC 的消耗时间(秒) |
GCT | 从应用程序启动到采样时 GC 的总时间 |
后面的参数代表 1000 毫秒打印一次,一个打印 10 次。
jstat -gccapacity
显示内容与 -gc
基本相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间。
jstat -gcutil
显示内容与 -gc
基本相同,但输出主要关注已使用空间占总空间的百分比。
表头 | 含义(字节) |
---|---|
SO | Survivor 0 区空间百分比 |
S1 | Survivor 1 区空间百分比 |
E | Eden 区空间百分比 |
O | Old 区空间百分比 |
N | 方法区空间百分比 |
CCS | 压缩空间百分比 |
YGC | 从应用程序启动到采样时 Young GC 的次数 |
YGCT | 从应用程序启动到采样时 Young GC 消耗时间(秒) |
FGC | 从应用程序启动到采样时 Full GC 的次数 |
FGCT | 从应用程序启动到采样时的 Full GC 的消耗时间(秒) |
GCT | 从应用程序启动到采样时 GC 的总时间 |
jstat -gccause
与 -gcutil
功能一样,但是会额外输出导致最后一次或当前正在发生的 GC 产生的原因。
jstat -gcnew
显示新生代 GC 状况。
jstat -gcnewcapacity
显示内容与 -gcnew
基本相同,输出主要关注使用到的最大、最小空间。
jstat -gcold
显示老年代 GC 状况。
jstat -gcoldcapacity
显示内容与 -gcold
基本相同,输出主要关注使用到的最大、最小空间。
其他参数:
下面的参数都是配合 option 参数后面使用。
基本使用语法为:
jstat -
interval 参数:用于指定输出统计数据的周期,单位为毫秒。即:查询间隔
count 参数:用于指定查询的总次数
-t 参数:可以在输出信息前加上一个 Timestamp 列,显示程序的运行时间。单位:秒
我们可以比较 Java 进程的启动时间以及总 GC 时间(GCT 列),或者两次测量的间隔时间以及总 GC 时间的增量,来得出 GC 时间占运行时间的比例。
如果该比例超过 20%,则说明目前堆的压力较大;如果该比例超过 90%,则说明堆里几乎没有可用空间,随时都可能抛出 OOM 异常。
-h 参数:可以在周期性数据输出时,输出多少行数据后输出一个表头信息
jstat -t
可以在输出信息前加上一个 Timestamp 列,显示程序的运行时间。单位:秒。
jstat -t -h
可以在周期性数据输出时,输出多少行数据后输出一个表头信息。
我们可以比较 Java 进程的启动时长以及总 GC 时长 (GCT 列),或者两次测量的间隔时长以及总 GC 时长的增量,来得出 GC 时长占运行时长的比例。
如果该比例超过 20%,则说明目前堆的压力较大;
如果该比例超过 98%,则说明这段时期内几乎一直在GC,堆里几乎没有可用空间,随时都可能抛出 OOM 异常。
示例:统计两次测量的时间间隔内,GC 时长占运行时长的比例:
使用jstat统计GC信息,并显示进程启动时间、统计间隔1000ms、统计20次
每隔一段较长的时间采样多组 OU(老年代内存量) 的最小值,如果这些最小值在上涨,说明无法回收对象在不断增加,可能是内存泄漏导致的。
在长时间运行的 Java 程序中,我们可以运行 jstat 命令连续获取多行性能数据,并取这几行数据中 OU 列(Old Used,已占用的老年代内存)的最小值
然后,我们每隔一段较长的时间重复一次上述操作,来获得多组 OU 最小值。如果这些值呈上涨趋势,则说明该 Java 程序的老年代内存已使用量在不断上涨,这意味着无法回收的对象在不断增加,因此很有可能存在内存泄漏(不再使用的对象仍然被引用,导致GC无法回收)。
官方帮助文档:
https://docs.oracle.com/en/java/javase/11/tools/jstack.html
jstack(JVM Stack Trace):用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。
线程快照:该进程内每条线程正在执行的方法堆栈的集合。
生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用 jstack 显示各个线程调用的堆栈情况。
在 thread dump 中,要留意下面几种状态
Object.wait()
或 TIMED_WAITING
option 参数 | 作用 |
---|---|
-F | 当正常输出的请求不被响应时,强制输出线程堆栈 |
-l | 除堆栈外,显示关于锁的附加信息 |
-m | 如果调用本地方法的话,可以显示 C/C++ 的堆栈 |
代码示例(死锁)
运行后,在命令行使用该命令:
输出的代码:
2022-01-31 21:51:08
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.231-b11 mixed mode):
"DestroyJavaVM" #15 prio=5 os_prio=0 tid=0x0000000002b12800 nid=0x5b50 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #13 prio=5 os_prio=0 tid=0x000000001e041800 nid=0x9bc waiting for monitor entry [0x000000001fd1f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.youngkbt.jstack.ThreadDeadLock$2.run(ThreadDeadLock.java:63)
- waiting to lock <0x000000076ba1e2f0> (a java.lang.StringBuilder)
- locked <0x000000076ba1e338> (a java.lang.StringBuilder)
at java.lang.Thread.run(Thread.java:748)
"Thread-0" #12 prio=5 os_prio=0 tid=0x000000001e03b800 nid=0x52f8 waiting for monitor entry [0x000000001fc1f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.youngkbt.jstack.ThreadDeadLock$1.run(ThreadDeadLock.java:35)
- waiting to lock <0x000000076ba1e338> (a java.lang.StringBuilder)
- locked <0x000000076ba1e2f0> (a java.lang.StringBuilder)
"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x000000001df8b000 nid=0x3408 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x000000001df47000 nid=0x533c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001df44800 nid=0x4ef8 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000000001df42800 nid=0x22b8 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000000001df40000 nid=0x3494 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001df35800 nid=0x2da8 runnable [0x000000001f4fe000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x000000076b904d88> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x000000076b904d88> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:47)
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001de9e000 nid=0xac waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001def2000 nid=0x2908 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001c7c3800 nid=0x1610 in Object.wait() [0x000000001f1df000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076b788ed8> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x000000076b788ed8> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000000001de83000 nid=0x31e4 in Object.wait() [0x000000001f0de000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076b786c00> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x000000076b786c00> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=2 tid=0x000000001de62800 nid=0x575c runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000002b28800 nid=0x1768 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000000002b2a000 nid=0x97c runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000000002b2c000 nid=0x4364 runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000000002b2d800 nid=0x4608 runnable
"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x0000000002b2f800 nid=0x4f38 runnable
"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000000002b32000 nid=0xb80 runnable
"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000000002b35000 nid=0xce4 runnable
"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000000002b36000 nid=0x5510 runnable
"GC task thread#8 (ParallelGC)" os_prio=0 tid=0x0000000002b37800 nid=0x193c runnable
"GC task thread#9 (ParallelGC)" os_prio=0 tid=0x0000000002b38800 nid=0x1010 runnable
"VM Periodic Task Thread" os_prio=2 tid=0x000000001e008000 nid=0x1144 waiting on condition
JNI global references: 12
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x000000001e044d58 (object 0x000000076ba1e2f0, a java.lang.StringBuilder),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000000001c7c2dc8 (object 0x000000076ba1e338, a java.lang.StringBuilder),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.youngkbt.jstack.ThreadDeadLock$2.run(ThreadDeadLock.java:63)
- waiting to lock <0x000000076ba1e2f0> (a java.lang.StringBuilder)
- locked <0x000000076ba1e338> (a java.lang.StringBuilder)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at com.youngkbt.jstack.ThreadDeadLock$1.run(ThreadDeadLock.java:35)
- waiting to lock <0x000000076ba1e338> (a java.lang.StringBuilder)
- locked <0x000000076ba1e2f0> (a java.lang.StringBuilder)
Found 1 deadlock.
部分图:(可以看出 BLOCKED 进入死锁阻塞)
其他代码示例:
因为内容结果太长,所以只给代码,在命令行的输入可以自行练习查看(也可以使用 option 参数查看额外内容)。
线程睡眠代码:
public class TreadSleepTest {
public static void main(String[] args) {
System.out.println("hello - 1");
try {
Thread.sleep(1000 * 60 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello - 2");
}
}
线程同步代码:
public class ThreadSyncTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class Number implements Runnable {
private int number = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
if (number <= 100) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
} else {
break;
}
}
}
}
}
在控制台输出结果的代码:
public class AllStackTrace {
public static void main(String[] args) {
Map all = Thread.getAllStackTraces();
Set> entries = all.entrySet();
for(Map.Entry en : entries){
Thread t = en.getKey();
StackTraceElement[] v = en.getValue();
System.out.println("【Thread name is :" + t.getName() + "】");
for(StackTraceElement s : v){
System.out.println("\t" + s.toString());
}
}
}
}
MAT简介:MAT可以解析Heap Dump(堆转储)文件dump.hprof,查看GC Roots、引用链、对象信息、类信息、线程信息。可以快速生成内存泄漏报表。
MAT(Memory Analyzer Tool)工具是一款功能强大的 Java 堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。
MAT 可以分析 heap dump 文件。在进行内存分析时,只要获得了反映当前设备内存映像的 hprof 文件,通过 MAT 打开就可以直观地看到当前的内存信息。一般说来,这些内存信息包含:
jmap(JVM Memory Map):作用一方面是获取 dump 文件(堆转储快照文件,二进制文件),它还可以获取目标 Java 进程的内存相关信息,包括 Java 堆各区域的使用情况、堆中对象的统计信息、类加载信息等。开发人员可以在控制台中输入命令 jmap -help
查阅 jmap 工具的具体使用方式和一些标准选项配置。
基本语法
基本使用语法为:
jmap [option]
jmap [option]
jmap [option] [server_id@]
选项 | 作用 |
---|---|
-dump | 生成 dump 文件(Java 堆转储快照),-dump:live 只保存堆中的存活对象 |
-heap | 输出整个堆空间的详细信息,包括 GC 的使用、堆配置信息,以及内存的使用信息等 |
-histo | 输出堆空间中对象的统计信息,包括类、实例数量和合计容量,-histo:live 只统计堆中的存活对象 |
-J |
传递参数给 jmap 启动的 jvm |
-finalizerinfo | 显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象,仅 linux/solaris 平台有效 |
-permstat | 以 ClassLoader 为统计口径输出永久代的内存状态信息,仅 linux/solaris 平台有效 |
-F | 当虚拟机进程对-dump 选项没有任何响应时,强制执行生成 dump 文件,仅 linux/solaris 平台有效 |
-h | -help | jmap 工具使用的帮助命令 |
-j |
传递参数给 jmap 启动的 JVM |
说明:这些参数和 linux 下输入显示的命令多少会有不同,包括也受 JDK 版本的影响。
JVM参数:OOM后生成、FGC前生成
使用 Visual VM 可以导出堆 dump 文件。
1.首先启动程序(需确保程序一直在运行中)
2.打开JvisualVM工具
3.打开对应的程序进程
4.点击线程->线程dump
// 开启在出现 OOM 错误时生成堆转储文件
-Xmx1024m
-XX:+HeapDumpOnOutOfMemoryError
// 将生成的堆转储文件保存到 /tmp 目录下,并以进程 ID 和时间戳作为文件名
-XX:HeapDumpPath=/tmp/java_%p_%t.hprof
// 在进行 Full GC 前生成堆转储文件
// 注:如果没有开启自动 GC,则此参数无效。JDK 9 之后该参数已被删除。
-XX:+HeapDumpBeforeFullGC
调优JVM参数主要关注停顿时间和吞吐量,两者不可兼得,提高吞吐量会拉长停顿时间。
//调整内存大小
-XX:MetaspaceSize=128m(元空间默认大小)
-XX:MaxMetaspaceSize=128m(元空间最大大小)
-Xms1024m(堆最大大小)
-Xmx1024m(堆默认大小)
-Xmn256m(新生代大小)
-Xss256k(栈最大深度大小)
//调整内存比例
//伊甸园:幸存区
-XX:SurvivorRatio=8(伊甸园:幸存区=8:2)
//新生代和老年代的占比
-XX:NewRatio=4 //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
//修改垃圾回收器
//设置Serial垃圾收集器(新生代)
//-XX:+UseSerialGC
//设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
//-XX:+UseParallelOldGC
//CMS垃圾收集器(老年代)
//-XX:+UseConcMarkSweepGC
//设置G1垃圾收集器
-XX:+UseG1GC
//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
-XX:MaxGCPauseMillis
//进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,JDK8默认值15,JDK9默认值7
-XX:InitialTenuringThreshold=7
//新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
-XX:PretenureSizeThreshold=1000000
//使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
-XX:CMSInitiatingOccupancyFraction
//G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
-XX:G1MixedGCLiveThresholdPercent=65
//Heap Dump(堆转储)文件
//当发生OutOfMemoryError错误时,自动生成堆转储文件。
-XX:+HeapDumpOnOutOfMemoryError
//错误输出地址
-XX:HeapDumpPath=/Users/a123/IdeaProjects/java-test/logs/dump.hprof
//GC日志
-XX:+PrintGCDetails(打印详细GC日志)
-XX:+PrintGCTimeStamps:打印GC时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps:打印GC时间戳(以日期格式)
-Xlog:gc:(打印gc日志地址)
STW:Stop The World,暂停其他所有工作线程直到收集结束。垃圾收集器做垃圾回收中断应用执行的时间。
可以通过-XX:MaxGCPauseMillis参数进行设置,以毫秒为单位,至少大于1。
//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
-XX:MaxGCPauseMillis=10
G1回收器默认200ms停顿时长。
吞吐量=运行时长/(运行时长+GC时长)。
通过-XX:GCTimeRatio=n参数可以设置吞吐量,99代表吞吐量为99%, 一般吞吐量不能低于95%。
示例:
-XX:GCTimeRatio=99
吞吐量太高会拉长停顿时间,造成用户体验下降。
根据程序运行时老年代存活对象大小(记为x)进行调整,整个堆内存大小设置为X的3~4倍。年轻代占堆内存的3/8。
示例:
//调整内存大小
-XX:MetaspaceSize=128m(元空间默认大小)
-XX:MaxMetaspaceSize=128m(元空间最大大小)
-Xms1024m(堆最大大小)
-Xmx1024m(堆默认大小)
-Xmn256m(新生代大小)
-Xss256k(栈最大深度大小)
调整伊甸园区和幸存区比例、新生代和老年代比例。
Young GC频繁时,我们可以提高新生代在堆内存中的比例、提高伊甸园区在新生代的比例,令新生代不那么快被填满。
默认情况,伊甸园区:S0:S1=8:1:1,新生代:老年代=1:2。
示例:
//调整内存比例
//伊甸园:幸存区
-XX:SurvivorRatio=8(伊甸园:幸存区=8:2)
//新生代和老年代的占比
-XX:NewRatio=4 //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
JDK8时Young GC默认把15岁的对象移动到老年代。JDK9默认值改为7。
当Full GC频繁时,我们提高升老年龄,让年轻代的对象多在年轻代待一会,从而降低Full GC频率。
//进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,JDK8默认值15,JDK9默认值7
-XX:InitialTenuringThreshold=7
Young GC时大对象会不顾年龄直接移动到老年代。当Full GC频繁时,我们关闭或提高大对象阈值,让老年代更迟填满。
默认是0,即大对象不会直接在YGC时移到老年代。
//新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
-XX:PretenureSizeThreshold=1000000
CMS的并发标记和并发清除阶段是用户线程和回收线程并发执行,如果老年代满了再回收会导致用户线程被强制暂停。所以我们修改回收条件为老年代的60%,保证回收时预留足够空间放新对象。CMS默认是老年代68%时触发回收机制。
//使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
-XX:CMSInitiatingOccupancyFraction
超过存活阈值的Region,其内对象会被混合回收到老年代。G1回收时也要预留空间给新对象。存活阈值默认85%,即当一个内存区块中存活对象所占比例超过 85% 时,这些对象就会通过 Mixed GC 内存整理并晋升至老年代内存区域。
//G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
-XX:G1MixedGCLiveThresholdPercent=65
JVM调优最实用、最有效的方式是升级垃圾回收器,根据CPU核数,升级当前版本支持的最新回收器。
示例:
设置Serial垃圾收集器(新生代)
//修改垃圾回收器
//设置Serial垃圾收集器(新生代)
-XX:+UseSerialGC
设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
//修改垃圾回收器
//设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
-XX:+UseParallelOldGC
设置CMS垃圾收集器(老年代)
//CMS垃圾收集器(老年代)
-XX:+UseConcMarkSweepGC
设置G1垃圾收集器
//修改垃圾回收器
//设置G1垃圾收集器
-XX:+UseG1GC
使用MAT分析堆转储日志中的大对象,看是否合理。大对象会直接进入老年代,导致Full GC频繁。
内存溢出: 申请的内存大于系统能提供的内存。
溢出原因:
使用JDK自带的命令行调优工具 ,判断是否有OOM:
MAT定位导致OOM:示例代码:写死循环创建对象,不断添加到list里,导致堆内存溢出;
-XX:+HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath
解决方案:
内存泄漏: 不再使用的对象仍然被引用,导致GC无法回收;
内存泄露的9种情况:
性能分析工具判断是否有内存泄漏:
解决办法:
CPU利用率过高,大量线程并发执行任务导致CPU飙升。例如锁等待(例如CAS不断自旋)、多线程都陷入死循环、Redis被攻击、网站被攻击、文件IO、网络IO。
jvm.gc.time:每分钟的GC耗时在1s以内,500ms以内尤佳
jvm.gc.meantime:每次YGC耗时在100ms以内,50ms以内尤佳
jvm.fullgc.count:最多几小时FGC一次,1天不到1次尤佳
jvm.fullgc.time:每次FGC耗时在1s以内,500ms以内尤佳
最差情况下能接受的GC频率:Young GC频率10s一次,每次500ms以内。Full GC频率10min一次,每次1s以内。
其实一小时一次Full GC已经算频繁了,一个不错的应用起码得控制一天一次Full GC。
上午8点是我们的业务高峰,一到高峰的时候,用户感觉到明显卡顿,监控工具(例如Prometheus和Grafana)发现TP99(99%请求在多少ms内完成)时长明显变高,有明显的的毛刺;内存使用率也不稳定,会周期性增大再降低,于是怀疑是GC导致。
通过jstat -gc观察服务器的GC情况,发现Young GC频率提高成原来的10倍,Full GC频率提高成原来的四倍。正常YGC 10min一次,FGC 10h一次。异常YGC 1min一次,FGC 3h一次;
所以主要问题是Young GC频繁,进而导致Full GC频繁。Full GC频繁会触发STW,导致TP99耗时上升。
调优后我们重新进行了一次压测,发现TP99耗时较之前降低60%。FullGC耗时降低80%,YoungGC次数减少30%。TP99耗时基本持平,完全符台预期。
绝大部分问题都出自代码。
日常开发中,要尽量减少非必要对象的创建,防止死循环创建对象,注意内存泄漏的12个场景,防止内存泄漏。
在一些内存占用率高的场景下需要以时间换空间,控制内存使用。
在集群下新增加几个服务器,分散节点压力,可以提高整体效率。
合理设置线程池的线程数量。
下面的参数只是一个预估值,适合初步设置,具体的线程数需要经过压测确定,压榨(更好的利用)CPU的性能。
记CPU核心数为N;
核心线程数:
最大线程数:设成核心线程数的2-4倍。数量主要由CPU和IO的密集性、处理的数据量等因素决定。
需要增加线程的情况:jstack打印线程快照,如果发现线程池中大部分线程都等待获取任务、则说明线程够用。如果大部分线程都处于运行状态,可以继续适当调高线程数量。
jstack:打印指定进程此刻的线程快照。定位线程长时间停顿的原因,例如死锁、等待资源、阻塞。如果有死锁会打印线程的互相占用资源情况。线程快照:该进程内每条线程正在执行的方法堆栈的集合。
使用中间件提高程序效率,比如缓存、消息队列等