JVM
调优(一) 上几篇文章,我们聊了一些 JVM
内存结构, GC
算法之类的一些内容,想必大家这些都听得多了,那么我们来点实践性的东西—— JVM
的调优。
由于 JVM
的参数众多,调优也是一个非常大的主题,不大可能在一期文章里面聊完,我们计划调优的文章分三期来聊。
内存
相关的调优,跟我们上期 JVM
内存结构紧密相关。 GC
相关的一些参数调优,当然这里可能不一定能较好地体现 调优
这个主题,可能更多的是介绍一些调整参数。 GC
日志输出等等。 其实还有一些其他的调优参数,我们把它归在第三篇,虚拟机运行的参数调优上。
那么,我们就开始这一期的主题啦—— 内存
调优
注意,我们本系列的所有调优参数,只针对 JDK 8
,至于 JDK 8
一些废弃的比如 PermSize
和 MaxPermSize
等参数,我们这里就不细说了,有兴趣的同学可以找找其他文章哈。
配置 | 描述 | 示例 |
---|---|---|
-Xms | 设置最小堆内存 | -Xmx1g |
-Xmx | 设置最大堆内存 | -Xmx2g |
-Xmn | 设置新生代内存 | -Xmn128m |
-XX:NewRatio | 指定老年代的堆大小和新生代的堆大小比例 | -XX:NewRatio=2 |
-XX:SurvivorRatio | 指定New Generation中Eden Space与一个Survivor Space的heap size比例 | -XX:SurvivorRatio=8 |
-XX:MetaspaceSize | 设置 Metaspace (元数据空间)默认大小 |
-XX:MetaspaceSize=1g |
-XX:MaxMetaspaceSize | 设置 Metaspace (无数据空间)最大值 |
-XX:MetaspaceSize=4g |
-Xss | 设置线程栈的大小 | -Xss128k |
下面我们就一个个参数来看看是什么意思,以及我们遇到什么问题的时候应该从什么地方去考虑调整。
-Xms
和 -Xmx
堆内存大小 这两个参数主要用于指定堆内存, -Xms
用于指定初始的堆大小,也就是最小的(如果没有设就会由虚拟机启动时分配的内存决定,由新生代+老年代的内存相加得到),而 -Xmx
用于指定最大的,当需要的内存超出 Xmx
指定的内存时,就会抛出 OutofMemoryError
我们来看个示例
VM args: -Xms1024k -Xmx12288k public class TestMemorySize { public static void main(String[] args) { List<TestObject> list = new ArrayList<>(); for (int i = 0; i < Integer.MAX_VALUE; i ++) { list.add(new TestObject(i)); } } static class TestObject { private int val; public TestObject(int val) { this.val = val; } } }
基于前面的文章,我们知道,当我们 new
一个对象的时候,它是分配在堆上面的,而在此示例中,我们把堆的最大大小限制为 12m ,但我们 new
了非常多的对象,所以,就把我们的堆挣爆了,当我们运行的时候,我们可以看到这样的错误:
某些版本还会抛出这样的错误:
实际上这两个可以理解为同样的错误,都是因为执行了多次的GC,但得不到有效可用的空间。我们可以通过参数 -XX:-UseGCOverheadLimit
这样就可以变为平常的 Java heap space
,当然,并不建议这样做。
基于上面的情况,当我们下次遇到这样的错误的时候( OOM
并且是 heap space
),那我们就知道是堆内存不足了,就应该考虑一下扩大堆内存——通过调整 Xmx
。
但同时,调整该参数不能盲目调整,如果你的应用不是属于非常耗内存(即同一时间要生成大量对象),那么这个值一般情况下 2g
左右应该是够的,如果出现异常,并且内存使用增长地非常快,那就要考虑一下是不是 内存溢出 了。
-Xmn
堆内存新生代大小 前面我们是指定了整个堆内存的大小,那么如果我们希望限制一下新生代的大小,让更多的对象分配到老年代(假设我们的应用需要比较多的新建对象,并且对象的生命周期非常长,类似 spring
的 bean
),那么我们就可以指定 -XmnSize
我们在上面的例子上修改 VM
参数为:
-Xms1024k -Xmx12288k -XX:+PrintGCDetails -Xmn4096k
PrintGCDetails
为打印 GC
的情况,方便我们看到 新生代 的大小,这里我们后面讲到虚拟机参数时会再细讲,这里只需要知道它可以看到 GC
情况就好了。
我们再运行上面的例子,可以看到:
Heap PSYoungGen total 3072K, used 41K [0x00000007bfc00000, 0x00000007c0000000, 0x00000007c0000000) eden space 2048K, 2% used [0x00000007bfc00000,0x00000007bfc0a578,0x00000007bfe00000) from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000) to space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000) ParOldGen total 6144K, used 325K [0x00000007bf400000, 0x00000007bfa00000, 0x00000007bfc00000) object space 6144K, 5% used [0x00000007bf400000,0x00000007bf451750,0x00000007bfa00000) Metaspace used 2698K, capacity 4486K, committed 4864K, reserved 1056768K class space used 291K, capacity 386K, committed 512K, reserved 1048576K
新生代是 eden
+ from
+ to
的大小,我们算一下,是4096,也就是我们指定的大小,而上面的 total
表示新生代实际上可用的只是 eden
+ from
或 eden
+ to
的大小,因为 from
和 to
是互为备份,只能用一个。
当然, PrintGCDetail
还会打印 GC
的一些日志,比如 YGC
和 FGC
等,这些我们后面涉及到这个命令再详聊。
NewRatio
老年代和新生代的比例 一般情况下,我们使用默认的值2就可以了,就是老年代的大小是新生代的两倍,一般情况下不需要进行调整。但跟我们前面说明 Xmn
的时候所说的,如果你确定很多对象的存活时间比较长,那么你就把比例调大,我们还是以上面的代码示例来举例,使用下面的 JVM
参数
-Xms1024k -Xmx12288k -XX:+PrintGCDetails -XX:NewRatio=5
注意,我们这里没有用 Xmn
参数,如果指定了 Xmn
参数,它的优先级比 NewRatio
高,即就是指定了固定的新生代大小,那这里的比例就没用了,所以这里建议直接使用 Xmn
就好了,那么 Xmx-Xmn
剩余的大小实际上就是老年代的大小了
Heap PSYoungGen total 1536K, used 1138K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000) eden space 1024K, 61% used [0x00000007bfe00000,0x00000007bfe9c9e0,0x00000007bff00000) from space 512K, 99% used [0x00000007bff00000,0x00000007bff7ff00,0x00000007bff80000) to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000) ParOldGen total 10240K, used 9778K [0x00000007bf400000, 0x00000007bfe00000, 0x00000007bfe00000) object space 10240K, 95% used [0x00000007bf400000,0x00000007bfd8c948,0x00000007bfe00000) Metaspace used 2695K, capacity 4486K, committed 4864K, reserved 1056768K class space used 291K, capacity 386K, committed 512K, reserved 1048576K
我们可以看到新生代占了大概2m,剩余的接近8m就归
SurvivorRatio
新生代中的 eden
和 s0
、 s1
的比例 这个参数完全是用于控制新生代的内存大小的,依旧是上面的代码示例, JVM
参数改为:
-Xms1024k -Xmx12288k -XX:+PrintGCDetails -Xmn4096k -XX:SurvivorRatio=6
这里我们限定了让 eden
区和 s0
的比例,也就意味着总的份数为8, s0
和 s
各占1,而 eden
占6。
Heap PSYoungGen total 3584K, used 2877K [0x00000007bfc00000, 0x00000007c0000000, 0x00000007c0000000) eden space 3072K, 93% used [0x00000007bfc00000,0x00000007bfecf720,0x00000007bff00000) from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000) to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000) ParOldGen total 8192K, used 8119K [0x00000007bf400000, 0x00000007bfc00000, 0x00000007bfc00000) object space 8192K, 99% used [0x00000007bf400000,0x00000007bfbedc08,0x00000007bfc00000) Metaspace used 2697K, capacity 4486K, committed 4864K, reserved 1056768K class space used 291K, capacity 386K, committed 512K, reserved 1048576K
我们可以看到这里打印出来的 eden
区的大小确实是占了新生代的6/8(4096k*6/8=3072)。
MetaspaceSize
和 MaxMetaspaceSize
设置元空间 我们之前说过旧版本的 JVM
有一个叫 方法区 的,这里会保存 JVM
的类定义等,还包括一个常量池。而在 jdk8
就已经把它废弃,改为 元空间 ,而这个的大小默认受限于物理内存,所以对一些比如多类定义的项目来说,已经不会再经常看到 OutOfMemory:PermgenSpace
类似的错误了。
我们来看看下面的例子
VM args:-Xms10m -Xmx100m -XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=10m public class MetaSpaceTest extends ClassLoader{ public static void main(String[] args) { // 类持有 List<Class<?>> classes = new ArrayList<Class<?>>(); for (int i = 0; i < Integer.MAX_VALUE; ++i) { ClassWriter cw = new ClassWriter(0); // 定义一个类名称为Class{i},它的访问域为public,父类为java.lang.Object,不实现任何接口 cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); // 定义构造函数<init>方法 MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null); // 第一个指令为加载this mw.visitVarInsn(Opcodes.ALOAD, 0); // 第二个指令为调用父类Object的构造函数 mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); // 第三条指令为return mw.visitInsn(Opcodes.RETURN); mw.visitMaxs(1, 1); mw.visitEnd(); MetaSpaceTest test = new MetaSpaceTest(); byte[] code = cw.toByteArray(); // 定义类 Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length); classes.add(exampleClass); } } }
上面我们使用了 asm
帮我们动态生成大量的类定义,而这些类定义都会被放在 元空间 中,而这个大小受 MaxMetaspaceSize
限制。
在这个例子中,我们调整了一下VM的参数,把堆内存的数值都调整了一下,避免提前堆内存不够,抛出 OutofMemory
了。
运行一下,我们可以看到下面的错误。
Xss
设置线程栈大小 前面我们说过, JVM
栈是每个线程独占的一块空间,当调用栈的深度或需要的内存超过了一定的值,这个值也就是我们这里的值,它就会抛出 StackOverflow
或 OutOfMemory
异常。
我们来看一下下面的例子:
VM args:-Xms10m -Xmx1024m -Xss256k public class TestXssParameter { public static void main(String[] args) { try { TestXssParameter.testStack(); } catch (Exception|Error e) { e.printStackTrace(); System.out.println(TestXssParameter.stackSize); } } private static int stackSize = 0; public static void testStack() { stackSize ++; testStack(); } }
运行一下,我们可以看到下面的错误
java.lang.StackOverflowError at metaspace.TestXssParameter.testStack(TestXssParameter.java:21) at metaspace.TestXssParameter.testStack(TestXssParameter.java:21) at metaspace.TestXssParameter.testStack(TestXssParameter.java:21) at metaspace.TestXssParameter.testStack(TestXssParameter.java:21) ...省略 2539
栈溢出,预料之中,我们可以再调整一下 Xss
的值,观察一下我们输出的 stackSize
的大小变化,这里留给各位大家去试试。
这一篇文章中,我们了解了一下, JVM
中最基本的一些针对内存结构的参数,希望下次在出现某些错误的时候,我们能够有针对性地调整参数以做一些优化。