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>]
- t:程序开启到执行jstat运行时间
- h<lines>:周期性每隔多少行打印表头
- pid: jps查询到的进程号
- <interval>:用于指定输出统计数据的周期,单位为毫秒。即:查询间隔
- <count>: 用于指定查询的总次数
- 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
Thread Dump
Thread Dump记录了对应进程中中所有线程在某一时刻的状态和堆栈信息。它包括线程的名称、状态、优先级以及线程所执行的方法和代码行数。Thread Dump文件是一个文本文件,可以通过命令行工具如 jstack
- 诊断内存泄露:通过分析线程堆栈信息,可以找到可能导致内存泄露的代码位置。
- 发现死锁线程:通过分析线程的堆栈信息,可以确定是否存在死锁情况。
- 性能分析:通过多次生成Thread Dump并比较,可以分析出程序在运行过程中的性能瓶颈12。
Heap Dump
Heap Dump则记录了JVM堆内存中的对象信息。它是一个二进制文件,包含了某一时刻JVM堆中对象的快照。Heap Dump主要用于分析内存泄露和内存溢出等问题。通过分析Heap Dump文件,可以查看当前内存中的对象信息,包括对象的数量、大小和引用关系等。常见的工具如 Memory Analyzer Tool
使用场景和生成方法
-
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运行时数据空间可以将内存溢出分为如下几点:
- 虚拟机栈和本地方法栈溢出:
如果虚拟机的栈内存允许动态扩展,并且方法递归层数太深时,导致扩展栈容量时无法申请到足够内存。
- 堆溢出
- 死循环创建对象太多
- 加载到内存中数据太多:从DB返回过量数据;调用第三方接口返回过量数据;接收MQ消息过多
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 方法区溢出 运行时生成大量动态类会造成方法区溢出
- CGlib动态代理:CGlib动态代理产生大量类填满了整个方法区(方法区存常量池、类信息、方法信息),直到溢出。CGlib动态代理是在内存中构建子类对象实现对目标对象功能扩展,如果enhancer.setUseCache(false);,即关闭用户缓存,那么每次创建代理对象都是一个新的实例,创建过多就会导致方法区溢出。注意JDK动态代理不会导致方法区溢出。 针对CGlib动态代理引起的内存溢出处理方案:①使用enhancer的s
使用弱引用(Weak References)和引用队列(Reference Queue)etUseCache(false) ②显式调用类加载器的clearAssertionStatus方法可以帮助解决类加载器泄漏的问题。③ - JSP:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)
1.2 OOM的排查和解决
OOM排查时一般从两个角度,一是从JVM层面,即新生代、老年代空间层面以及GC层面去分析(比如是否有改动Xmx Xms Xmn, GC选择是否恰当等);二是从dump分析代码。下面实例是直接从代码层面分析的
1.2.1 使用JDK自带的命令行调优工具 ,判断是否有OOM:
- 使用jps命令查看当前Java进程;
- 使用jstat命令多次统计GC,比较GC时长占运行时长的比例;
- 如果比例超过20%,就代表堆压力已经很大了;
- 如果比例超过98%,说明这段时期内几乎一直在GC,堆里几乎没有可用空间,随时都可能抛出 OOM 异常。
1.2.2 使用MAT定位oom
①MAT解析dump文件;
② 定位大对象:点击直方图图标(Histogram),对象会按内存大小排序,查看内存占用最大的对象;

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

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

1.3 解决方案
- 通过jinfo修改JVM参数,比如增大内存空间
- 代码优化,分析代码引起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 解决办法:
- 牢记内存泄漏的场景,当一个对象不会被使用时,给它的所有引用赋值null,堤防静态容器,记得关闭连接、别用逻辑删除,变量的作用域要合理。
- 使用java.lang.ref包的弱引用WeakReference,下次垃圾收集器工作时被回收。
- 检查代码;
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(); } } }
参考 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
浙公网安备 33010602011771号