JVM调优的基本思路是:监控发现问题-》工具分析定位问题-》JVM性能调优。本文主要围绕这三点进行逐个分析

 

一. 监控发现问题

通过监控工具Prometheus+Grafna/Spring Actuator+Admin,监控服务器有没有以下问题:

  • 死锁:两个线程在持有锁的前提下,尝试获取对方锁,从而一直阻塞的情况
  • 内存泄漏:程序未释放无效内存从而导致内存可用空间减小。常见场景:数据库连接未关闭、缓存未清理等
  • 内存溢出:当前申请空间大于可用内存。内存泄漏长时间会导致内存溢出
  • GC频繁
  • 程序响应时间较长
  • CPU负载过高

二. 工具分析定位问题

 JVM调优时,吞吐量和停顿时长无法兼顾,吞吐量提高的代价是停顿时间拉长

1 调优常用指令

  • jps: java process status,查看所有正在运行的java进程
  • jstat:JVM static monitorTool,用于监控指定进程的jvm统计信息,是运行期定位虚拟机性能问题首先工具
  • jmap:用于生成堆快照,此外还可以查询finalize执行队列,java堆和永久代使用情况,比如空间使用率,使用哪种垃圾回收器
  • jinfo:jinfo查看并修改JVM参数
  • jstack:JVM Stack Trace,打印指定进程的此刻线程快照,用于查看线程死锁、阻塞等问题

2 JDK自带命令调优工具

2.1 jps 查看所有正在运行JVM进程

指令格式 jps [options 参数]

 

一个阻塞代码等待用户输入
public class ScannerTest {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String info = scanner.next();
    }
}

 

运行后在命令行输入jps,打印出当前所有运行的java进程

         

 jps 参数

  • -q  仅打印进程号  

            

  • -l  除了打印进程号,还打印完整类名

         

  • -m 传递给main()的参数

            如下图,传入参数aaa 

            

    输入 jps -m

              

  • -v  打印JVM参数         

           

 2.2 jstat 查看某个进程JVM信息

 用于监视jvm运行状态信息.比如类装载、堆信息、GC信息等

bash格式: jstat  -<options> -[t] -[h<lines>] -<pid> -[<interval> <count>]

  1.   t:程序开启到执行jstat运行时间
  2.   h<lines>:周期性每隔多少行打印表头
  3.   pid: jps查询到的进程号
  4.   <interval>:用于指定输出统计数据的周期,单位为毫秒。即:查询间隔
  5.   <count>: 用于指定查询的总次数
  6.      options内容如下:

      类加载相关:

            -class:显示classLoader相关信息,类加载、卸载数量、类装载所消耗时间等      

                             

                            

 

 

 

     gc相关

        -gc: 打印gc过程中堆空间使用情况,gc时间等。包括Eden S0 S1 Old Perment空间以及已用空间,youngGC、FullGC 次数和耗时信息

                                               

                                 

        -gcutil:与-gc类似,更关注于java堆中已使用空间与总空间比值

                                

                                 

        -gccapacity: 与-gc类似,更关注于java堆中各区域已使用最大、最小空间情况

        -gccause: 与gc类似,最后新增一列最后一次或当前正在发生的 GC 产生原因

          LGCC:上次gc原因

          GCC:当前GC原因

                                     

 

        -gcnew:显示新生代gc情况信息

        -gcnewcapacity: 与-gcnew类似,更关注于已使用最大、最小空间情况

        -gcold: 显示老年代gc情况信息

        -gcoldcapacity:与-gcold类似,更关注于已使用最大、最小空间情况

                                  

          OGCMN:老年代最小空间

          OGCMX:老年代最大空间

          OGC:当前老年大空间

          OC:老年代大小

        -gcpermcapacity:显示永久代已使用最大、最小空间情况

    JIT相关

        -compiler:显示 JIT 编译器编译过的方法、耗时等信息

                             

        -printcompilation:输出已经被 JIT 编译的方法

 

2.3 jmap 查看某个进程堆内存使用情况

jmap指令 jmap [options] pid 

options如下:

 

  • jmap使用情况1  jmap -heap pid 查看某个进程堆内存使用情况,包括使用的GC算法、堆配置参数和各代中堆内存使用情况
root@ubuntu:/# jmap -heap 21711
Attaching to process ID 21711, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 20.10-b01

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
MinHeapFreeRatio = 40   
MaxHeapFreeRatio = 70   
MaxHeapSize      = 2067791872 (1972.0MB)
NewSize          = 1310720 (1.25MB)
MaxNewSize       = 17592186044415 MB
OldSize          = 5439488 (5.1875MB)
NewRatio         = 2   
SurvivorRatio    = 8   
PermSize         = 21757952 (20.75MB)
MaxPermSize      = 85983232 (82.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 6422528 (6.125MB)
   used     = 5445552 (5.1932830810546875MB)
   free     = 976976 (0.9317169189453125MB)
   84.78829520089286% used
From Space:
   capacity = 131072 (0.125MB)
   used     = 98304 (0.09375MB)
   free     = 32768 (0.03125MB)
   75.0% used
To Space:
   capacity = 131072 (0.125MB)
   used     = 0 (0.0MB)
   free     = 131072 (0.125MB)
   0.0% used
PS Old Generation
   capacity = 35258368 (33.625MB)
   used     = 4119544 (3.9287033081054688MB)
   free     = 31138824 (29.69629669189453MB)
   11.683876009235595% used
PS Perm Generation
   capacity = 52428800 (50.0MB)
   used     = 26075168 (24.867218017578125MB)
   free     = 26353632 (25.132781982421875MB)
   49.73443603515625% used
   ....

 

  • jmap使用情况2   jmap -histo [:live] pid

查看某个进程内实例以及大小,加上live表示只统计活的对象

root@ubuntu:/# jmap -histo:live 21711 | more
num     #instances         #bytes  class name----------------------------------------------
   1:         38445        5597736  <constMethodKlass>
   2:         38445        5237288  <methodKlass>
   3:          3500        3749504  <constantPoolKlass>
   4:         60858        3242600  <symbolKlass>
   5:          3500        2715264  <instanceKlassKlass>
   6:          2796        2131424  <constantPoolCacheKlass>
   7:          5543        1317400  [I
   8:         13714        1010768  [C
   9:          4752        1003344  [B
  10:          1225         639656  <methodDataKlass>
  11:         14194         454208  java.lang.String
  12:          3809         396136  java.lang.Class
  13:          4979         311952  [S
  14:          5598         287064  [[I
  15:          3028         266464  java.lang.reflect.Method
  16:           280         163520  <objArrayKlassKlass>
  17:          4355         139360  java.util.HashMap$Entry
  18:          1869         138568  [Ljava.util.HashMap$Entry;
  19:          2443          97720  java.util.LinkedHashMap$Entry
  20:          2072          82880  java.lang.ref.SoftReference
  21:          1807          71528  [Ljava.lang.Object;
  22:          2206          70592  java.lang.ref.WeakReference
  23:           934          52304  java.util.LinkedHashMap
  24:           871          48776  java.beans.MethodDescriptor
  25:          1442          46144  java.util.concurrent.ConcurrentHashMap$HashEntry
  26:           804          38592  java.util.HashMap
  27:           948          37920  java.util.concurrent.ConcurrentHashMap$Segment
  28:          1621          35696  [Ljava.lang.Class;
  29:          1313          34880  [Ljava.lang.String;
  30:          1396          33504  java.util.LinkedList$Entry
  31:           462          33264  java.lang.reflect.Field
  32:          1024          32768  java.util.Hashtable$Entry
  33:           948          31440  [Ljava.util.concurrent.ConcurrentHashMap$HashEntry;

 

上面中class name返回对象类型

B  byte
C  char
D  double
F  float
I  int
J  long
Z  boolean
[  数组,如[I表示int[]
[L+类名 其他对象

 

 

  • jmap使用情况3  将堆使用情况快照dump到文件中,然后使用jhat查看。

jmap dump格式如下

  jmpa -dump:format=b,file=dumpFileName pid

root@ubuntu:/# jmap -dump:format=b,file=/tmp/dump.dat 21711
Dumping heap to /tmp/dump.dat ...
Heap dump file created
root@ubuntu:/# jhat -port 9998 /tmp/dump.dat
Reading from /tmp/dump.dat...
Dump file created Tue Jan 28 17:46:14 CST 2014Snapshot read, resolving...
Resolving 132207 objects...
Chasing references, expect 26 dots..........................
Eliminating duplicate references..........................
Snapshot resolved.
Started HTTP server on port 9998Server is ready.

注意如果Dump文件太大,可能需要加上-J-Xmx512m这种参数指定最大堆内存,即jhat -J-Xmx512m -port 9998 /tmp/dump.dat。然后就可以在浏览器中输入主机地址:9998查看了:

 除了以上手动导出dump外,还可以采用如下自动方式在出现oom情况下自动导出dump文件

// 开启在出现 OOM 错误时生成堆转储文件
-Xmx1024m
-XX:+HeapDumpOnOutOfMemoryError
// 将生成的堆转储文件保存到 /tmp 目录下,并以进程 ID 和时间戳作为文件名
-XX:HeapDumpPath=/tmp/java_%p_%t.hprof

// 在进行 Full GC 前生成堆转储文件
// 注:如果没有开启自动 GC,则此参数无效。JDK 9 之后该参数已被删除。
-XX:+HeapDumpBeforeFullGC

 

Heap Dump 文件分析重点

大对象:

查找占用内存最多的对象(如巨型数组、缓存)。

对象数量:
检查是否有异常大量的相同类型对象(可能是内存泄漏)。

GC Roots:
分析哪些对象被垃圾回收根(如静态变量、线程栈)引用,导致无法被回收。

泄漏 suspects:
MAT 会自动生成 "Leak Suspects" 报告,指出可能的内存泄漏点。

Heap Dump分析可以参考 https://cloud.tencent.com/developer/article/1900453

2.4 jstack 打印指定进程当前线程快照

jstack pid是进程id

线程快照:进程内每个线程正在执行方法堆栈信息;

生成线程快照可用于定位线程长时间停顿原因:诸如死锁、阻塞、死循环、请求外部系统资源长时间等待等问题;当线程出现长时间停顿时可以用jstack显示各个线程堆栈使用情况

在thread dump要额外注意以下几种状态,尤其时前4中状态:  

  •   死锁 dead_lock
  •   等待资源 waiting on condition
  •   等待监视器 waiting on monitor entry
  •   阻塞 blocked
  •   执行中 runnable
  •   对象等待中 object.wait()
  •   挂起 suspended
  •   停止 parked

jstack 命令格式 jstack [options]

option 参数作用
-F 当正常输出的请求不被响应时,强制输出线程堆栈
-l 除堆栈外,显示关于锁的附加信息
-m 如果调用本地方法的话,可以显示 C/C++ 的堆栈

 

 

如下例代码实现了死锁

       

       

       

 

 Thread Dump 和 Heap Dump 的主要区别在于它们记录的信息类型和用途不同。‌

Thread Dump

Thread Dump记录了对应进程中中所有线程在某一时刻的状态和堆栈信息。它包括线程的名称、状态、优先级以及线程所执行的方法和代码行数。Thread Dump文件是一个文本文件,可以通过命令行工具如 jstack 生成。Thread Dump的主要用途包括:

  • ‌诊断内存泄露‌:通过分析线程堆栈信息,可以找到可能导致内存泄露的代码位置。
  • ‌发现死锁线程‌:通过分析线程的堆栈信息,可以确定是否存在死锁情况。
  • ‌性能分析‌:通过多次生成Thread Dump并比较,可以分析出程序在运行过程中的性能瓶颈‌12。

Heap Dump

Heap Dump则记录了JVM堆内存中的对象信息。它是一个二进制文件,包含了某一时刻JVM堆中对象的快照。Heap Dump主要用于分析内存泄露和内存溢出等问题。通过分析Heap Dump文件,可以查看当前内存中的对象信息,包括对象的数量、大小和引用关系等。常见的工具如 Memory Analyzer Tool (MAT)和 VisualVM 可以用来分析Heap Dump文件,帮助定位内存泄漏问题并进行优化‌13。

使用场景和生成方法

  • ‌Thread Dump‌:

    • 使用场景:诊断内存泄露、发现死锁、性能分析。
    • 生成方法:可以使用命令行工具如jstack或jcmd,通过指定进程ID来生成Thread Dump文件。例如,使用jstack命令可以执行以下命令来生成Thread Dump文件:jstack <pid> > dump.txt,其中<pid>是Java进程的进程ID,dump.txt是保存Thread Dump信息的文件名‌。如果想用jstack查看进程下某个线程日志,需要top -Hp 进程ID得到一系列ID为线程ID,然后再用jstack 进程ID|grep 十六进制(线程ID)
  • ‌Heap Dump‌:

    • 使用场景:分析内存泄露、内存溢出等问题。
    • 生成方法:可以通过命令行工具如jmap或jcmd生成Heap Dump文件。例如,使用jmap命令可以执行以下命令来生成Heap Dump文件:jmap -dump:live,format=b,file=heapdump.hprof <pid>,其中<pid>是Java进程的进程ID,heapdump.hprof是保存Heap Dump信息的文件名‌

 2.5 内存泄漏案例

使用jstat,每隔一段较长的时间采样多组 OU(老年代内存量) 的最小值,如果这些最小值在上涨,说明无法回收对象在不断增加,可能是内存泄漏导致的。

在长时间运行的 Java 程序中,我们可以运行 jstat 命令连续获取多行性能数据,并取这几行数据中 OU 列(Old Used,已占用的老年代内存)的最小值

然后,我们每隔一段较长的时间重复一次上述操作,来获得多组 OU 最小值。如果这些值呈上涨趋势,则说明该 Java 程序的老年代内存已使用量在不断上涨,这意味着无法回收的对象在不断增加,因此很有可能存在内存泄漏(不再使用的对象仍然被引用,导致GC无法回收)。

使用jstat统计GC信息,并显示进程启动时间、统计间隔1000ms、统计20次

 

2.6 内存溢出案例

使用jstat,我们可以比较 Java 进程的启动时长以及总 GC 时长 (GCT 列),或者两次测量的间隔时长以及总 GC 时长的增量,来得出 GC 时长占运行时长的比例。

如果该比例超过 20%,则说明目前堆的压力较大;

如果该比例超过 98%,则说明这段时期内几乎一直在GC,堆里几乎没有可用空间,随时都可能抛出 OOM 异常。

2.7 找出某个Java进程中最耗费CPU的Java线程并定位堆栈信息,用到的命令有ps、top、printf、jstack、grep

  • 1. ps -ef |grep 找出进程ID -- PID  可根据名字(下例中是mrf-center)找出PID
root@ubuntu:/# ps -ef | grep mrf-center | grep -v grep
root     21711     1  1 14:47 pts/3    00:02:10 java -jar mrf-center.jar

 

  • 2.  top -Hp p 找出进程中最耗CPC的线程

               TIME列就是耗时列,CPU时间最长的是线程ID为21742的线程,然后计算出21742的16进制是得到21742的十六进制值为54ee         

                     

  •  3. jstack pid|grep 输出进程21711的堆栈信息,然后根据线程ID的十六进制值grep
root@ubuntu:/# jstack 21711 | grep 54ee
"PollIntervalRetrySchedulerThread" prio=10 tid=0x00007f950043e000 nid=0x54ee in Object.wait() [0x00007f94c6eda000]

 

可以看到CPU消耗在PollIntervalRetrySchedulerThread这个类的Object.wait(),我找了下我的代码,定位到下面的代码

// Idle wait
getLog().info("Thread [" + getName() + "] is idle waiting...");
schedulerThreadState = PollTaskSchedulerThreadState.IdleWaiting;
long now = System.currentTimeMillis();
long waitTime = now + getIdleWaitTime();
long timeUntilContinue = waitTime - now;
synchronized(sigLock) {try {
    if(!halted.get()) {
    sigLock.wait(timeUntilContinue);
    }
    } catch (InterruptedException ignore) {
    }
}

 

它是轮询任务的空闲等待代码,上面的sigLock.wait(timeUntilContinue)就对应了前面的Object.wait()。

3 JDK自带的可视化监控工具

  • jconsole
  • Visual VM:Visual VM可以监视应用程序的 CPU、GC、堆、方法区、线程快照,查看JVM进程、JVM 参数、系统属性

三 JVM调优

1 jvm调优常用参数

-XX: MetaspaceSize=128m (元空间默认空间大小)

-XX: MaxMetaspaeSize=128m (元空间最大空间大小)

-Xmx1024m (最大堆内存大小)

-Xms1024m (初始堆内存大小)

-Xmn1024m (新生代大小)

-Xss256m (栈的最大深度)

 

//堆内存比例 Young GC频繁时,我们可以提高新生代在堆内存中的比例、提高伊甸园区在新生代的比例,令新生代不那么快被填满

-XX: SurvivorRatio=8 (Eden:S0:S1=8:1:1)

-XX:NewRatio=4 (新生代:老年代=1:4)

 

//修改垃圾回收器

-XX:+UseSerialGC (新生代使用串行回收器)

-XX:+UseParallelOldGC(新生代使用Parallel Scavenge回收器,老年代使用Parallel Old回收器)

-XX:+UseConcMarkSweepGC (老年代使用cms回收器)

-XX:UseG1GC (设置G1垃圾收集器)

 

-XX: GCMaxPauseMillis (GC最大停顿时间)

-XX: InitialTenuringThreshold=7 (新生代对象经过多少代移到老年代,jdk8 默认是15,jdk9默认是7。 当Full GC频繁时,我们提高升老年龄,让年轻代的对象多在年轻代待一会,从而降低Full GC频率)

-XX:PretenureSizeThreshold=100000 (放入新生代对象size超过这个值便会直接放入老年代,如果这个值设为0,则对放入新生代对象大小没有限制)


//使用多少比例的老年代后开始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日志地址)

-XX: GCTimeRatio=99 (吞吐量为99%, 吞吐量=运行时间/(运行时间+GC时间))

 

2 JVM调优

2.1 避免Full GC:

Full GC 的执行时间最长,也是JVM调优的重点。

  • 调大老年代比例
  • 增加整个堆内存
  • 使用G1、ZGC等现代回收器。

2.2 调整内存大小 -Xmx Xms Xmn

根据程序运行时老年代存活对象大小进行调整,整个堆内存大小设置为老年代活动对象大小的3~4倍,年轻代占总堆空间的3/8.

-Xms:初始堆内存大小。默认:物理内存小于192MB时,默认为物理内存的1/2;物理内存大192MB且小于128GB时,默认为物理内存的1/4;物理内存大于等于128GB时,都为32GB。
-Xmx:最大堆内存大小,建议保持和初始堆内存大小一样。因为从初始堆到最大堆的过程会有一定的性能开销,而且现在内存不是稀缺资源。
-Xmn:年轻代大小。JDK官方建议年轻代占整个堆大小空间的3/8左右。

2.3 减少停顿时间 MaxGCPauseMillis

 

2.4 提高吞吐量 GCTimeRatio

吞吐量太高会拉长停顿时间,造成用户体验下降(吞吐量高gc时间拉长原因是为了提高吞吐量,就会降低gc频率从而导致gc的时间延长了)。 

2.5 调整内存不同区域的比例 NewTimeRadio SurvivorRadio

Young GC频繁时,我们可以提高新生代在堆内存中的比例、提高伊甸园区在新生代的比例,令新生代不那么快被填满。

默认情况,伊甸园区:S0:S1=8:1:1,新生代:老年代=1:2。

//调整内存比例
 //伊甸园:幸存区
-XX:SurvivorRatio=8(伊甸园:幸存区=8:2//新生代和老年代的占比
 -XX:NewRatio=4  //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2

 

2.6 调整老年代年龄 -XX: InitialTenuringThreshold

JDK8时Young GC默认把15岁的对象移动到老年代。JDK9默认值改为7。

当Full GC频繁时,我们提高升老年龄,让年轻代的对象多在年轻代待一会,从而降低Full GC频率。

  //进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,JDK8默认值15,JDK9默认值7
 -XX:InitialTenuringThreshold=7

 

2.7 调整大对象阈值 PreTenureSizeThreshhold

Young GC时大对象会不顾年龄直接移动到老年代。当Full GC频繁时,我们关闭或提高大对象阈值,让老年代更迟填满。

默认是0,即大对象不会直接在YGC时移到老年代。

 //新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
  -XX:PretenureSizeThreshold=1000000

 

2.8 调整GC触发条件

CMS调整老年代触发回收比例

CMS的并发标记和并发清除阶段是用户线程和回收线程并发执行,如果老年代满了再回收会导致用户线程被强制暂停。所以我们修改回收条件为老年代的60%,保证回收时预留足够空间放新对象。CMS默认是老年代68%时触发回收机制。

 //使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
 -XX:CMSInitiatingOccupancyFraction

 

G1调整老年代触发回收比例

超过存活阈值的Region,其内对象会被混合回收到老年代。G1回收时也要预留空间给新对象。存活阈值默认85%,即当一个内存区块中存活对象所占比例超过 85% 时,这些对象就会通过 Mixed GC 内存整理并晋升至老年代内存区域

 //G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
 -XX:G1MixedGCLiveThresholdPercent=65

 

2.9 【最有效】选择合适的垃圾回收器

JVM调优最实用、最有效的方式是升级垃圾回收器,根据CPU核数,升级当前版本支持的最新回收器。

CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。
CPU多核,关注吞吐量 ,那么选择Parallel Scavenge+Parallel Old组合(JDK8默认)。
CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择ParNew+CMS,吞吐量降低但是低停顿。
CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。

 

2.10 代码角度分析

分析代码是否出现内存泄漏、内存溢出情况。可以参考下面中OOM问题排查以及引起内存泄漏9种情况进行回答

通过jmap分析是否有大对象产生,为什么产生这种大对象(通过MAT分析是不是代码中一次性加载过量数据、出现死循环等)

通过jstack分析是否出现死锁引起cpu飙升等问题

四 CPU飙升和GC频繁的调优方案

 

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(十六进制),打印线程快照,找到线程执行的代码。一般如果有死锁的话就会显示线程互相占用情况。
解决问题:优化代码、增加系统资源(增多服务器、增大内存)。

 

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。

实例

监控发现问题

上午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耗时上升。

解决方案

排查内存泄漏、大对象、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

 

其他优化方案

优化业务代码

绝大部分问题都出自代码。

日常开发中,要尽量减少非必要对象的创建,防止死循环创建对象,注意内存泄漏的12个场景,防止内存泄漏。

在一些内存占用率高的场景下需要以时间换空间,控制内存使用。

增加机器

在集群下新增加几个服务器,分散节点压力,可以提高整体效率。

调整线程池参数

合理设置线程池的线程数量。

下面的参数只是一个预估值,适合初步设置,具体的线程数需要经过压测确定,压榨(更好的利用)CPU的性能。

记CPU核心数为N;

核心线程数:

CPU密集型:N+1。数量与CPU核数相近是为了不浪费CPU,并防止频繁的上下文切换,加1是为了有线程被阻塞后还能不浪费CPU的算力。
I/O密集型:2N,或N/(1-阻塞系数)。I/O密集型任务CPU使用率并不是很高,可以让CPU在等待I/O操作的时去处理别的任务,充分利用CPU,所以数量就比CPU核心数高一倍。有些公司会考虑阻塞系数,阻塞系数是任务线程被阻塞的比例,一般是0.8~0.9。
实际开发中更适合的公式:N*((线程等待时间+线程计算时间)/线程计算时间)
最大线程数:设成核心线程数的2-4倍。数量主要由CPU和IO的密集性、处理的数据量等因素决定。

需要增加线程的情况:jstack打印线程快照,如果发现线程池中大部分线程都等待获取任务、则说明线程够用。如果大部分线程都处于运行状态,可以继续适当调高线程数量。

【编外】内存泄漏和内存溢出

1 内存溢出 申请内存大于可用内存

 1.1 内存溢出概念

  基于java运行时数据空间可以将内存溢出分为如下几点:

  •   虚拟机栈和本地方法栈溢出:

    如果虚拟机的栈内存允许动态扩展,并且方法递归层数太深时,导致扩展栈容量时无法申请到足够内存。

  •   堆溢出
  1.   死循环创建对象太多
  2.   加载到内存中数据太多:从DB返回过量数据;调用第三方接口返回过量数据;接收MQ消息过多
  3.   集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  •  方法区溢出  运行时生成大量动态类会造成方法区溢出
  1. CGlib动态代理:CGlib动态代理产生大量类填满了整个方法区(方法区存常量池、类信息、方法信息),直到溢出。CGlib动态代理是在内存中构建子类对象实现对目标对象功能扩展,如果enhancer.setUseCache(false);,即关闭用户缓存,那么每次创建代理对象都是一个新的实例,创建过多就会导致方法区溢出。注意JDK动态代理不会导致方法区溢出。                                                                                                                                                                                                                                                                               针对CGlib动态代理引起的内存溢出处理方案:①使用enhancer的setUseCache(false) ②显式调用类加载器的clearAssertionStatus方法可以帮助解决类加载器泄漏的问题。③使用弱引用(Weak References)和引用队列(Reference Queue)
     
  2. JSP:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)

1.2 OOM的排查和解决

OOM排查时一般从两个角度,一是从JVM层面,即新生代、老年代空间层面以及GC层面去分析(比如是否有改动Xmx Xms Xmn, GC选择是否恰当等);二是从dump分析代码。下面实例是直接从代码层面分析的

1.2.1 使用JDK自带的命令行调优工具 ,判断是否有OOM:

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

1.2.2 使用MAT定位oom

①MAT解析dump文件;
② 定位大对象:点击直方图图标(Histogram),对象会按内存大小排序,查看内存占用最大的对象;

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

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

 

1.3 解决方案

  1. 通过jinfo修改JVM参数,比如增大内存空间
  2. 代码优化,分析代码引起oom位置进行修改

 

 

2 内存泄漏 不再使用的对象仍在被引用,无法被GC回收

2.1 内存泄漏9种情况

  • 静态集合类持有对象

    Java中静态集合类有hashmap, linkedlist, arraylist.而静态集合类的生命周期与jvm程序一致。如下例中obj对象被add到静态集合类arraylist中,所以就算obj不再使用也无法释放内存

                 

  •  单例模式持有对象

  单例模式和静态集合类比较类似,由于单例模式的生命周期和jvm程序一致,如果单例对象持有外部对象的引用,那么这个外部对象就算不在被使用也无法对其进行gc回收

  • 内部类持有外部类

  如下例中,Inner是Outer类的内部类,同时内部类Inner被静态集合类引用,这样就算外部类Outer不再使用也不会被gc回收从而造成内存泄漏

package org.example.a;
 
import java.util.ArrayList;
import java.util.List;
 
class Outer{
    private int[] data;
 
    public Outer(int size) {
        this.data = new int[size];
    }
 
    class Innner{
 
    }
 
    Innner createInner() {
        return new Innner();
    }
}
 
public class Demo {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        int counter = 0;
        while (true) {
            list.add(new Outer(100000).createInner());
            System.out.println(counter++);
        }
    }
}

 

 

  • 数据库,网络,IO等连接没有关闭

在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用 close 方法来释放与数据库的连接。如果对 Connection、Statement 或 ResultSet 不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

        

  •  变量作用域不合理 

            如下例中msg是成员变量,其作用域与UsingRandom对象生命周期相同,就算其在接收readFromNet后不在使用也不会被回收

           

  •  HashSet中对象改变哈希值

  当一个对象被存储到hashset后,就不能改变这个对象参与计算hash值的字段了,否则对象hash值改变就找不到对应value了。

  如下例中,首先point中有一个属性x参与了hashCode计算,在main方法中首先将x设置为41存入到list中,但紧接着将这个参与计算hashcode值的x改成20了,这样在hashset中便找不到原来10的值了,执行remove后也不会将存到hashset中x=10的值删掉了

                 

                 

  •  缓存引用忘记删除

            

 

 2.2内存泄漏的排查和解决

性能分析工具判断是否有内存泄漏:

  • 1. JDK自带的命令行调优工具:

    每隔一段较长的时间通过jstat命令采样多组 OU(老年代内存量) 的最小值;
    如果这些最小值在上涨,说明无法回收对象在不断增加,可能是内存泄漏导致的。

  • 2. MAT监视诊断内存泄漏:

    生成堆转储文件:MAT直接从Java进程导出dump文件
    可疑点:查看泄漏怀疑(Leak Suspects),找到内存泄漏可疑点
    可疑线程:可疑点查看详情(Details),找到可疑线程
    定位代码:查看线程调用栈(See stacktrace),找到问题代码的具体位置。

2.3 解决办法:

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

 Jconsole vs JMC/VisualVM

jconsole 是一个强大的工具,但在某些情况下,可能需要更高级的监控和诊断工具,如 VisualVM 或 Java Mission Control (JMC)。这些工具提供了更深入的分析能力和更多的功能,例如堆转储分析、线程死锁检测等 

 

JMC vs VisualVM

JMC优势

  • 低开销记录:JFR在JVM内部实现,开销通常<1%

  • 持续监控:支持长期记录(几天/几周)

如下例中死锁代码,JMC可以显示:

  • 精确的阻塞时长

  • 竞争锁的代码位置

  • 持有锁的线程栈

  • 历史阻塞统计

而VisualVM只会显示线程阻塞情况

// 模拟锁竞争
public class LockContention {
    private static final Object lock = new Object();
    
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized(lock) {
                while(true); // 持有锁不释放
            }
        }).start();
        
        // 多个线程竞争锁
        for(int i=0; i<10; i++) {
            new Thread(() -> {
                synchronized(lock) {
                    System.out.println("Got lock");
                }
            }).start();
        }
    }
}
View Code

 

 

参考 https://blog.csdn.net/qq_40991313/article/details/132382094#4.4%20MAT%E5%88%86%E6%9E%90%E5%A0%86%E8%BD%AC%E5%82%A8%E6%96%87%E4%BB%B6

posted on 2025-06-20 17:34  colorfulworld  阅读(69)  评论(0)    收藏  举报