JVM 中最重要的一部分就是堆空间了,基本上大多数的线上 JVM 问题都是因为堆空间造成的 OutOfMemoryError。因此掌握 JVM 关于堆空间的参数配置对于排查线上问题非常重要。
堆配置
我们使用 -Xms 设置堆的初始空间大小,使用 -Xmx 设置堆的最大空间大小。
java -Xms20m -Xmx30m GCDemo
在上面的命令中,我们设置 JVM 的初始堆大小为 20M,最大堆空间为 30M。
年轻代
在 JDK1.8 中,堆分为年轻代和老年代。JVM 提供了参数 -Xmn 来设置年轻代内存的大小,但没有提供参数设置老年代的大小。但其实老年代的大小就等于堆大小减去年轻代大小。
java -Xms20m -Xmn10M GCDemo
上面的命令中,我们设置 JVM 堆初始大小为20M。其中年轻代的大小为 10M,那么剩下的就是老年代的大小,有 10M了。 我们可以给上述命令加上 -XX:+PrintGCDetails 参数来查看内存区域的分配信息。
# java -Xms20m -Xmn10M -XX:+PrintGCDetails
Heap
PSYoungGen total 9216K, used 671K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 8% used [0x00000000ff600000,0x00000000ff6a7dc8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 0K [0x0000000085c00000, 0x0000000086600000, 0x00000000ff600000)
object space 10240K, 0% used [0x0000000085c00000,0x0000000085c00000,0x0000000086600000)
Metaspace used 2540K, capacity 4480K, committed 4480K, reserved 1056768K
class space used 283K, capacity 384K, committed 384K, reserved 1048576K
如上图所示,我们可以看到老年代的大小为 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。
# java -Xms20m -Xmn10M -XX:SurvivorRatio=2 -XX:+PrintGCDetails GCDemo
Error: Could not find or load main class GCDemo
Heap
PSYoungGen total 7680K, used 631K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 5120K, 12% used [0x00000000ff600000,0x00000000ff69dc20,0x00000000ffb00000)
from space 2560K, 0% used [0x00000000ffd80000,0x00000000ffd80000,0x0000000100000000)
to space 2560K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffd80000)
ParOldGen total 10240K, used 0K [0x0000000085c00000, 0x0000000086600000, 0x00000000ff600000)
object space 10240K, 0% used [0x0000000085c00000,0x0000000085c00000,0x0000000086600000)
Metaspace used 2540K, capacity 4480K, committed 4480K, reserved 1056768K
class space used 283K, capacity 384K, committed 384K, reserved 1048576K
从上图可以看到: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 日志。
java -XX:PermSize10m -XX:MaxPermSize50m -XX:+PrintGCDetails GCDemo
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize10m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize50m; support was removed in 8.0
Error: Could not find or load main class GCDemo
Heap
PSYoungGen total 36864K, used 1269K [0x00000000d7400000, 0x00000000d9d00000, 0x0000000100000000)
eden space 31744K, 4% used [0x00000000d7400000,0x00000000d753d728,0x00000000d9300000)
from space 5120K, 0% used [0x00000000d9800000,0x00000000d9800000,0x00000000d9d00000)
to space 5120K, 0% used [0x00000000d9300000,0x00000000d9300000,0x00000000d9800000)
ParOldGen total 84992K, used 0K [0x0000000085c00000, 0x000000008af00000, 0x00000000d7400000)
object space 84992K, 0% used [0x0000000085c00000,0x0000000085c00000,0x000000008af00000)
Metaspace used 2541K, capacity 4480K, committed 4480K, reserved 1056768K
class space used 283K, capacity 384K, committed 384K, reserved 1048576K
从上面的执行结果可以看到,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:+PrintGC | 输出GC日志 |
-XX:+PrintGCDateStamps | 输出GC的时间戳(以日期的形式) |
-XX:+PrintGCTimeStamps | 输出GC的时间戳(以基准时间的形式) |
-XX:+PrintGCDetails | 输出GC的详细日志 |
-Xloggc | gc日志文件的输出路径 |
-XX:+UseConcMarkSweepGC | 设置年老代为并发收集 |
-XX:+UseParNewGC | 设置年轻代为并发收集 |
-XX:ParallelGCThreads | 配置并行收集器的线程数 |
-XX:+UseCMSCompactAtFullCollection | 打开对年老代的压缩 |
-XX:CMSFullGCsBeforeCompaction | 设置运行次FullGC以后对内存空间进行压缩、整理 |
-XX:CMSInitiatingOccupancyFraction | 设置年老代空间到?%时就开始执行CMS |