JAVA8 JVM内存模型
内存分配剖析
public class MathTest {
public static void main(String[] args) {
Math math = new Math();
int a = 2;
int b = 3;
int c = math.plus(a, b);
c= math.mult(c);
int d = math.plusAndMult(2, 3);
}
}
public class Math {
public int plus(int a, int b) { //一个方法对应一块栈帧内存区域
return a + b;
}
public Integer mult10(Integer a) {
return a * 10;
}
public Integer plusAndMult(int a, int b) {
return mult10(plus(a, b));
}
}
首先我们来看MathTest 对应的字节码信息
MathTest字节码
main
0 new #2 <top/wuweij/jvm/Math>
3 dup
4 invokespecial #3 <top/wuweij/jvm/Math.<init>>
7 astore_1
8 iconst_2
9 istore_2
10 bipush 7
12 istore_3
13 aload_1
14 iload_2
15 iload_3
16 invokevirtual #4 <top/wuweij/jvm/Math.plus>
19 istore 4
21 aload_1
22 iload 4
24 invokestatic #5 <java/lang/Integer.valueOf>
27 invokevirtual #6 <top/wuweij/jvm/Math.mult>
30 invokevirtual #7 <java/lang/Integer.intValue>
33 istore 4
35 aload_1
36 iconst_2
37 iconst_3
38 invokevirtual #8 <top/wuweij/jvm/Math.plusAndMult>
41 invokevirtual #7 <java/lang/Integer.intValue>
44 istore 5
46 return
这里我们结合jvm字节码来分析一下jvm内存模型。
执行命令 java MathTest
1. 判断class是否被加载,没有的话加载class(这个时候会把MathTest的信息加载到方法区/元空间)
2. 分配内存(由于main方法没有实例化 MathTest
不会在堆中分配内存,所以不存在this)
3. 线程执行,首先给main方法分配一个栈帧0
这个时候开始执行main方法(通过方法区/元空间获取)这里叫做静态链接
- 这个时候我们看一下main方法的局部变量表 局部变量都是通过位置访问的(和数组类似)
- 这里需要说明的是如果方法不是静态的方法,那么局部变量表中的第一个位置一定是
this
,这就是普通方法中可以使用this
而静态方法中没有this
的原因了。
4. 开执行 Math math = new Math()
这个时候的指令全部在操作数栈中完成,所以操作数栈开始为空,每执行完一条指令程序计数器都会修改,来确定执行到哪里。程序计数器用于垃圾收集器进行垃圾回收(STW)或者线程挂起后恢复现场用的。
0 new #2 <top/wuweij/jvm/Math>
3 dup
4 invokespecial #3 <top/wuweij/jvm/Math.<init>>
7 astore_1
new #2 <top/wuweij/jvm/Math>
- 类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个 符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
- 内存分配。
这个时候其实就是在栈中分配一个引用地址,然后在堆中分配一个空间给Math
, 把引用地址指向Math
。由于需要在堆中分配空间,堆是所有线程共享的,所以就存在并发问题。jvm分配内存策略:
- 划分内存的方法
- “指针碰撞”(Bump the Pointer)(默认用指针碰撞) ,异常重试
- “空闲列表”(Free List)
- 解决并发问题的方法
- CAS(compare and swap) 虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
- 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)即每个线程在Java堆中预先分配一小块内存。通过
XX:+/- UseTLAB
参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB),XX:TLABSize 指定TLAB大小。
- 初始化。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
- 设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对 象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data) 和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈 希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
dup
这个指令为复制栈顶的值,压如栈顶,也就是复制math
的引用,这里没有发现有什么作用,哪位大佬可以解释一下。invokespecial #3 <top/wuweij/jvm/Math.<init>>
- 执行方法 (执行构造方法)
执行<init>
方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。
astore_1
把栈顶值存如局部变量表 1 位置,然后从操作数栈中移除。
5. 开执行 int a = 2;
- 8
iconst_2
把常量2压入操作数栈中 - 9
istore_2
把操作数栈中的值存入局部变量表 2 的位置, 然后出栈。
6. int b = 7;
- 10
bipush 7
把常量7压入操作数栈中。 - 12
istore_3
把 操作数栈中的值存入局部变量表3的位置。
7. int c = math.plus(a, b);
- 13
aload_1
把局部变量表1的值压入栈中 - 14
iload_2
把局部变量表2的值压入栈中 - 15
iload_3
把局部变量表3的值压入栈中 - 16
invokevirtual #4 <top/wuweij/jvm/Math.plus>
plus 方法
0 iload_1
1 iload_2
2 iadd
3 ireturn
调用对象的实现方法,这个时候通过动态链接来获取该方法,这个时候又会重新生成一个新的栈帧1
里面的结构和栈帧0
一样,栈帧1
执行完以后通过方法出口返回到栈帧0
中,方法执行完成。
- 19
istore 4
把 操作数栈中的值存入局部变量表4中。
剩下的就都差不多了,这里就不在分析了。
总结
通过对JVM的内存模型结合字节码分析了java程序的整体运行流程,总体上看,jvm把内存分为了堆和栈两大块,
栈的特点为:线程安全,不存在锁竞争的问题,即用即回收,所有不需要垃圾回收。
堆的特点为:线程不安全,所有存在锁竞争问题,需要垃圾处理器来回收垃圾,我们平时说的对JVM的调优优化也主要针对堆的调优。