转载

聊聊JVM调优(一)

聊聊 JVM 调优(一)

上几篇文章,我们聊了一些 JVM 内存结构, GC 算法之类的一些内容,想必大家这些都听得多了,那么我们来点实践性的东西—— JVM 的调优。

由于 JVM 的参数众多,调优也是一个非常大的主题,不大可能在一期文章里面聊完,我们计划调优的文章分三期来聊。

  • 第一篇(也就是我们这一篇啦),主要介绍的是关联 内存 相关的调优,跟我们上期 JVM 内存结构紧密相关。
  • 第二篇,我们会介绍 GC 相关的一些参数调优,当然这里可能不一定能较好地体现 调优 这个主题,可能更多的是介绍一些调整参数。
  • 第三篇,虚拟机运行的一些参数调优,包括 GC 日志输出等等。

其实还有一些其他的调优参数,我们把它归在第三篇,虚拟机运行的参数调优上。

那么,我们就开始这一期的主题啦—— 内存 调优

聊聊JVM调优(一)

注意,我们本系列的所有调优参数,只针对 JDK 8 ,至于 JDK 8 一些废弃的比如 PermSizeMaxPermSize 等参数,我们这里就不细说了,有兴趣的同学可以找找其他文章哈。

参数列表

配置 描述 示例
-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 了非常多的对象,所以,就把我们的堆挣爆了,当我们运行的时候,我们可以看到这样的错误:

聊聊JVM调优(一)

某些版本还会抛出这样的错误:

聊聊JVM调优(一)

实际上这两个可以理解为同样的错误,都是因为执行了多次的GC,但得不到有效可用的空间。我们可以通过参数 -XX:-UseGCOverheadLimit 这样就可以变为平常的 Java heap space ,当然,并不建议这样做。

基于上面的情况,当我们下次遇到这样的错误的时候( OOM 并且是 heap space ),那我们就知道是堆内存不足了,就应该考虑一下扩大堆内存——通过调整 Xmx

但同时,调整该参数不能盲目调整,如果你的应用不是属于非常耗内存(即同一时间要生成大量对象),那么这个值一般情况下 2g 左右应该是够的,如果出现异常,并且内存使用增长地非常快,那就要考虑一下是不是 内存溢出 了。

-Xmn 堆内存新生代大小

前面我们是指定了整个堆内存的大小,那么如果我们希望限制一下新生代的大小,让更多的对象分配到老年代(假设我们的应用需要比较多的新建对象,并且对象的生命周期非常长,类似 springbean ),那么我们就可以指定 -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 + fromeden + to 的大小,因为 fromto 是互为备份,只能用一个。

当然, PrintGCDetail 还会打印 GC 的一些日志,比如 YGCFGC 等,这些我们后面涉及到这个命令再详聊。

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 新生代中的 edens0s1 的比例

这个参数完全是用于控制新生代的内存大小的,依旧是上面的代码示例, JVM 参数改为:

-Xms1024k -Xmx12288k -XX:+PrintGCDetails -Xmn4096k -XX:SurvivorRatio=6

这里我们限定了让 eden 区和 s0 的比例,也就意味着总的份数为8, s0s 各占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)。

MetaspaceSizeMaxMetaspaceSize 设置元空间

我们之前说过旧版本的 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 了。

运行一下,我们可以看到下面的错误。

聊聊JVM调优(一)

Xss 设置线程栈大小

前面我们说过, JVM 栈是每个线程独占的一块空间,当调用栈的深度或需要的内存超过了一定的值,这个值也就是我们这里的值,它就会抛出 StackOverflowOutOfMemory 异常。

我们来看一下下面的例子:

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

原文  https://segmentfault.com/a/1190000022599255
正文到此结束
Loading...