JVM性能调优
- Java内存区域
在几次面试中,被问到JVM性能调优。由于项目经验少且没有接触到大型项目,对JVM性能调优的认知几乎为0,面试之前背了这方面的知识点,但记忆得不深刻,面试时还是哑口无言。
最近看了个关于JVM性能调优视频,感觉讲得挺好的,下面结合视频以及一些资料,总结一下JVM性能调优的知识。
Java内存区域
JVM性能优化主要是体现在调整Java内存区域的一些重要参数。比如,JVM对整个堆内存进行full gc时,会STW(Stop the World,只运行垃圾回收线程,而暂停其他Java线程),STW过程会造成系统“卡顿”,用户体验感降低。因此,需要降低full gc的频率,full gc的触发条件是老年代堆内存放满,而老年代堆内存又与年轻代堆内存有密切关系。如果年轻代堆内存过小,老年代堆内存就会很快放满,full gc就会不断被触发,造成系统卡顿。如果年轻代堆内存过大,会造成一次垃圾收集(minor gc)的时间过长,也会造成系统卡顿。只有根据实际情况,调控堆内存空间各区域的比例,才能优化系统。
优化的JVM的前提是知道Java内存区域的构成。
大致描述一下,.java文件经过编译器编译成.class文件,.class文件进入JVM被类加载子系统进行加工,为字节码执行作准备,最后由字节码执行引擎输出该.class对应的机器码文件,机器码文件交由操作系统执行。
上述这些内容想必大家也见不少,背过也忘过。个人认为,这些模型必须通过实际案例,将各区域的分工合作关系理清,才能够真正地理解和记住。
下面给我一段源码以及.class的反汇编结果:
public class JvmTest1 {
public static void main(String[] args) {
Animal a = new Animal("狗","汪汪汪");
Animal b = new Animal("猫","喵喵喵");
}
}
class Animal{
private String name;
private String call;
public Animal(String name, String call) {
this.name = name;
this.call = call;
}
}
对.class进行反汇编,并打印到JvmTest1文件:
给出部分反汇编内容:
Compiled from "JvmTest1.java"
Code:
0: new #2 // class Animal
3: dup
4: ldc #3 // String 狗
6: ldc #4 // String 汪汪汪
8: invokespecial #5 // Method Animal."<init>":(Ljava/lang/String;Ljava/lang/String;)V
11: astore_1
12: new #2 // class Animal
15: dup
16: ldc #6 // String 猫
18: ldc #7 // String 喵喵喵
20: invokespecial #5 // Method Animal."<init>":(Ljava/lang/String;Ljava/lang/String;)V
23: astore_2
下面主要看看JvmTest1.class的反汇编结果,我将结合Java运行时数据区各模块的协作来分析:
虚拟机栈是线程私有的(每个线程都有自己的虚拟机栈),当main线程运行时,main方法就压入main线程的虚拟机栈了,虚拟机栈的构成如下图所示,
可以看到,虚拟机栈的组成单位是栈帧,而每个栈帧由4部分组成:
- 局部变量表:存储该方法的局部变量
- 操作数栈:存放数值
- 动态链接:
- 方法出口:函数结束后,返回到调用该方法的地方
Animal a = new Animal("狗","汪汪汪");
创建Animal对象,Java内存区域都有哪些变化呢?根据观察容易发现,这段代码对应反汇编的:
0: new #2 // class Animal
3: dup
4: ldc #3 // String 狗
6: ldc #4 // String 汪汪汪
8: invokespecial #5 // Method Animal."<init>":(Ljava/lang/String;Ljava/lang/String;)V
11: astore_1
Java创建一个对象的过程比较复杂,这里简单说说,执行new
指令时Jvm会去常量池中定位一个类的符号引用,并且检查符号引用代表的类是否已经被加载、解析和初始化过,如果没有则需要进过类加载过程;如果有,则在堆空间中挖出一块空间给该对象,并且赋零值和设置对象头信息,最后执行invokespecial
指令,调用该类的构造函数,按照程序员的意愿对对象进行初始化。
以上是《深入理解Java虚拟机》一书中提到的,但根据反汇编结果来看,new与invokespecial之间还经过了dup和两次ldc,为什么呢?
查阅资料发现,new指令做了两件事:创建对象和并将该对象的引用地址压入操作数栈。而dup指令做的事是:复制操作数栈栈顶元素的值,并将其压入栈顶。这句话有点绕,简单地说,执行完dup之后,操作数栈顶有两个连续相同的两个对象的引用地址,对象的引用地址就是常听到的this
。而两次ldc的作用是:获取常量池中的“狗”和“汪汪汪”,将它们压入操作数栈栈顶。此时操作数栈顶的前3元素依次是:“汪汪汪”、“狗”、this。
这三个元素都是为了invokespecial准备的。现在栈顶元素是剩下的this。
最后执行astore_1指令将this存到局部变量表中的a变量中,然后this出栈。
以上就是创建一个Animal对象的大致过程
未完待续…