JVM调优
JVM参数总结
堆内存相关
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

显式指定堆内存–Xms和-Xmx
与性能有关的最常见实践之一是根据应用程序要求初始化堆内存。如果我们需要指定最小和最大堆大小(推荐显示指定大小),以下参数可以帮助你实现
1 |
|
- heap size 表示要初始化内存的具体大小。
- unit 表示要初始化内存的单位。单位为 g (GB)、 m(MB)、k(KB)。
举个例子 ,如果我们要为 JVM 分配最小 2 GB 和最大 5 GB 的堆内存大小,我们的参数应该这样来写:
1 |
|
显式新生代内存(Young Generation)
根据Oracle 官方文档,在堆总可用内存配置完成之后,第二大影响因素是为 Young Generation
在堆内存所占的比例。默认情况下,YG 的最小大小为 1310 MB,最大大小为 无限制。
一共有两种指定 新生代内存(Young Generation)大小的方法:
1.通过-XX:NewSize
和-XX:MaxNewSize
指定
1 |
|
举个例子,如果我们要为 新生代分配 最小 256m 的内存,最大 1024m 的内存我们的参数应该这样来写:
1 |
|
2.通过-Xmn<young size>[unit]
指定
如果我们要为 新生代分配 256m 的内存(NewSize 与 MaxNewSize 设为一致),我们的参数应该这样来写:
1 |
|
GC 调优策略中很重要的一条经验总结是这样说的:
将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。
另外,你还可以通过 -XX:NewRatio=<int>
来设置老年代与新生代内存的比值。
比如下面的参数就是设置老年代与新生代内存的比值为 1。也就是说老年代和新生代所占比值为 1:1,新生代占整个堆栈的 1/2。
1 |
|
显式指定永久代/元空间的大小
从 Java 8 开始,如果我们没有指定 Metaspace(元空间) 的大小,随着更多类的创建,虚拟机会耗尽所有可用的系统内存(永久代并不会出现这种情况)。
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小
1 |
|
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是本地内存。
下面是一些常用参数:
1 |
|
修正(参见:issue#1947open in new window):
1、Metapace 的初始容量并不是 -XX:MetaspaceSize
设置,无论 -XX:MetaspaceSize
配置什么值,对于 64 位 JVM 来说,Metaspace 的初始容量都是 21807104(约 20.8m)。
可以参考 Oracle 官方文档 Other Considerationsopen in new window 中提到的:
Specify a higher value for the option MetaspaceSize to avoid early garbage collections induced for class metadata. The amount of class metadata allocated for an application is application-dependent and general guidelines do not exist for the selection of MetaspaceSize. The default size of MetaspaceSize is platform-dependent and ranges from 12 MB to about 20 MB.
MetaspaceSize 的默认大小取决于平台,范围从 12 MB 到大约 20 MB。
另外,还可以看一下这个试验:JVM 参数 MetaspaceSize 的误解open in new window。
2、Metaspace 由于使用不断扩容到-XX:MetaspaceSize
参数指定的量,就会发生 FGC,且之后每次 Metaspace 扩容都会发生 Full GC。
也就是说,MetaspaceSize 表示 Metaspace 使用过程中触发 Full GC 的阈值,只对触发起作用。
垃圾搜集器内部是根据变量 _capacity_until_GC
来判断 Metaspace 区域是否达到阈值的,初始化代码如下所示:
1 |
|
相关阅读:issue 更正:MaxMetaspaceSize 如果不指定大小的话,不会耗尽内存 #1204 。
垃圾回收器
为了提高应用程序的稳定性,选择正确的垃圾收集算法至关重要。
JVM 具有四种类型的 GC 实现:
- 串行垃圾收集器
- 并行垃圾收集器
- CMS 垃圾收集器
- G1 垃圾收集器
可以使用以下参数声明这些实现:
1 |
|
有关垃圾回收实施的更多详细信息,请参见[此处。
GC 日志记录
生产环境上,或者其他要测试 GC 问题的环境上,一定会配置上打印 GC 日志的参数,便于分析 GC 相关的问题。
1 |
|
处理 OOM
对于大型应用程序来说,面对内存不足错误是非常常见的,这反过来会导致应用程序崩溃。这是一个非常关键的场景,很难通过复制来解决这个问题。
这就是为什么 JVM 提供了一些参数,这些参数将堆内存转储到一个物理文件中,以后可以用来查找泄漏:
1 |
|
这里有几点需要注意:
- HeapDumpOnOutOfMemoryError 指示 JVM 在遇到 OutOfMemoryError 错误时将 heap 转储到物理文件中。
- HeapDumpPath 表示要写入文件的路径; 可以给出任何文件名; 但是,如果 JVM 在名称中找到一个
<pid>
标记,则当前进程的进程 id 将附加到文件名中,并使用.hprof
格式 - OnOutOfMemoryError 用于发出紧急命令,以便在内存不足的情况下执行; 应该在
cmd args
空间中使用适当的命令。例如,如果我们想在内存不足时重启服务器,我们可以设置参数:-XX:OnOutOfMemoryError="shutdown -r"
。 - UseGCOverheadLimit 是一种策略,它限制在抛出 OutOfMemory 错误之前在 GC 中花费的 VM 时间的比例
其他
-server
: 启用“ Server Hotspot VM”; 此参数默认用于 64 位 JVM-XX:+UseStringDeduplication
: Java 8u20 引入了这个 JVM 参数,通过创建太多相同 String 的实例来减少不必要的内存使用; 这通过将重复 String 值减少为单个全局char []
数组来优化堆内存。-XX:+UseLWPSynchronization
: 设置基于 LWP (轻量级进程)的同步策略,而不是基于线程的同步。-XX:LargePageSizeInBytes
: 设置用于 Java 堆的较大页面大小; 它采用 GB/MB/KB 的参数; 页面大小越大,我们可以更好地利用虚拟内存硬件资源; 然而,这可能会导致 PermGen 的空间大小更大,这反过来又会迫使 Java 堆空间的大小减小。-XX:MaxHeapFreeRatio
: 设置 GC 后, 堆空闲的最大百分比,以避免收缩。-XX:SurvivorRatio
: eden/survivor 空间的比例, 例如-XX:SurvivorRatio=6
设置每个 survivor 和 eden 之间的比例为 1:6。-XX:+UseLargePages
: 如果系统支持,则使用大页面内存; 请注意,如果使用这个 JVM 参数,OpenJDK 7 可能会崩溃。-XX:+UseStringCache
: 启用 String 池中可用的常用分配字符串的缓存。-XX:+UseCompressedStrings
: 对 String 对象使用byte []
类型,该类型可以用纯 ASCII 格式表示。-XX:+OptimizeStringConcat
: 它尽可能优化字符串串联操作。
JDK监控和故障处理工具
JDK 命令行工具
这些命令在 JDK 安装目录下的 bin 目录下:
jps
(JVM Process Status): 类似 UNIX 的ps
命令。用于查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;- **
jstat
**(JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据; jinfo
(Configuration Info for Java) : Configuration Info for Java,显示虚拟机配置信息;jmap
(Memory Map for Java) : 生成堆转储快照;jhat
(JVM Heap Dump Browser) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果;jstack
(Stack Trace for Java) : 生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。
jps:查看所有 Java 进程
jps
(JVM Process Status) 命令类似 UNIX 的 ps
命令。
jps
:显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(Local Virtual Machine Identifier,LVMID)。jps -q
:只输出进程的本地虚拟机唯一 ID。
1 |
|
jps -l
:输出主类的全名,如果进程执行的是 Jar 包,输出 Jar 路径。
1 |
|
jps -v
:输出虚拟机进程启动时 JVM 参数。
jps -m
:输出传递给 Java 进程 main() 函数的参数。
jstat: 监视虚拟机各种运行状态信息
jstat(JVM Statistics Monitoring Tool) 使用于监视虚拟机各种运行状态信息的命令行工具。 它可以显示本地或者远程(需要远程主机提供 RMI 支持)虚拟机进程中的类信息、内存、垃圾收集、JIT 编译等运行数据,在没有 GUI,只提供了纯文本控制台环境的服务器上,它将是运行期间定位虚拟机性能问题的首选工具。
jstat
命令使用格式:
1 |
|
比如 jstat -gc -h3 31736 1000 10
表示分析进程 id 为 31736 的 gc 情况,每隔 1000ms 打印一次记录,打印 10 次停止,每 3 行后打印指标头部。
常见的 option 如下:
jstat -class vmid
:显示 ClassLoader 的相关信息;jstat -compiler vmid
:显示 JIT 编译的相关信息;jstat -gc vmid
:显示与 GC 相关的堆信息;jstat -gccapacity vmid
:显示各个代的容量及使用情况;jstat -gcnew vmid
:显示新生代信息;jstat -gcnewcapcacity vmid
:显示新生代大小与使用情况;jstat -gcold vmid
:显示老年代和永久代的行为统计,从 jdk1.8 开始,该选项仅表示老年代,因为永久代被移除了;jstat -gcoldcapacity vmid
:显示老年代的大小;jstat -gcpermcapacity vmid
:显示永久代大小,从 jdk1.8 开始,该选项不存在了,因为永久代被移除了;jstat -gcutil vmid
:显示垃圾收集信息;
另外,加上 -t
参数可以在输出信息上加一个 Timestamp 列,显示程序的运行时间。
jinfo: 实时地查看和调整虚拟机各项参数
jinfo vmid
:输出当前 jvm 进程的全部参数和系统属性 (第一部分是系统的属性,第二部分是 JVM 的参数)。
jinfo -flag name vmid
:输出对应名称的参数的具体值。比如输出 MaxHeapSize、查看当前 jvm 进程是否开启打印 GC 日志 ( -XX:PrintGCDetails
:详细 GC 日志模式,这两个都是默认关闭的)。
1 |
|
使用 jinfo 可以在不重启虚拟机的情况下,可以动态的修改 jvm 的参数。尤其在线上的环境特别有用,请看下面的例子:
jinfo -flag [+|-]name vmid
开启或者关闭对应名称的参数。
1 |
|
jmap:生成堆转储快照
**-heap
**:显示 Java 堆的详细信息,包括使用的垃圾回收算法、堆的配置信息以及堆的内存使用情况。这包括年轻代、老年代以及永久代(如果使用)的内存使用情况。
-dump:[live,]format=b,file=<filename>
:用于生成堆转储快照。 如果不使用 jmap
命令,要想获取 Java 堆转储,可以使用 “-XX:+HeapDumpOnOutOfMemoryError”
参数,可以让虚拟机在 OOM 异常出现之后自动生成 dump 文件,Linux 命令下可以通过 kill -3
发送进程退出信号也能拿到 dump 文件。
jmap
的作用并不仅仅是为了获取 dump 文件,它还可以查询 finalizer 执行队列、Java 堆和永久代的详细信息,如空间使用率、当前使用的是哪种收集器等。和jinfo
一样,jmap
有不少功能在 Windows 平台下也是受限制的。
示例:将指定应用程序的堆快照输出到桌面。后面,可以通过 jhat、Visual VM 等工具分析该堆文件。
1 |
|
jhat: 分析 heapdump 文件
jhat
用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果。
1 |
|
jstack :生成虚拟机当前时刻的线程快照
jstack
(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合
生成线程快照的目的主要是定位线程长时间出现停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。线程出现停顿的时候通过jstack
来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者在等待些什么资源。
JDK 可视化分析工具
JConsole:Java 监视与管理控制台
JConsole 是基于 JMX 的可视化监视、管理工具。可以很方便的监视本地及远程服务器的 java 进程的内存使用情况。你可以在控制台输入jconsole
命令启动或者在 JDK 目录下的 bin 目录找到jconsole.exe
然后双击启动。
连接 Jconsole
如果需要使用 JConsole 连接远程进程,可以在远程 Java 程序启动时加上下面这些参数:
1 |
|
在使用 JConsole 连接时,远程进程地址如下:
1 |
|
查看 Java 程序概况

Visual VM:多合一故障处理工具
VisualVM 提供在 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的 Java 应用程序的详细信息。在 VisualVM 的图形用户界面中,您可以方便、快捷地查看多个 Java 应用程序的相关信息。Visual VM 官网:[https://visualvm.github.io/ 。Visual VM 中文文档:[https://visualvm.github.io/documentation.html。
下面这段话摘自《深入理解 Java 虚拟机》。
VisualVM(All-in-One Java Troubleshooting Tool)是到目前为止随 JDK 发布的功能最强大的运行监视和故障处理程序,官方在 VisualVM 的软件说明中写上了“All-in-One”的描述字样,预示着他除了运行监视、故障处理外,还提供了很多其他方面的功能,如性能分析(Profiling)。VisualVM 的性能分析功能甚至比起 JProfiler、YourKit 等专业且收费的 Profiling 工具都不会逊色多少,而且 VisualVM 还有一个很大的优点:不需要被监视的程序基于特殊 Agent 运行,因此他对应用程序的实际性能的影响很小,使得他可以直接应用在生产环境中。这个优点是 JProfiler、YourKit 等工具无法与之媲美的。
VisualVM 基于 NetBeans 平台开发,因此他一开始就具备了插件扩展功能的特性,通过插件扩展支持,VisualVM 可以做到:
- 显示虚拟机进程以及进程的配置、环境信息(jps、jinfo)。
- 监视应用程序的 CPU、GC、堆、方法区以及线程的信息(jstat、jstack)。
- dump 以及分析堆转储快照(jmap、jhat)。
- 方法级的程序运行性能分析,找到被调用最多、运行时间最长的方法。
- 离线程序快照:收集程序的运行时配置、线程 dump、内存 dump 等信息建立一个快照,可以将快照发送开发者处进行 Bug 反馈。
- 其他 plugins 的无限的可能性……
这里就不具体介绍 VisualVM 的使用,如果想了解的话可以看:
- https://visualvm.github.io/documentation.html
- https://www.ibm.com/developerworks/cn/java/j-lo-visualvm/index.html
如何排查线程死锁?
首先使用jps查找程序的线程
然后使用jstack 线程id,就可以找到死锁线程
如下图,t2正在等待锁对象的地址为0x000000076b4alaf8,而t1正在锁住该对象,所以发生了思死锁

解决线上CPU飙高?
1、通过top命令找到CPU占用最厉害的进程id(PID)

2、执行top -H -p 进程号命令:查看java进程下的所有线程占CPU的情况

3、转换16进制线程pid:printf ‘0x%x\n’ 线程ID
4、jstack 进程ID |grep 16进制线ID -A 20

然后就可以找到哪句代码导致CPU飙高
5、jstat -gcutil 进程号 统计间隔毫秒 统计次数(缺省代表一致统计),查看某进程GC持续变化情况,如果发现返回中FullGC很大且一直增大,也可以使用jmap -heap 进程ID查看一下进程的堆内从是不是要溢出了,特别是老年代内从使用情况一般是达到阈值(具体看垃圾回收器和启动时配置的阈值)就会进程Full GC。
6、执行“jmap -dump:format=b,file=filename 进程ID”,导出某进程下内存heap输出到文件中。
如何监控堆OOM?

系统已经OOM挂了
提前设置参数
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath= dump文件路径
然后导出dump文件,通过可视化工具分析
系统在运行中但还未OOM
导出dump文件:jmap -dump: format=b,file=dump文件路径 线程id
dump文件是当前系统中堆的快照,然后结合JVisual VM进行调试
但是也不是一上来就导出jump文件,比如系统频繁的进行Full GC或者CPU Load飙高的警告才进行操作
虽然导出文件会进行GC的STW,但是可以快速的定位OOM,也是值得的,可以提前避免系统挂掉
造成OOM的原因
一次性申请的资源太多
应用一次性申请的对象太多了,有可能把数据库所有的数据都查询过来,然后把数据都放到List中,就有可能造成OOM
解决方法:限制申请数据的数量,比如做一个分页
不合理的递归调用
如果程序中存在不合理的递归调用,递归层次过深,可能会消耗大量的栈空间,最终导致栈溢(StackOverflowError),间接引发 OOM
内存资源耗尽未释放(内存泄漏)
内存泄漏是指程序在运行过程中无法释放不再使用的对象占用的内存空间,导致内存不断累积,最终耗尽了可用内存而引发 OOM。常见的内存泄漏情况包括未关闭资源(如文件流、数据库连接)、静态集合未清理导致的长生命周期对象等。
解决方法:引入池化的思想,最多只能申请10个线程/10个连接等,用完的资源要马上释放
本身分配的资源空间不够
堆空间不足可能是由于程序需要的内存超过了 JVM 分配的最大堆空间限制,也可能是堆内存碎片化导致无法为大对象分配连续的内存空间。
解决方法:jmap -heap 查看堆信息,调整堆内存大小
JVM内存泄漏的原因
对象引用未及时释放
如果某个对象持有了对其他对象的引用,并且这个引用没有被及时释放,就会导致对象无法被垃圾回收器回收。
- 静态集合类(如 HashMap、ArrayList)中的对象引用未被及时清除,可能会导致内存泄漏,静态类或变量需要手动进行回收
1 |
|
由于objectList
是静态的,它的生命周期与类的生命周期相同,直到程序结束。即使在添加对象后,我们不再需要这些对象,它们仍然被objectList
引用,导致它们无法被垃圾回收器回收。
为了避免这种内存泄漏,可以在不再需要对象时及时清理集合,例如:
1 |
|
- 单例模式:和上面的例子原理类似,单例对象在初始化后会以静态变量的方式在了JVM 的整个生命周期中存在
如果单例对象持有外部的引用,那么这个外部对象将不能被 GC 回收,导致内存泄漏。
数据连接、I/O、Socket等连接
创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被GC 回收。
变量的作用域不合理
变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为nul,很可能导致内存泄漏的发生。

hash值发生变化
对象Hash值改变使用HashMap、HashSet等容器中时候于对象修改之后的hash值和存储进容器时的hash值不同,因为我们重写了hashCode方法,所以修改之后无法使用remove清除原来对象 (这造成内存泄漏也是为什么String类型被设置成了不可变类型)。
如何解决:
保证作为HashMap 的Key 的对象是不可变的。也就是使用只读多对象来当作Key
重写hashCode时,保证hashCode 的生成只和某些不可变的成员相关,如Person对象,hashcode只和name相关,改变age并不会改变hashCode。
ThreadLocal使用不当
ThreadLocal的弱引用导致内存泄漏也是个老生常谈的话题了,使用完ThreadLocal一定要记得使用remove方法来进行清除。