一篇Java架构师的JVM性能调优文章
性能定义
-
吞吐量 - 指不考虑 GC 引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标。
-
延迟 - 其度量标准是缩短由于垃圾啊收集引起的停顿时间或者完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。
-
内存占用 - 垃圾收集器流畅运行所需要的内存数量。
调优原则
GC 优化的两个目标:
-
将进入老年代的对象数量降到最低
-
减少 Full GC 的执行时间
GC 优化的基本原则是:将不同的 GC 参数应用到两个及以上的服务器上然后比较它们的性能,然后将那些被证明可以提高性能或减少 GC 执行时间的参数应用于最终的工作服务器上。
将进入老年代的对象数量降到最低
除了可以在 JDK7 及更高版本中使用的 G1 收集器以外,其他分代 GC 都是由 Oracle JVM 提供的。关于分代 GC,就是对象在 Eden 区被创建,随后被转移到 Survivor 区,在此之后剩余的对象会被转入老年代。也有一些对象由于占用内存过大,在 Eden 区被创建后会直接被传入老年代。老年代 GC 相对来说会比新生代 GC 更耗时,因此,减少进入老年代的对象数量可以显著降低 Full GC 的频率。你可能会以为减少进入老年代的对象数量意味着把它们留在新生代,事实正好相反,新生代内存的大小是可以调节的。
降低 Full GC 的时间
Full GC 的执行时间比 Minor GC 要长很多,因此,如果在 Full GC 上花费过多的时间(超过 1s),将可能出现超时错误。
-
如果通过减小老年代内存来减少 Full GC 时间,可能会引起 OutOfMemoryError 或者导致 Full GC 的频率升高。
-
另外,如果通过增加老年代内存来降低 Full GC 的频率,Full GC 的时间可能因此增加。
因此,你需要把老年代的大小设置成一个“合适”的值。
GC 优化需要考虑的 JVM 参数
GC 优化时最常用的参数是-Xms
,-Xmx
和-XX:NewRatio
。-Xms
和-Xmx
参数通常是必须的,所以NewRatio
的值将对 GC 性能产生重要的影响。
有些人可能会问如何设置永久代内存大小,你可以用-XX:PermSize
和-XX:MaxPermSize
参数来进行设置,但是要记住,只有当出现OutOfMemoryError
错误时你才需要去设置永久代内存。
GC 优化的过程
GC 优化的过程和大多数常见的提升性能的过程相似,下面是笔者使用的流程:
1.监控 GC 状态
你需要监控 GC 从而检查系统中运行的 GC 的各种状态。
2.分析监控结果后决定是否需要优化 GC
在检查 GC 状态后,你需要分析监控结构并决定是否需要进行 GC 优化。如果分析结果显示运行 GC 的时间只有 0.1-0.3 秒,那么就不需要把时间浪费在 GC 优化上,但如果运行 GC 的时间达到 1-3 秒,甚至大于 10 秒,那么 GC 优化将是很有必要的。
但是,如果你已经分配了大约 10GB 内存给 Java,并且这些内存无法省下,那么就无法进行 GC 优化了。在进行 GC 优化之前,你需要考虑为什么你需要分配这么大的内存空间,如果你分配了 1GB 或 2GB 大小的内存并且出现了OutOfMemoryError
,那你就应该执行堆快照(heap dump)来消除导致异常的原因。
注意:
堆快照(heap dump)是一个用来检查 Java 内存中的对象和数据的内存文件。该文件可以通过执行 JDK 中的jmap
命令来创建。在创建文件的过程中,所有 Java 程序都将暂停,因此,不要在系统执行过程中创建该文件。
你可以在互联网上搜索 heap dump 的详细说明。
3.设置 GC 类型/内存大小
如果你决定要进行 GC 优化,那么你需要选择一个 GC 类型并且为它设置内存大小。此时如果你有多个服务器,请如上文提到的那样,在每台机器上设置不同的 GC 参数并分析它们的区别。
4.分析结果
在设置完 GC 参数后就可以开始收集数据,请在收集至少 24 小时后再进行结果分析。如果你足够幸运,你可能会找到系统的最佳 GC 参数。如若不然,你还需要分析输出日志并检查分配的内存,然后需要通过不断调整 GC 类型/内存大小来找到系统的最佳参数。
5.如果结果令人满意,将参数应用到所有服务器上并结束 GC 优化
如果 GC 优化的结果令人满意,就可以将参数应用到所有服务器上,并停止 GC 优化。
在下面的章节中,你将会看到上述每一步所做的具体工作。
命令
jmap
jmap 即 JVM Memory Map。
jmap 用于生成 heap dump 文件。
如果不使用这个命令,还可以使用 -XX:+HeapDumpOnOutOfMemoryError
参数来让虚拟机出现 OOM 的时候,自动生成 dump 文件。
jmap 不仅能生成 dump 文件,还可以查询 finalize 执行队列、Java 堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
命令格式:
jmap [option] LVMID
option 参数:
-
dump - 生成堆转储快照
-
finalizerinfo - 显示在 F-Queue 队列等待 Finalizer 线程执行 finalizer 方法的对象
-
heap - 显示 Java 堆详细信息
-
histo - 显示堆中对象的统计信息
-
permstat - to print permanent generation statistics
-
F - 当-dump 没有响应时,强制生成 dump 快照
示例:jmap -dump PID 生成堆快照
dump 堆到文件,format 指定输出格式,live 指明是活着的对象,file 指定文件名
$ jmap -dump:live,format=b,file=dump.hprof 28920 Dumping heap to /home/xxx/dump.hprof ... Heap dump file created
dump.hprof 这个后缀是为了后续可以直接用 MAT(Memory Anlysis Tool)打开。
示例:jmap -heap 查看指定进程的堆信息
注意:使用 CMS GC 情况下,jmap -heap 的执行有可能会导致 java 进程挂起。
jmap -heap PID
[root@chances bin]# ./jmap -heap 12379
Attaching to process ID 12379, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 17.0-b16
using thread-local object allocation.
Parallel GC with 6 thread(s)
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 83886080 (80.0MB)
NewSize = 1310720 (1.25MB)
MaxNewSize = 17592186044415 MB
OldSize = 5439488 (5.1875MB)
NewRatio = 2
SurvivorRatio = 8
PermSize = 20971520 (20.0MB)
MaxPermSize = 88080384 (84.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 9306112 (8.875MB)
used = 5375360 (5.1263427734375MB)
free = 3930752 (3.7486572265625MB)
57.761608714788736% used
From Space:
capacity = 9306112 (8.875MB)
used = 3425240 (3.2665634155273438MB)
free = 5880872 (5.608436584472656MB)
36.80634834397007% used
To Space:
capacity = 9306112 (8.875MB)
used = 0 (0.0MB)
free = 9306112 (8.875MB)
0.0% used
PS Old Generation
capacity = 55967744 (53.375MB)
used = 48354640 (46.11457824707031MB)
free = 7613104 (7.2604217529296875MB)
86.39733629427693% used
PS Perm Generation
capacity = 62062592 (59.1875MB)
used = 60243112 (57.452308654785156MB)
free = 1819480 (1.7351913452148438MB)
97.06831451706046% used