JVM内存模型
java内存模型.png程序计数器
- 也称为PC寄存器,每个线程都有一个程序计数器,线程私有
- 实际就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将执行的指令代码),由执行引擎读取下一条指令
- 非常小的内存空间,几乎可以忽略不计.是唯一一个在JVM规范中没有OOM的区域
本地方法栈
- 区别去java虚拟机的是,java虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到native方法服务
- 也会有StackOverflowError和OutOfMemoryError异常
方法区
- 方法区是所有线程共享的
- 所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。
- 所有定义的方法信息都保存在该区域,此区属于共享区间
- 静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中
- 实例变量存在堆中,和方法区无关
虚拟机栈
- 线程私有,声明周期和线程一致
- 每个方法在执行的时候都会创建一个栈帧
- 每个栈帧都有局部变量表,操作数栈,动态链接,方法出口
局部变量表
局部变量表是一组变量值存储空间,用于存储方法参数和方法内部定义的局部变量
操作数栈
- 也称为操作栈,是一个后进先出的栈
- 默认方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈写入和提取内容,也就是出栈/入栈操作
- 举例:整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了int类型的数值,当执行这个命令的时候,会将两个int值出栈并相加,然后将结果入栈
public class Test {
public static void main(String[] args) {
int a=0;
int b=10;
int c=a+b;
}
}
javap -v Test.class查看字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_0 //将0压入操作数栈
1: istore_1 //将操作数栈中的0放到局部变量表下标1的位置
2: bipush 10 //将10压入操作数栈
4: istore_2 //将操作数栈栈顶元素10放到局部变量下标2的位置
5: iload_1 //将局部变量表下标1位置的元素重新放到操作数栈栈顶
6: iload_2 //将局部变量表下标2位置的元素重新放到操作数栈栈顶
7: iadd //弹出操作数栈顶的两个元素进行相加并放到栈顶
8: istore_3 //将局部变量表栈顶的元素放到局部变量下标3的位置
9: return
LineNumberTable:
line 6: 0
line 7: 2
line 8: 5
line 9: 9
LocalVariableTable://局部变量表
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
2 8 1 a I
5 5 2 b I
9 1 3 c I
-
在概念模型中,两个栈帧作为虚拟机的元素,是完全相互独立的。但在大多数虚拟机实现里都会做一些处理,令两个栈帧出现一部分重叠
image.png
动态链接
- 每一个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态链接
- 这个引用是一个符号引用,不是方法实际运行的入口地址,需要动态找到具体方法的入口
方法返回地址
- 正常完成出口:方法正确执行,执行引擎遇到方法返回的指令,回到上层的方法调用者
- 异常完成出口:方法执行过程中发生了异常,并且没有处理异常,这样就不会给上层调用者产生任何返回值
- 方法正常退出,将会放回程序结束并将值给上层方法,经过调整之后以指向方法调用指令后面的一条指令,继续执行上层方法
堆
- 线程共享,主要存放对象实例和数组。
- 从内存回收的角度来看,由于现在收集器基本上都采用分代收集算法,所以对空间还可以细分为:新生代(年轻代),老年代(年老代).再 细致一点,可以分为 Eden 空间,From Survivor 空间, To Survivor 空间.
- GC主要就是管理堆空间,对分代GC来说,堆也是分代的(只是一种思想)
- 堆的优点:运行期动态分配内存大小,自动进行垃圾回收
- 堆的缺点:效率相对较慢
堆的结构
image.png- 新生代用来存放新分配的对象;新生代经过垃圾回收,没有回收掉的对象,被复制到老年代
- 老年代存储对象比新生代存储对象的年龄大得多
- 老年代存储一些大对象
- 整个堆大小=新生代+老年代
- 新生代=Eden+存活区
对象的内存布局
- (以HotSpot虚拟机为例来说明),分为:对象头、实例数据和对齐填充
- 对象头包含两个部分
- Mark Word:存储对象自身的运行数据,如:HashCode、GC分代年龄、锁状态标志等
- 类型指针:对象指向它的类元数据的指针
- 实例数据
- 真正存放对象实例数据的地方
- 对齐填充
- 这部分不一定存在,仅仅是占位符。
栈、堆方法区交互关系
image.png字节码
栈帧概述
- 栈帧是用于支持JVM进行方法调用和方法执行的数据结构
- 栈帧随着方法调用而创建,随着方法结束而销毁
- 栈帧里面存储了方法的局部变量表,操作数栈,动态链接,方法返回地址等信息
局部变量表
- 用来存放方法参数和方法内部定义的局部变量的存储空间
- 以变量槽slot为单位,目前一个slot存放32位以内的数据类型
- 对64位的数据占2个slot
- 对于实例方法,第0位slot存放的是this,然后从1到n,依次分配给参数列表(static没有this)
- 然后根据方法体内部定义的变量顺序和作用域分配slot
public class Test {
public int add(int a,int b){
int c=a+b;
return a+b+c;
}
public static void main(String[] args) {
new Test().add(1,3);
}
}
字节码
image.png
将add方法改成static之后查看字节码
image.png
- slot是复用的,以节省栈帧的空间,这种设计可能会影响到系统的垃圾收集行为
public static void main(String[] args) {
{
byte[] bs = new byte[1024 * 1024 * 3];
}
int a=10;
}
image.png
操作数栈
- 用来存放方法运行期间,各个指令操作的数据
- 操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配
public class Test {
public int add(int a, int b) {
int c = a + b;
return a + b + c;
}
public static void main(String[] args) {
new Test().add(1, 2);
}
}
只看add和main方法的字节码
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=3
0: iload_1 //局部变量表下标1的值加入操作数栈栈顶
1: iload_2//局部变量表下标2的值加入操作数栈栈顶
2: iadd//弹出操作数栈的2个元素相加并加值压入操作数栈顶
3: istore_3//将返回的值赋予局部变量表下标3的位置
4: iload_1//局部变量表下标1的值加入操作数栈栈顶
5: iload_2//局部变量表下标2的值加入操作数栈栈顶
6: iadd//弹出操作数栈的2个元素相加并加值压入操作数栈顶
7: iload_3//局部变量表下标3的值加入操作数栈栈顶
8: iadd//弹出操作数栈的2个元素相加并加值压入操作数栈顶
9: ireturn//结果返回
LineNumberTable:
line 5: 0
line 6: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/peakmain/jvm/Test;
0 10 1 a I
0 10 2 b I
4 6 3 c I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: new #2 //新建一个 class com/peakmain/jvm/Test
3: dup
4: invokespecial #3 // 调用初始化
7: iconst_1//将常量1压入操作数栈
8: iconst_2//将常量2压入操作数栈
9: invokevirtual #4 // 调用方法的add方法
12: pop//弹出操作数栈栈顶元素
13: return
LineNumberTable:
line 10: 0
line 11: 13
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 args [Ljava/lang/String;
分析:
main函数
-
iconst_1:将常量1压入操作数栈
image.png -
iconst_2:将常量2压入操作数栈
image.png
add方法:
- iload_1 :局部变量表下标1的值加入操作数栈栈顶
-
iload_2:局部变量表下标2的值加入操作数栈栈顶
image.png -
iadd:弹出操作数栈的2个元素相加并加值压入操作数栈顶
image.png -
istore_3:将返回的值赋予局部变量表下标3的位置
image.png -
iload_1:局部变量表下标1的值加入操作数栈栈顶
image.png -
iload_2:局部变量表下标2的值加入操作数栈栈顶
image.png -
iadd:弹出操作数栈的2个元素相加并加值压入操作数栈顶
image.png -
iload_3:局部变量表下标3的值加入操作数栈栈顶
image.png -
iadd//弹出操作数栈的2个元素相加并加值压入操作数栈顶
image.png - ireturn:结果返回
分派
- 分派:又分为静态分派和动态分派
- 1:静态分派:所有依赖静态类型来定位方法执行版本的分派方式,比如:重载方法
- 2:动态分派:根据运行期的实际类型来定位方法执行版本的分派方式,比如:覆盖方法
java中堆栈的应用
1.栈和堆都是用来存放数据的地方,与c++不同的是,java自动管理堆和栈,程序员不能直接地设置堆或栈
2.栈的优势:存取速度快,缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
堆的优势:可以动态地分配内存,java的垃圾回收器会自动收走这些不再使用的数据。缺点:由于要运行时动态地分配内存,存取速度较慢
3.java数据类型有两种,一种是基本数据类型还有一种是包装性数据类型
public class Test {
public static void main(String[] args) {
int a=10;
Integer b=10;
Integer c=Integer.valueOf(10);
System.out.println(a==b);
}
}
查看字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: bipush 10 //10压入操作数栈
2: istore_1 //放到局部变量表1的位置
3: bipush 10 //10压入操作数栈
5: invokestatic #2 // 实际调用的Integer.valueof方法 Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
8: astore_2 //放到局部变量表2的位置
9: bipush 10
11: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 实际和字节码5一模一样
14: astore_3
//调用system.out.println
15: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_1 //局部变量表下标1的位置重新压入操作数栈
19: aload_2 //局部变量表下标2的位置重新压入操作数栈
20: invokevirtual #4 //调用的是Integer.intValue Method java/lang/Integer.intValue:()I
23: if_icmpne 30 //如果两个值不相等则进行跳转,跳转到字节码30的位置
26: iconst_1 //相等则1压入操作数栈
27: goto 31 //跳转到字节码31的位置
30: iconst_0 //不相等0压入操作数栈
31: invokevirtual #5 // Method java/io/PrintStream.println:(Z)V
34: return
LineNumberTable:
line 7: 0
line 8: 3
line 9: 9
line 10: 15
line 11: 34
LocalVariableTable:
Start Length Slot Name Signature
0 35 0 args [Ljava/lang/String;
3 32 1 a I
9 26 2 b Ljava/lang/Integer;
15 20 3 c Ljava/lang/Integer;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 30
locals = [ class "[Ljava/lang/String;", int, class java/lang/Integer, class java/lang/Integer ]
stack = [ class java/io/PrintStream ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class "[Ljava/lang/String;", int, class java/lang/Integer, class java/lang/Integer ]
stack = [ class java/io/PrintStream, int ]
- 基本类型(primitive types), 共有 8 种,即 int, short, long, byte, float, double, boolean, char(注意, 并没有 string 的基本类型)。都存在于栈中
- 另一种是包装类数据,如 Integer, String, Double 等将相应的基本数据类型包装起来的类。这些类数据全部 存在于堆中,Java 用 new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占 用更多的时间
- String 是一个特殊的包装类数据
public class Test {
public static void main(String[] args) {
String str = "abc";
}
}
我们看下上面代码的字节码
image.png
- ldc实际是将常量池的字符串变量abc压入操作数栈
- astore将操作数栈栈顶放到局部变量表1的位置
这里我们就会发现String str="abc"并没有new()来创建实例,而是创建一个str对象指向常量池abc对象
所以我们很容易知道下面这行代码返回的是true
public class Test {
public static void main(String[] args) {
String str = "abc";
String str1 = "abc";
System.out.println(str==str1);
}
}
equal和==的区别
public class Test {
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1 == str2);
System.out.println(str1 .equals(str2));
}
}
查看字节码
image.png
switch和if-else哪个性能高
switch
public class Test {
public static void main(String args[]) {
char grade = 'C';
switch (grade) {
case 'A':
System.out.println("优秀");
break;
case 'B':
case 'C':
System.out.println("良好");
break;
case 'D':
System.out.println("及格");
break;
case 'F':
System.out.println("你需要再努力努力");
break;
default:
System.out.println("未知等级");
}
}
}
转成对应的字节码
image.png
我们会发现一共9条字节码指令
再来看下if-else
public class Test {
public static void main(String args[]) {
char grade = 'C';
if(grade=='A'){
System.out.println("优秀");
}else if(grade=='B'||grade=='C'){
System.out.println("良好");
}else if(grade=='D'){
System.out.println("及格");
}else if(grade=='F'){
System.out.println("你需要再努力努力");
}else{
System.out.println("未知等级");
}
}
}
image.png
我们发现一共需要13条字节码指令
a++和++a的区别
a++
public class Test {
public static void main(String args[]) {
int a=1;
for (int j=0;j<10;j++){
a=a++;
System.out.println(a);
}
System.out.println(a);
}
}
字节码分析
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_1 //1压入操作数栈
1: istore_1 //放到局部变量表1的位置
2: iconst_0 //常量0压入操作数栈中
3: istore_2//放到局部变量表2的位置
4: iload_2//加载局部变量表2的值放到操作数栈
5: bipush 10 //10压入操作数栈
7: if_icmpge 21 //比较两个操作数栈的值 当大于等于0的时候进行跳转
10: iload_1 //局部变量表1的数加载到操作数栈中(这里就是1)
11: iinc 1, 1 //局部变量表1位置数据+1,注意只是局部变量表+1,操作数栈还是1
14: istore_1 //操作数栈的值放到局部变量表1的位置
15: iinc 2, 1 //局部变量表2(也就是j)进行+1
18: goto 4 //回到字节码4的位置
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_1
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
LineNumberTable:
line 5: 0
line 6: 2
line 7: 10
line 6: 15
line 9: 21
line 10: 28
LocalVariableTable:
Start Length Slot Name Signature
4 17 2 j I
0 29 0 args [Ljava/lang/String;
2 27 1 a I
++a
public class Test {
public static void main(String args[]) {
int a=1;
for (int j=0;j<10;j++){
a=++a;
}
System.out.println(a);
}
}
字节码分析
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_1 //1压入操作数栈
1: istore_1 //放到局部变量表1的位置
2: iconst_0 //常量0压入操作数栈中
3: istore_2//放到局部变量表2的位置
4: iload_2//加载局部变量表2的值放到操作数栈
5: bipush 10 //10压入操作数栈
7: if_icmpge 21 //比较两个操作数栈的值 当大于等于0的时候进行跳转
10: iinc 1, 1 //局部变量表1位置数据+1,注意只是局部变量表+1,操作数栈还是1
13: iload_1 //局部变量表1位置的值也就是此时的2压入操作数栈
14: istore_1 //再将操作数栈的值2设置给局部变量表下标为1位置
15: iinc 2, 1 //j+1
18: goto 4
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_1
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
LineNumberTable:
line 5: 0
line 6: 2
line 7: 10
line 6: 15
line 9: 21
line 10: 28
LocalVariableTable:
Start Length Slot Name Signature
4 17 2 j I
0 29 0 args [Ljava/lang/String;
2 27 1 a I
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 4
locals = [ int, int ]
frame_type = 250 /* chop */
offset_delta = 16
网友评论