一、JVM常用参数
1、开始
查看当前JDK版本所支持的 垃圾回收 器有哪些、以及默认使用的回收器
js
java -XX:+PrintFlagsFinal -version | grep -E '\<Use.*GC\>'
JDK8和JDK11运行上述命令结果如下:
JDK8:
JDK11:
如果执行:
js
java -XX:+PrintFlagsFinal -version
会输出所有当前JVM参数配置。
2、参数配置:
-
-Xms:初始堆大小
-
-Xmx:最大堆大小
-
-XX:NewSize 设置新生代最小空间大小。
-
-XX:MaxNewSize 设置新生代最大空间大小。
-
-XX:NewRatio=n: 设置年轻代和年老代的比值。如:为3表示年轻代和年老代比值为1:3,年轻代占整个年轻代年老代和的 1/4
-
-XX:SurvivorRatio=n: 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。如3表示 Eden:3 Survivor:2,-个Survivor 区占整个年轻代的1/5(默认为8 Eden占新生代的8/10)
-
-XX:PermSize 设置永久代最小空间大小。
-
-XX:MaxPermSize 设置永久代最大空间大小。
-
-Xss 设置每个线程的堆栈大小,也就是栈最大深度大小。
-
- XX:+UseSerialGC: 启用串行垃圾收集器
-
- XX:MaxHeapFreeRatio: 堆最大空闲比率
-
- XX:MinHeapFreeRatio: 堆最小空闲比率
-
- XX:+UseParallelGC: 启用并行垃圾收集器
-
-XX:ParallelGCThreads=n: 设置并行收集器收集时使用的 CPU 数。并行收集线程数
-
-XX:MaxGCPauseMilis=n: 设置并行收集最大的暂停时间 (如果到这个时间了,垃圾回收器依然没有回收完,也会停止回收)
-
-XX:GCTimeRatio=n: 设置垃圾回收时间占程序运行时间的百分比。公式为:1/(1+n)
-
-XX:+CMSIncrementalMode: 设置为增量模式。适用于单 CPU 情况
-
- XX:+UseConcMarkSweepGC:启用并发标记清除收集器
-
- XX:CMSInitiatingOccupancyFraction: 触发CMS垃圾回收的堆内存占用百分比
-
- XX:+UseG1GC: 启用G1垃圾收集器
-
- XX:MaxGCPauseMillis: 最大垃圾回收停顿时间
-
- XX:G1HeapRegionSize: 设置G1区域的大小
为什么要区分新生代和老生代?
堆中区分的新生代和老年代是为了垃圾回收,新生代中的对象存活期一般不长,
而老年代中的对象存活期较长,所以当垃圾回收器回收内存时,新生代中垃圾回收效果较好,
会回收大量的内存,而老年代中回收效果较差,内存回收不会太多。
3、收集器设置:
3.1串行收集器
3.1.1 使用场景
单核处理器环境、 实时性要求高的应用、内存较小的应用
3.1.2 参数配置
-
- XX:+UseSerialGC:启用串行垃圾收集器
-
- XX:MaxHeapFreeRatio:堆最大空闲比率
-
- XX:MinHeapFreeRatio:堆最小空闲比率
3.1.3 性能分析
单线程收集,效率较低、 停顿时间短、 CPU使用率低
3.1.4 优缺点
优点:简单高效,适合小型应用
缺点:并发能力差,不适合多核处理器;内存回收过程中会触发全线程暂停
3.2 并行收集器参数
3.2.1 使用场景
多核处理器环境、 对吞吐量有较高要求的应用、 大内存应用
3.2.2 参数配置
-
- XX:+UseParallelGC:启用并行垃圾收集器
-
-XX:ParallelGCThreads=n: 设置并行收集器收集时使用的 CPU 数。并行收集线程数
-
-XX:MaxGCPauseMilis=n: 设置并行收集最大的暂停时间 (如果到这个时间了,垃圾回收器依然没有回收完,也会停止回收)
-
-XX:GCTimeRatio=n: 设置垃圾回收时间占程序运行时间的百分比。公式为:1/(1+n)
-
-XX:+CMSIncrementalMode: 设置为增量模式。适用于单 CPU 情况
3.2.3 性能分析
多线程并行收集,效率高、 吞吐量高、 停顿时间相对较长
3.2.4 优缺点
优点:适合多核处理器,提高垃圾回收效率
缺点:CPU资源消耗较大、停顿时间较长
3.3 并发收集器参数
3.3.1 使用场景
对响应时间有较高要求的应用、 多核处理器环境、 大内存应用
3.3.2 参数配置
-
- XX:+UseConcMarkSweepGC:启用并发标记清除收集器
-
- XX:+CMSIncrementalMode:CMS增量模式
-
- XX:CMSInitiatingOccupancyFraction:触发CMS垃圾回收的堆内存占用百分比
3.3.3 性能分析
并发标记和清除,减少停顿时间、 对CPU资源友好、 吞吐量相对较低
3.3.4 优缺点
优点:减少停顿时间,适合交互式应用
缺点:在并发阶段可能会出现浮动垃圾、CMS回收失败可能导致堆内存溢出
3.4 G1收集器参数
3.4.1 使用场景
需要低延迟的大堆内存应用、 多核处理器环境、 对停顿时间有严格要求的应用
3.4.2 参数配置
-
- XX:+UseG1GC:启用G1垃圾收集器
-
- XX:MaxGCPauseMillis:最大垃圾回收停顿时间
-
- XX:G1HeapRegionSize:设置G1区域的大小
3.4.3 性能分析
服务端垃圾收集器,面向服务端应用
提前预测和满足停顿时间要求
自适应调整不同区域的垃圾收集频率
3.4.4 优缺点
优点:服务端应用,低延迟,高吞吐量
缺点:内存碎片问题、对硬件资源要求较高
3.5 CMS收集器
CMS全称 Concurrent Mark Sweep ,是一款并发的、主要使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,在初始化阶段会导致STW。
CMS收集器 说的是新生代的ParNew收集器和
老年代的CMS收集器的结合体, 是标记-清除算法的垃圾回收器。
3.5.1 使用场景
- JDK8及更高版本同等环境下只要cpu性能比较好并且内存不算大 (最少4G)可以使用CMS
- JDK7及更低版本同等环境下 可选择CMS (G1不完善)
3.5.2 参数配置
-XX:+UseParNewGC 表示使用并行收集器 ParNew 对新生代进行垃圾回收,
-XX:+UseConcMarkSweepGC 使用并发标记清除收集器 CMS 对老年代进行垃圾回收。
-XX:ParallelGCThreads 表示 Young GC 与 CMS GC 工作时的并行线程数,
-XX:ParallelCMSThreads建议根据处理器数量进行合理设置。
-XX:+UseCMSCompactAtFullCollection
由于 CMS GC 会产生内存碎片,且只在 Full GC 时才会进行内存碎片压缩(因此 使用 CMS 垃圾回收器避免不了 Full GC)。这个参数表示开启 Full GC 时的压缩功能,减少内存碎片。
-XX:CMSInitiatingOccupancyFraction 表示触发 CMS GC 的老年代使用阈值,一般设置为 70~80(百分比),设置太小会增加 CMS GC 发现的频率,设置太大可能会导致并发模式失败或晋升失败。
-XX:+UseCMSInitiatingOccupancyOnly 表示 CMS GC 只基于 CMSInitiatingOccupancyFraction 触发,如果未设置该参数则 JVM 只会根据 CMSInitiatingOccupancyFraction 触发第一次 CMS GC ,后续还是会自动触发。建议同时设置这两个参数。
-XX:+CMSClassUnloadingEnabled 表示开启 CMS 对永久代的垃圾回收(或元空间),避免由于永久代空间耗尽带来 Full GC。
-XX:+CMSParallelRemarkEnabled 开启降低标记停顿。
3.5.3 优缺点
优点:并发收集,低停顿
缺点:
- 标记清除,产生大量内存碎片。(导致fullGc)
- 无法处理浮动垃圾,内存不足时出现"Concurrent Mode Failure"(并发模式故障),切换到SerialOld收集模式
- CMS收集器对CPU资源非常敏感。CPU个数少于4个时,CMS对于用户程序的影响就可能变得很大,为了应付这种情况,虚拟机提供了一种称为"增量式并发收集器"的CMS收集器变种。
打印 GC 回收的过程日志信息
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename
JDK自带的命令行调优工具
常用命令总结
**jps:**查看正在运行的 Java 进程。jps -v查看进程启动时的JVM参数;
jstat:查看指定进程的 JVM 统计信息。jstat -gc查看堆各分区大小、YGC,FGC次数和时长。如果服务器没有 GUI 图形界面,只提供了纯文本控制台环境,它是运行期定位虚拟机性能问题的首选工具。
jinfo:实时查看和修改指定进程的 JVM 配置参数。jinfo -flag查看和修改具体参数。
jstack:打印指定进程此刻的线程快照。定位线程长时间停顿的原因,例如死锁、等待资源、阻塞。如果有死锁会打印线程的互相占用资源情况。
jps:查看正在运行的 Java 进程
jps(Java Process Status):显示指定系统内所有的 HotSpot 虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。
说明:对于本地虚拟机进程来说,进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的,是唯一的。
基本使用语法为:
bash
jps [options参数] [hostid参数]
代码示例
一个阻塞状态的线程,等待用户输入:
java
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 参数
jstat:查看 JVM 统计信息
概述
jstat (JVM Statistics Monitoring Tool):用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译 等运行数据。在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上 ,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。
基本使用语法为:
js
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
#[-t]是程序开启到采样的运行时间 interval查询间隔 count查询次数 [-h<lines>]周期性输出,每隔多少行打印一次表头
查看命令相关参数:jstat-h 或 jstat-help。其中 vmid 是进程 id 号,也就是 jps 之后看到的前面的号码,如下:
option参数:
选项 option 可以由以下值构成:
类装载相关的:
- -class:显示 ClassLoader 的相关信息:类的装载、卸载数量、总空间、类装载所消耗的时间等
垃圾回收相关的:
- -gc:显示堆各分区大小、YGC,FGC次数和时长。包括 Eden 区、两个 Survivor 区、老年代、永久代等的容量、已用空间、GC 时间合计等信息
- -gccapacity:显示内容与 -gc 基本相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间
- -gcutil:显示内容与 -gc 基本相同,但输出主要关注已使用空间占总空间的百分比
- -gccause:与 -gcutil 功能一样,但是会额外输出导致最后一次或当前正在发生的 GC 产生的原因
- -gcnew:显示新生代 GC 状况
- -gcnewcapacity:显示内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间
- -gcold:显示老年代 GC 状况
- -gcoldcapacity:显示内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间
- -gcpermcapacity:显示永久代使用到的最大、最小空间
JIT 相关的:
- -compiler:显示 JIT 编译器编译过的方法、耗时等信息
- -printcompilation:输出已经被 JIT 编译的方法
例如 jstat -class
js
jstat -class 86517
Loaded Bytes Unloaded Bytes Time
18051 32345.3 0 0.0 112.14
jstat -compiler 显示 JIT 编译器编译过的方法、耗时等信息。
jstat -printcompilation
输出已经被 JIT 编译的方法。
jstat -gc
显示与 GC 相关的堆信息。包括 Eden 区、两个 Survivor 区、老年代、永久代等的容量、已用空间、GC 时间合计等信息。
利用后台命令查询:
上图相关信息含义如下表所示:
表头 | 含义(字节) |
---|---|
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 的总时间 |
JDK自带的可视化监控工具
- jconsole
- Visual VM:Visual VM可以监视应用程序的 CPU、GC、堆、方法区、线程快照,查看JVM进程、JVM 参数、系统属性。
二、JVM 调优相关
调优的目的是什么?
答:JVM调优的目的是通过调整JVM的配置参数和优化应用程序代码,使其在给定的硬件和软件环境下达到更好的性能表现。
JVM调优的目的:提高系统的吞吐量、降低系统的响应时间、提高系统的稳定性、降低系统的资源消耗。
调优JVM参数主要关注停顿时间和吞吐量,两者不可兼得,提高吞吐量会拉长停顿时间。
**2.1 减少停顿时间:**MaxGCPauseMillis
**STW:**Stop The World,暂停其他所有工作线程直到收集结束。垃圾收集器做垃圾回收中断应用执行的时间。
可以通过-XX:MaxGCPauseMillis参数进行设置,以毫秒为单位,至少大于1。
java
//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
-XX:MaxGCPauseMillis=10
G1回收器默认200ms停顿时长。
**2.2 提高吞吐量:**GCTimeRatio
吞吐量=运行时长/(运行时长+GC时长)。
通过-XX:GCTimeRatio=n参数可以设置吞吐量,99代表吞吐量为99%, 一般吞吐量不能低于95%。
示例:
js
-XX:GCTimeRatio=99
吞吐量太高会拉长停顿时间,造成用户体验下降。
2.3 调整堆内存大小
根据程序运行时老年代存活对象大小(记为x)进行调整,整个堆内存大小设置为X的3~4倍。年轻代占堆内存的3/8。
- -Xms:初始堆内存大小。默认:物理内存小于192MB时,默认为物理内存的1/2;物理内存大192MB且小于128GB时,默认为物理内存的1/4;物理内存大于等于128GB时,都为32GB。
- -Xmx:最大堆内存大小,建议保持和初始堆内存大小一样。因为从初始堆到最大堆的过程会有一定的性能开销,而且现在内存不是稀缺资源。
- -Xmn:年轻代大小。JDK官方建议年轻代占整个堆大小空间的3/8左右。
示例:
js
//调整内存大小
-XX:MetaspaceSize=128m(元空间默认大小)
-XX:MaxMetaspaceSize=128m(元空间最大大小)
-Xms1024m(堆最大大小)
-Xmx1024m(堆默认大小)
-Xmn256m(新生代大小)
-Xss256k(栈最大深度大小)
2.4 MAT分析堆转储文件
2.4.1 简介
**MAT简介:**MAT可以解析Heap Dump(堆转储)文件dump.hprof,查看GC Roots、引用链、对象信息、类信息、线程信息。可以快速生成内存泄漏报表。
MAT(Memory Analyzer Tool)工具是一款功能强大的 Java 堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。
MAT 可以分析 heap dump 文件。在进行内存分析时,只要获得了反映当前设备内存映像的 hprof 文件,通过 MAT 打开就可以直观地看到当前的内存信息。一般说来,这些内存信息包含:
- 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值
- 所有的类信息,包括 classloader、类名称、父类、静态变量等
- GCRoot 到所有的这些对象的引用路径
- 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)
2.4.2 生成dump文件方式
方法一:jmap
jmap (JVM Memory Map):作用一方面是获取 dump 文件(堆转储快照文件,二进制文件),它还可以获取目标 Java 进程的内存相关信息,包括 Java 堆各区域的使用情况、堆中对象的统计信息、类加载信息等。
我们开发人员可以在控制台中输入命令 jmap -help 查阅 jmap 工具的具体使用方式和一些标准选项配置。
基本语法
基本使用语法为:
jmap [option] <pid>
jmap [option] <executable <core>
jmap [option] [server_id@] <remote server IP or hostname>
选项 | 作用 |
---|---|
-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 |
-j | 传递参数给 jmap 启动的 JVM |
说明:这些参数和 linux 下输入显示的命令多少会有不同,包括也受 JDK 版本的影响。
方法二:MAT直接从Java进程导出dump文件
js
// 开启在出现 OOM 错误时生成堆转储文件
-Xmx1024m
-XX:+HeapDumpOnOutOfMemoryError
// 将生成的堆转储文件保存到 /tmp 目录下,并以进程 ID 和时间戳作为文件名
-XX:HeapDumpPath=/tmp/java_%p_%t.hprof
// 在进行 Full GC 前生成堆转储文件
// 注:如果没有开启自动 GC,则此参数无效。JDK 9 之后该参数已被删除。
-XX:+HeapDumpBeforeFullGC
2.5 排查大对象
使用MAT分析堆转储日志中的大对象,看是否合理。大对象会直接进入老年代,导致Full GC频繁。
2.5.1 内存溢出
概念
内存溢出: 申请的内存大于系统能提供的内存。
溢出原因:
-
本地直接内存溢出:本地直接内存设的太小导致溢出。设置直接内存最大值-XX:MaxDirectMemorySize,若不指定则默认与Java堆最大值一致。
-
虚拟机栈和本地方法栈溢出 :如果虚拟机的栈内存允许动态扩展,并且方法递归层数太深时,导致扩展栈容量时无法申请到足够内存。
-
方法区溢出:运行时生成大量动态类时会内存溢出。
- CGlib动态代理 :CGlib动态代理产生大量类填满了整个方法区(方法区存常量池、类信息、方法信息),直到溢出。CGlib动态代理 是在内存中构建子类对象实现对目标对象功能扩展,如果enhancer.setUseCache(false); 即关闭用户缓存,那么每次创建代理对象都是一个新的实例,创建过多就会导致方法区溢出。注意JDK动态代理不会导致方法区溢出。
- JSP:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)。
-
堆溢出:
- 死循环创建过多对象;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 内存中加载的数据量过于庞大,如一次从数据库取出的数据集太大、第三方接口接口传输的大对象、接收的MQ消息太大;
- Tomcat参数设置不当导致OOM:Tomcat会给每个线程创建两个默认4M大小的缓冲区,高并发情况下会导致缓冲区创建过多,导致OOM。
-
程序计数器不会内存溢出。
OOM的排查和解决
使用JDK自带的命令行调优工具 ,判断是否有OOM:
- 使用jsp命令查看当前Java进程;
- 使用jstat命令多次统计GC,比较GC时长占运行时长的比例;
- 如果比例超过20%,就代表堆压力已经很大了;
- 如果比例超过98%,说明这段时期内几乎一直在GC,堆里几乎没有可用空间,随时都可能抛出 OOM 异常。
**MAT定位导致OOM:**示例代码:写死循环创建对象,不断添加到list里,导致堆内存溢出;
-
JVM参数设置,内存溢出后生成dump文件,设置路径;
js-XX:+HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath
-
MAT解析dump文件;
-
**定位大对象:**点击直方图图标(Histogram),对象会按内存大小排序,查看内存占用最大的对象;
上次生产内存溢出的截图:
-
**这个对象被谁引用:**点击支配树(dominator tree),看大对象被哪个线程调用。这里可以看到是被主线程调用。
-
**定位具体代码:**点击概述图标(thread_overview),看线程的方法调用链和堆栈信息,查看大对象所属类和第几行,定位到具体代码,解决问题。
解决方案:
- 通过jinfo 命令查看并修改JVM 参数,直接增加内存。如-Xmx256m
- 检查错误日志,查看"OutOfMemory"错误前是否有其它异常或错误。
- 对代码进行走查和分析,找出可能发生内存溢出的位置。
- 使用内存查看工具动态查看内存使用情况。
2.5.2 内存泄漏
概念
内存泄漏: 不再使用的对象仍然被引用,导致GC无法回收;
内存泄露的9种情况:
- 静态容器里的对象 :静态集合类的生命周期与 JVM 程序一致,容器里的对象引用也将一直被引用得不到GC ;Java里不准静态方法引用非静态方法也是防止内存泄漏。
- 单例对象引用的外部对象:单例模式里,如果单例对象如果持有外部对象的引用,因为单例对象不会被回收,那么这个外部对象也不会被回收。
- 外部类跟随内部类被引用:内部类持有外部类,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
- 数据库、网络、IO等连接忘记关闭 :在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用 close 方法来释放与数据库的连接。如果对 Connection 、Statement 或 ResultSet 不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
- 变量作用域不合理:例如一个变量只会在某个方法中使用,却声明为成员变量,并且被使用后没有被赋值为null,将会导致这个变量明明已经没用了,生命周期却还跟对象一致。
- HashSet中对象改变哈希值:当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则对象哈希值改变,找不到对应的value。
- 缓存引用忘删除:一旦你把对象引用放入到缓存中,他就很容易遗忘,缓存忘了删除,将导致引用一直存在。
- 逻辑删除而不是真实删除:监听器和其他回调:如果客户端在你实现的 API 中注册回调,却没有显示的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为 软WeakHashMap 中的键。例如出栈只是移动了指针,而没有将出栈的位置赋值null,导致已出栈的位置还存在引用。
- 线程池时,ThreadLocal忘记remove():使用线程池的时候,ThreadLocal 需要在使用完线程中的线程变量手动 remove(),否则会内存泄漏。因为线程执行完后没有销毁而是被线程池回收,导致ThreadLocal中的对象不能被自动垃圾回收。
内存泄漏的排查和解决
性能分析工具判断是否有内存泄漏:
-
JDK自带的命令行调优工具:
- 每隔一段较长的时间通过jstat命令采样多组 OU(老年代内存量) 的最小值;
- 如果这些最小值在上涨,说明无法回收对象在不断增加,可能是内存泄漏导致的。
-
MAT监视诊断内存泄漏:
- 生成堆转储文件:MAT直接从Java进程导出dump文件
- 可疑点:查看泄漏怀疑(Leak Suspects),找到内存泄漏可疑点
- 可疑线程:可疑点查看详情(Details),找到可疑线程
- 定位代码:查看线程调用栈(See stacktrace),找到问题代码的具体位置。
-
GC详细日志:启动参数开启GC详细日志,设置日志地址;-XX:+PrintGCDetails;
-
编译器警告:查看Eclipse等编译器的内存泄漏警告;
-
Java基准测试工具:分析代码性能;
解决办法:
- 牢记内存泄漏的场景,当一个对象不会被使用时,给它的所有引用赋值null,堤防静态容器,记得关闭连接、别用逻辑删除,只要用到了引用,变量的作用域要合理。
- 使用java.lang.ref包的弱引用WeakReference,下次垃圾收集器工作时被回收。
- 检查代码;
2.6 CPU飙升和GC频繁的调优方案
2.6.1 CPU飙升
原因
CPU利用率过高,大量线程并发执行任务导致CPU飙升。例如锁等待(例如CAS不断自旋)、多线程都陷入死循环、Redis被攻击、网站被攻击、文件IO、网络IO。
定位步骤
- 定位进程ID:通过top命令查看当前服务CPU使用最高的进程,获取到对应的pid(进程ID)
- 定位线程ID:使用top -Hp pid,显示指定进程下面的线程信息,找到消耗CPU最高的线程id
- 线程ID转十六进制 :转十六进制是因为下一步jstack 打印的线程快照 (线程正在执行方法的堆栈集合)里线程id是十六进制。
- 定位代码:使用jstack pid | grep tid(十六进制),打印线程快照,找到线程执行的代码。一般如果有死锁的话就会显示线程互相占用情况。
- 解决问题:优化代码、增加系统资源(增多服务器、增大内存)。
2.6.2 GC调优
GC频率的合理范围
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。
举例:
某一业务在高峰时段,一到高峰的时候,用户感觉到明显卡顿,监控工具(例如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耗时上升。
解决方案
- 排查内存泄漏、大对象、BUG;
- 增大堆内存:服务器加8G内存条,同时提高初始堆内存、最大堆内存。-Xms、-Xmx。
- 提高新生代比例:新生代和老年代默认比例是1:2。-XX:NewRatio=由4改为默认的2
- 降低升老年龄:让存活对象更快进入老年代。-XX:InitialTenuringThreshold=15(JDK8默认)改成7(JDK9默认)
- 设置大对象阈值:让大于1M的大对象直接进入老年代。-XX:PretenureSizeThreshold=0(默认)改为1000000(单位是字节)
- 垃圾回收器升级为G1:因为是JDK8,所以直接由默认的Parallel Scavenge+Parallel Old组合,升级为低延时的G1回收器。如果是
- JDK7版本,不支持G1,可以修改成ParNew+CMS或Parallel Scavenge+CMS,以降低吞吐量为代价降低停顿时间。-XX:CMSInitiatingOccupancyFraction
- 降低G1的存活阈值:超过存活阈值的Region,其内对象会被混合回收到老年代。降低存活阈值,更早进入老年代。-XX:G1MixedGCLiveThresholdPercent=90设为默认的85
2.7 启动脚本
js
nohup java -DgalaxyApplicationName=pms-standarddata -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -Xms512m -Xmx512m -Xmn256m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/hxb/cds/pms/pms-standarddata/logs/ -Dcom.alibaba.nacos.naming.log.level=warn -Dcom.alibaba.nacos.config.log.level=warn -Dspring.profiles.active=uat-db -Djasypt.encryptor.password=HFty2@yD52dv -jar /home/hxb/cds/pms/pms-standarddata/pms-standarddata.jar >/dev/null 2>&1 &