聊聊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
中最基本的一些针对内存结构的参数,希望下次在出现某些错误的时候,我们能够有针对性地调整参数以做一些优化。
参考文章
https://www.cnblogs.com/happyflyingpig/p/8886952.html
我开通公众号了,有兴趣的朋友可以支持下哈
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。