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 中最基本的一些针对内存结构的参数,希望下次在出现某些错误的时候,我们能够有针对性地调整参数以做一些优化。