JVM自动内存管理之一
2019-01-15 本文已影响66人
AlanKim
首先看个大图:
874710-20161206164443851-339965653.png程序计数器:
- 一块很小的内存空间,可以理解为当前线程所执行字节码的行号指针。
- 如果执行的是java方法,则计数器记录的是正在执行的字节码指令的内存地址;
- 如果是执行native方法,那么此值为空。
- 程序计数器是唯一一个在JVM规范中,没有规定任何OutOfMemoryError(即内存溢出)情况的区域。
java虚拟机栈 — 服务于java字节码方法
- 线程私有
- 后进先出栈,LIFO
- 存储栈帧,支持java方法的调用、执行和退出
- 可能会出现OutOfMemoryError异常和StackOverFlowError异常
本地方法栈 — 服务于本地native方法
- 线程私有
- LIFO
- 支持native方法的调用、执行和退出
- 可能会出现OutOfMemoryError异常和StackOverFlowError异常
- hotspot 把java虚拟机栈和本地方法栈这两个的实现合二为一了,JVM规范并没有要求虚拟机怎么实现。
栈帧:
-
是java虚拟机栈中存储的内容
-
用于存储数据和部分过程结果
-
以及处理动态链接、方法返回值和异常分派
-
hotspot虚拟机实现中,-Xss128K 用于控制分配给线程的栈空间,其实也就是java虚拟机栈+本地方法栈,-XSS跟堆空间没关系。
-
一个完整的栈帧包括:
-
局部变量表
- 变量值的存储空间,方法参数、局部变量
-
- 长度在编译期就确定了
- 由若干个slot组成,每个slot都应该能存放boolean byte char short float 以及 reference returnAddress数据
- 两个slot可以存储long 和 double数据,64位。类似JMM中long double的实现,不过由于是线程私有,所以不会有线程安全信息。
- reference:对象实例的引用,获取实例内存地址,以及方法区中对应的实例类型class信息
- returnAddress,基本上不用了。
- 用于方法之间存放参数,以及在方法执行过程中,存储技术数据类型的值+对象的引用。
- 如果是一个成员方法,那么在第0个slot,存放的是当前方法所属对象的reference,用this可以访问到。
- slot可重用
-
操作数栈
- 后进先出,LIFO(Last In,First Out)
- 代替cpu的寄存器(jvm本身没有寄存器),是指令的工作区,结果会暂存到操作数栈,然后再出栈存入局部变量表。可以认为是计算时临时数据的存储区域。
- 由若干个Entry组成
- 单个Entry可以存储一个jvm中定义的任意数据类型的值,包括long和double
- 但是long和double类型的Entry深度为2,其他类型深度为1
- 执行过程中,用于存储计算参数和计算结果;方法调用时,用来准备调用方法的参数,以及接收方法返回结果。
-
动态链接信息
-
方法正常完成信息—方法返回的结果地址
-
方法异常完成信息
-
在编译期就已经确定需要多大的局部变量表,多长的栈深度,写入到class文件中。
栈实战
基本源码如下:
public class Test{
public int calc(){
int a = 100;
int b = 200;
int c = 300;
return (a+b) * c;
}
}
编译并javap:
javac Test.java
javap -verbose Test
结果如下:
Classfile /Users/kinomousakai/middleplatform/Test.class
Last modified May 4, 2018; size 262 bytes
MD5 checksum 469e4b9f043da5724726120da5102b22
Compiled from "Test.java"
public class Test
minor version: 0
major version: 54
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // Test
super_class: #3 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // Test
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 calc
#9 = Utf8 ()I
#10 = Utf8 SourceFile
#11 = Utf8 Test.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 Test
#14 = Utf8 java/lang/Object
{
public Test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public int calc();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 100
2: istore_1
3: sipush 200
6: istore_2
7: sipush 300
10: istore_3
11: iload_1
12: iload_2
13: iadd
14: iload_3
15: imul
16: ireturn
LineNumberTable:
line 4: 0
line 5: 3
line 6: 7
line 8: 11
}
SourceFile: "Test.java"
关键看stack相关,下面详细说明下:
初始阶段-第一步:
WechatIMG249.png此时:
- 程序计数器为0,表示执行第0行
- 局部变量表的第0个写入this
- 操作栈会通过指令bipush,将后面的100 写入操作栈顶
第二步
WechatIMG250.png此时:
- 程序计数器变为2,因为第0行有两个数据,bipush和100,占用了两个偏移量。
- istore_1 将操作数栈顶的数据出栈,并存入局部变量表第一个位置。
- 偏移量3-10 执行0-2 一样的动作,执行完10之后,内存区域如下:
忽略操作数栈顶的100,以及程序计数器的11,这是第11个指令的作用。
第三步
接上一步的图,第11步做了如下操作:
- 程序计数器变为11,表示当前执行第11位地址偏移量对应的指令
- iload_1表示将 局部变量表Slot=1的数据,存入到操作数栈顶中
第四步:
WechatIMG252.png此时:
- 程序计数器变为12
- iload_2 表示将局部变量表Slot=2的数据存入操作数栈顶
- 此时操作数栈深度变为2,其中数据为,栈顶entry=200,entry栈顶+1 = 100
第五步
WechatIMG253.png此时:
- 程序计数器变为13
- iadd表示将操作数栈中距离栈顶最近的两个元素,出栈,相加,并将结果写入操作数栈顶,此时操作数栈深度变为1
第六步:
WechatIMG254.png此时:
- 程序计数器变为14
- 将局部变量表中下标为3的元素取出,并压入操作数栈顶,此时操作数栈深度重新变成2
第七步
WechatIMG255.png此时:
- 操作数栈变为15
- imul为整数乘法指令,把操作数栈顶最近的两个元素取出,相乘,并重新压入操作数栈顶
- 执行第16位偏移量,程序计数器变为16
- ireturn会将操作栈顶的元素出栈,并将其作为返回结果,整个逻辑结束。此时操作栈深度为0