JVM,JVM常用参数,收集器设置,垃圾回收,jvm调优,MAT分析堆转储文件,jvm内存溢出、内存泄露解决方案,CPU飙升和GC频繁的调优方案

一、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 使用场景
  1. JDK8及更高版本同等环境下只要cpu性能比较好并且内存不算大 (最少4G)可以使用CMS
  2. 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:

  1. 使用jsp命令查看当前Java进程;
  2. 使用jstat命令多次统计GC,比较GC时长占运行时长的比例;
  3. 如果比例超过20%,就代表堆压力已经很大了;
  4. 如果比例超过98%,说明这段时期内几乎一直在GC,堆里几乎没有可用空间,随时都可能抛出 OOM 异常。

**MAT定位导致OOM:**示例代码:写死循环创建对象,不断添加到list里,导致堆内存溢出;

  1. JVM参数设置,内存溢出后生成dump文件,设置路径;

    js 复制代码
    -XX:+HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath
  2. MAT解析dump文件;

  3. **定位大对象:**点击直方图图标(Histogram),对象会按内存大小排序,查看内存占用最大的对象;

    上次生产内存溢出的截图:

  4. **这个对象被谁引用:**点击支配树(dominator tree),看大对象被哪个线程调用。这里可以看到是被主线程调用。

  5. **定位具体代码:**点击概述图标(thread_overview),看线程的方法调用链和堆栈信息,查看大对象所属类和第几行,定位到具体代码,解决问题。

解决方案:

  1. 通过jinfo 命令查看并修改JVM 参数,直接增加内存。如-Xmx256m
  2. 检查错误日志,查看"OutOfMemory"错误前是否有其它异常或错误。
  3. 对代码进行走查和分析,找出可能发生内存溢出的位置。
  4. 使用内存查看工具动态查看内存使用情况。

2.5.2 内存泄漏

概念

内存泄漏: 不再使用的对象仍然被引用,导致GC无法回收;

内存泄露的9种情况

  • 静态容器里的对象 :静态集合类的生命周期与 JVM 程序一致,容器里的对象引用也将一直被引用得不到GCJava里不准静态方法引用非静态方法也是防止内存泄漏。
  • 单例对象引用的外部对象:单例模式里,如果单例对象如果持有外部对象的引用,因为单例对象不会被回收,那么这个外部对象也不会被回收。
  • 外部类跟随内部类被引用:内部类持有外部类,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
  • 数据库、网络、IO等连接忘记关闭 :在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用 close 方法来释放与数据库的连接。如果对 ConnectionStatementResultSet 不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
  • 变量作用域不合理:例如一个变量只会在某个方法中使用,却声明为成员变量,并且被使用后没有被赋值为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基准测试工具:分析代码性能;

解决办法:

  1. 牢记内存泄漏的场景,当一个对象不会被使用时,给它的所有引用赋值null,堤防静态容器,记得关闭连接、别用逻辑删除,只要用到了引用,变量的作用域要合理。
  2. 使用java.lang.ref包的弱引用WeakReference,下次垃圾收集器工作时被回收。
  3. 检查代码;

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 &
相关推荐
Algorithm15762 小时前
JVM是什么,与Java的关系是什么,以及JVM怎么实现的跨平台性
java·开发语言·jvm
王佑辉2 小时前
【jvm】所有的线程都共享堆吗
jvm
琪露诺大湿7 小时前
JavaEE-多线程初阶(1)
java·linux·开发语言·jvm·数据库·java-ee·1024程序员节
威哥爱编程9 小时前
Java Z 垃圾收集器如何彻底改变内存管理
java·jvm·zgc
与海boy12 小时前
第七章 JVM对高效并发的支持
jvm
王佑辉12 小时前
【jvm】如何设置堆内存大小
jvm
MegaDataFlowers1 天前
JDK、JRE、JVM之间的关系
java·开发语言·jvm
程序员阿鹏1 天前
详解:单例模式中的饿汉式和懒汉式
java·开发语言·jvm·后端·单例模式·eclipse
梦城忆1 天前
JVM基础(内存结构)
开发语言·jvm·python
AGi_1 天前
Java基础-JVM
java·开发语言·jvm