堆栈空间配置
JVM 中最重要的一部分就是堆空间了,基本上大多数的线上 JVM 问题都是因为堆空间造成的 OutOfMemoryError。因此掌握 JVM 关于堆空间的参数配置对于排查线上问题非常重要。
tips:本文所有配置,如无特别说明,均基于JDK1.8。
堆配置
我们使用 -Xms 设置堆的初始空间大小,使用 -Xmx 设置堆的最大空间大小。
java -Xms20m -Xmx30m GCDemo
在上面的命令中,我们设置 JVM 的初始堆大小为 20M,最大堆空间为 30M。
年轻代
在 JDK1.8 中,堆分为年轻代和老年代。JVM 提供了参数 -Xmn 来设置年轻代内存的大小,但没有提供参数设置老年代的大小。但其实老年代的大小就等于堆大小减去年轻代大小。
java -Xms20m -Xmn10M GCDemo
上面的命令中,我们设置 JVM 堆初始大小为20M。其中年轻代的大小为 10M,那么剩下的就是老年代的大小,有 10M了。 我们可以给上述命令加上-XX:+PrintGCDetails
参数来查看内存区域的分配信息。
如上图所示,我们可以看到老年代的大小为 10M。
Eden区
在年轻代中,分为三个区域,分别是:eden 空间、from 空间、to 空间。如果要设置这部分的大小,那么就使用 -XX:SurvivorRatio 这个参数,该参数设置 eden / from 空间的比例关系,该参数的公式如下:
-XX:SurvivorRatio = eden/from = eden/to
例如我们的年轻代有 10 M,而我们设置 -XX:SurvivorRatio 参数为 2。也就是说 eden / from = eden / to = 2
。这里教一个快速计算的方法,我们假设 eden = 2,那么 from = 1,to = 1,那么 eden + from + to = 10M。这样就可以算出每一份大小是 10/4 = 2.5M。所以 Eden 区 = 2.5 * 2 = 5M,from 区是 2.5 M,to 区是 2.5 M。
下面我们运行下命令来验证一下。
java -Xms20m -Xmn10M -XX:SurvivorRatio=2 -XX:+PrintGCDetails GCDemo
在上面的启动参数中,我们设置堆初始大小为 20M,年轻代大小为 10M,年轻代的 SurvivorRatio 比例为 2。那么最终分配的结果将会是:年轻代 10M,其中 Eden 区 5M、From 区 2.5M、To 区 2.5 M,老年代 10M。
从上图可以看到:eden 空间是 5120 K,from 和 to 空间是 2560 K。
上图还有一个细节,即 PSYoungGen 这里的 total 只有 7680K,难道年轻代只有 7.5M 的内存吗?为什么不是 10M 呢?其实是因为这里的 total 指的是可用内存,from space 和 to space 两个区域,同一时间只有一个区域是可以用的。所以可用内存是 5120 + 2560 = 7680。
永久代(JDK1.7)
在 JDK 1.8 之前,所加载的类信息都放在永久代中。我们用 -XX:PermSize 设置永久代初始大小,用 -XX:MaxPermSize 设置永久代最大大小。
java -XX:PermSize10m -XX:MaxPermSize50m -XX:+PrintGCDetails GCDemo
在上面的启动参数中,我们设置永久代初始大小为 10M,最大大小为 50M。我们在 JDK1.7 的环境下运行上面的命令,会看到如下的 GC 日志。
在上图中,我们可以看到永久代的大小为我们设置的 10M。
元空间(JDK1.8)
在 JDK 1.8 之前,所有加载的类信息都放在永久代中。但在 JDK1.8 之时,永久代被移除,取而代之的是元空间(Metaspace)。在元空间这块内存中,有两个参数很相似,它们是: -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize。
java -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=50m -XX:+PrintGCDetails GCDemo
上面的命令中,我们设置 MetaspaceSize 为 10M,MaxMetaspaceSize 为 50M。但其实它们并不是设置初始大小和最大大小的。
从上面的执行结果可以看到,Metaspace 空间的大小为 2.6M 左右,并不是我们设置的 10M。那是因为 MetaspaceSize 设置的是元空间发生 GC 的初始阈值。当达到这个值时,元空间发生 GC 操作,这个值默认是 20.8M。而 MaxMetaspaceSize 则是设置元空间的最大大小,默认基本是机器的物理内存大小。虽然可以不设置,但还是建议设置一下,因为如果一直不断膨胀,那么 JVM 进程可能会被 OS kill 掉。
栈空间
栈空间是每个线程各自有的一块区域,如果栈空间太小,也会导致 StackOverFlow 异常。而要设置栈空间大小,只需要使用 -Xss 参数就可以。
java -Xss2m GCDemo
上面的启动命令设置最大栈空间为 2M。
直接内存
在 JVM 中还有一块内存,它独立于 JVM 的堆内存,它就是:直接内存。我们可以使用 -XX:MaxDirectMemorySize 设置最大直接内存。如果不设置,默认为最大堆空间,即 -Xmx。
java -XX:MaxDirectMemorySize=50m GCDemo
上面的启动命令设置直接内存最大值为 50M。
当直接内存使用达到设置值时,就会触发垃圾回收。如果不能有效释放足够空间,就会引发直接内存溢出导致系统的 OOM。
总结
参数 | 含义 |
---|---|
-Xms | 初始堆大小 |
-Xmx | 最大堆空间 |
-Xmn | 设置新生代大小 |
-XX:SurvivorRatio | 设置新生代eden空间和from/to空间的比例关系 |
-XX:PermSize | 方法区初始大小 |
-XX:MaxPermSize | 方法区最大大小 |
-XX:MetaspaceSize | 元空间GC阈值(JDK1.8) |
-XX:MaxMetaspaceSize | 最大元空间大小(JDK1.8) |
-Xss | 栈大小 |
-XX:MaxDirectMemorySize | 直接内存大小,默认为最大堆空间 |
相关查看参数
打印显式参数 -XX:+PrintVMOptions
该参数表示程序运行时,打印虚拟机接受到的命令行显式参数。我们用下面的命令运行程序:
java -XX:+UseSerialGC -XX:+PrintVMOptions com.chenshuyi.ClassLoadDemo
输出结果:
VM option '+UseSerialGC' VM option '+PrintVMOptions' Hello, I'm chenshuyi
可以看到我们设置了+UseSerialGC
和+PrintVMOptions
两个参数,最后运行时也将这两个参数打印出来了。
打印显式隐式参数 -XX:+PrintCommandLineFlags
该参数打印传递给虚拟机的显式和隐式参数。我们用下面的命令运行程序:
java -XX:+UseSerialGC -XX:+PrintCommandLineFlags com.chenshuyi.ClassLoadDemo
输出结果:
-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC Hello, I'm chenshuyi
可以看到程序不仅输出了我们显式设置的参数,还将虚拟机默认的参数打印了出来,包括初始堆大小(134217728),最大堆大小(2147483648)等。
打印所有系统参数 -XX:+PrintFlagsFinal
该参数会打印所有的系统参数的值。我们用下面的命令运行程序:
java -XX:+UseSerialGC -XX:+PrintFlagsFinal com.chenshuyi.ClassLoadDemo > jvm_flag_final.txt
之后打开 jvm_flag_final.txt 文件,可以看到有 800 多行,这是因为程序将虚拟机的所有参数都打印了出来。下面列几个我们常用的参数看看就可以:
... uintx InitialHeapSize := 134217728 {product} ... uintx MaxMetaspaceSize = 18446744073709547520 {product} ... uintx MetaspaceSize = 21807104 {pd product}
从上面我们列出的部分参数可以看到,上面三个参数分别是设置初始堆大小、元空间最大大小、初始元空间大小。
最后,让我们来总结一下,加强记忆。
- -XX:+PrintVMOptions 程序运行时,打印虚拟机接受到的命令行显式参数。
- -XX:+PrintCommandLineFlags 打印传递给虚拟机的显式和隐式参数。
- -XX:+PrintFlagsFinal 打印所有的系统参数的值
gc日志配置
说到 Java 虚拟机,不得不提的就是 Java 虚拟机的 GC(Garbage Collection)日志。而对于 GC 日志,我们不仅要学会看懂,而且要学会如何设置对应的 GC 日志参数。今天就让我们来学习一下 Java 虚拟机中所有与 GC 日志有关的参数。相信掌握了这些参数之后,对于大家线上打印 GC 日志是有不少帮助的。
为了能够更直观地显示出每个参数的作用,我们将以下面的 Demo 为例子去设置 GC 日志参数。
/** * @author 陈树义 * @date 2018.09.29 */ public class GCDemo { public static void main(String[] args) { // allocate 4M space byte[] b = new byte[4 * 1024 * 1024]; System.out.println("first allocate"); // allocate 4M space b = new byte[4 * 1024 * 1024]; System.out.println("second allocate"); } }
在上面的程序中,我们两次分配了 4M 的内存空间。为了认为制造 GC,我们启动时的 JVM 参数固定加上下面几个参数:
-XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
- -XX:+UseSerialGC 表示强制使用Serial+SerialOld收集器组合
- -Xms20m 表示堆空间初始大小为 20 M。
- -Xmx20m 表示堆空间最大大小为 20 M。
- -Xmn10m 表示新生代大小为 10M。
- -XX:SurvivorRatio=8 表示Eden:Survivor=8:1
经过上面这个设置,此时我们的堆空间的内存比例情况如下:Eden区 8M,FromSurvivor 1M,ToSurvivor 1M,老年代 10M。
下面就让我们来看看油管 GC 的参数有哪些吧。
打印GC日志
在 GC 日志参数中,最简单的一个参数就是打印 GC 日志:-XX:PrintGC。我们用下面的命令运行程序:
java -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGC com.chenshuyi.GCDemo
输出结果:
first allocate second allocate [GC (Allocation Failure) 4767K->4374K(19456K), 0.0045179 secs]
可以看到程序在第一次分配数组空间的时候发生了 GC,并且把 GC 前后以及堆空间大小都打印了出来。该日志显示 GC 前堆空间使用量为 4767K(4M左右)。GC 后堆空间为 4374K,当前可用堆大小为 19456K。
但你会发现使用 PrintGC 参数打印出来的日志比较简单,无法查看更详细的信息。如果你要查看更详细的信息,那么就需要下面这个参数。
打印详细GC日志
如果要查看更加详细的 GC 日志,那么就要使用 -XX:+PrintGCDetils 参数。下面我们使用该参数运行程序:
java -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGC com.chenshuyi.GCDemo
程序输出:
first allocate second allocate [GC (Allocation Failure) [DefNew: 4603K->278K(9216K), 0.0036744 secs] 4603K->4374K(19456K), 0.0037100 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4538K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000) eden space 8192K, 52% used [0x00000007bec00000, 0x00000007bf0290e0, 0x00000007bf400000) from space 1024K, 27% used [0x00000007bf500000, 0x00000007bf545920, 0x00000007bf600000) to space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000) tenured generation total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000) the space 10240K, 40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000) Metaspace used 2649K, capacity 4486K, committed 4864K, reserved 1056768K class space used 286K, capacity 386K, committed 512K, reserved 1048576K
从上面的日志可以看出,该参数能打印出更加详细的 GC 信息,包括:年轻代的信息、永久代的信息。
[GC (Allocation Failure) [DefNew: 4603K->278K(9216K), 0.0036744 secs] 4603K->4374K(19456K), 0.0037100 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
该参数还会在退出之前打印出整个堆的详细信息:
Heap def new generation total 9216K, used 4538K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000) eden space 8192K, 52% used [0x00000007bec00000, 0x00000007bf0290e0, 0x00000007bf400000) from space 1024K, 27% used [0x00000007bf500000, 0x00000007bf545920, 0x00000007bf600000) to space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000) tenured generation total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000) the space 10240K, 40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000) Metaspace used 2649K, capacity 4486K, committed 4864K, reserved 1056768K class space used 286K, capacity 386K, committed 512K, reserved 1048576K
GC前后打印堆信息
上面两个命令基本上可以应付 90% 的使用场景了,但有时候我们在 GC 前后还想获取更加详细的信息。那么我们可以使用 PrintHeapAtGC 参数,该参数会在 GC 前后打印堆信息。
使用下面的命令运行程序:
java -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintHeapAtGC com.chenshuyi.GCDemo
输出结果:
first allocate second allocate {Heap before GC invocations=0 (full 0): def new generation total 9216K, used 4767K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000) eden space 8192K, 58% used [0x00000007bec00000, 0x00000007bf0a7e98, 0x00000007bf400000) from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000) to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000) tenured generation total 10240K, used 0K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000) the space 10240K, 0% used [0x00000007bf600000, 0x00000007bf600000, 0x00000007bf600200, 0x00000007c0000000) Metaspace used 2646K, capacity 4486K, committed 4864K, reserved 1056768K class space used 286K, capacity 386K, committed 512K, reserved 1048576K Heap after GC invocations=1 (full 0): def new generation total 9216K, used 278K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000) eden space 8192K, 0% used [0x00000007bec00000, 0x00000007bec00000, 0x00000007bf400000) from space 1024K, 27% used [0x00000007bf500000, 0x00000007bf545950, 0x00000007bf600000) to space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000) tenured generation total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000) the space 10240K, 40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000) Metaspace used 2646K, capacity 4486K, committed 4864K, reserved 1056768K class space used 286K, capacity 386K, committed 512K, reserved 1048576K }
仔细看一下,会发现在 GC 发生前后都打印了一次堆空间信息。
通过这个参数,我们可以详细了解每次 GC 时堆空间的详细信息。
打印GC发生的时间 -XX:+PrintGCTimeStamps
这个参数非常简单,就是在每次 GC 日志的前面加上一个时间戳。这个时间戳表示 JVM 启动后到现在所逝去的时间。
使用下面的参数运行程序:
java -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGC -XX:+PrintGCTimeStamps com.chenshuyi.GCDemo
输出结果:
first allocate second allocate 0.130: [GC (Allocation Failure) 4767K->4374K(19456K), 0.0051351 secs]
上面日志第 3 行中的「0.130」就是该 GC 发生的时间。
-XX:+PrintGCApplicationConcurrentTime 打印应用程序的执行时间
使用下面的命令运行程序:
java -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGC -XX:+PrintGCApplicationConcurrentTime com.chenshuyi.GCDemo
运行结果:
first allocate second allocate Application time: 0.0371892 seconds [GC (Allocation Failure) 4767K->4374K(19456K), 0.0040074 secs] Application time: 0.0010712 seconds
-XX:+PrintGCApplicationStoppedTime 打印应用由于GC而产生的停顿时间
使用下面的命令运行程序:
java -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGC -XX:+PrintGCApplicationStoppedTime com.chenshuyi.GCDemo
运行结果:
first allocate second allocate [GC (Allocation Failure) 4767K->4374K(19456K), 0.0045644 secs] Total time for which application threads were stopped: 0.0047873 seconds, Stopping threads took: 0.0000329 seconds
可以看到最后一行打印出了因为 GC 而暂停的时间。
保存GC日志 -Xloggc
这个参数可以将 GC 日志输出到文件中保存起来。
使用下面的参数运行程序:
java -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGC -XX:+PrintReferenceGC -Xloggc:gc.log com.chenshuyi.GCDemo
运行之后在本目录会生成一个 gc.log 文件,打开该文件:
Java HotSpot(TM) 64-Bit Server VM (25.181-b13) for bsd-amd64 JRE (1.8.0_181-b13), built on Jul 7 2018 01:02:31 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00) Memory: 4k page, physical 8388608k(45132k free) /proc/meminfo: CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:+PrintGC -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC 0.124: [GC (Allocation Failure) 4767K->4374K(19456K), 0.0047748 secs]
可以看到堆的相关信息,以及 GC 的信息。
总结
除了上面这些参数,还有可以查看弱引用的参数:-XX:+PrintReferenceGC。它跟踪软引用、弱引用、虚引用和Finallize队列的信息,但是使用场景较为狭窄。基本上掌握上面的几个常用的 GC 日志参数就足够排查使用,最重要的是弄清楚每个参数的作用和用法。
最后用列表的形式总结一下,加深一下印象。
参数 | 含义 |
---|---|
-XX:PrintGC | 打印GC日志 |
-XX:+PrintGCDetails | 打印详细的GC日志。还会在退出前打印堆的详细信息。 |
-XX:+PrintHeapAtGC | 每次GC前后打印堆信息。 |
-XX:+PrintGCTimeStamps | 打印GC发生的时间。 |
-XX:+PrintGCApplicationConcurrentTime | 打印应用程序的执行时间 |
-XX:+PrintGCApplicationStoppedTime | 打印应用由于GC而产生的停顿时间 |
-XX:+PrintReferenceGC | 跟踪软引用、弱引用、虚引用和Finallize队列。 |
-XLoggc | 将GC日志以文件形式输出。 |