聊聊JVM调优(一)

上几篇文章,我们聊了一些JVM内存结构,GC算法之类的一些内容,想必大家这些都听得多了,那么我们来点实践性的东西——JVM的调优。
由于JVM的参数众多,调优也是一个非常大的主题,不大可能在一期文章里面聊完,我们计划调优的文章分三期来聊。

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

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

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

注意,我们本系列的所有调优参数,只针对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了非常多的对象,所以,就把我们的堆挣爆了,当我们运行的时候,我们可以看到这样的错误:
图片 12png.png
某些版本还会抛出这样的错误:
图片 13png.png
实际上这两个可以理解为同样的错误,都是因为执行了多次的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了。

运行一下,我们可以看到下面的错误。
图片 14.png

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://www.cnblogs.com/happyflyingpig/p/8886952.html

我开通公众号了,有兴趣的朋友可以支持下哈
qrcode_for_gh_2c07e1ab847b_258.jpg

shun记忆
92 声望4 粉丝