写在前面
上一篇文章我们通过结构图,图解了JVM运行时的内存区域模型。但是苦于没有和代码对照上,所以在理解上有些困难。所以本文结合一段实际的代码和编译后的字节码,再次深度剖析JVM运行时的内存区域。
由于内容较长,我打算分三期来写,预计内容如下:
1.JVM运行时内存区域模型
2.结合代码剖析JVM运行时内存区域
3.JVM内存分配
代码和内存区域对照
class Phone{
String Brand;
int price;
Phone(String a, int b){
this.Brand = a;
this.price = b;
}
}
public class PhoneDemo {
final int test1 = 123;
public static void main(String[] args){
Phone p1 = new Phone("Apple",5000);
Phone p2 = new Phone("Huawei",4000);
System.out.println(p1.price);
System.out.println(p2.price);
}
}
以下我们带来图中内容的详解 ——
【1】本地变量表
如上文所述,本地变量表中不存储对象实例,而是存储对象的引用。
实际对象在new方法创建后存储再堆上并且会分配地址和默认值。
【2】栈帧
方法的执行就是在Java栈中的栈帧的进栈和出栈。
注意栈帧中会存在对于当前方法所在类的运行时常量池的引用,便于查询常量数据。
【3】静态常量池 -> 运行时常量池
先说说静态常量池
通过javap -verbose我们可以查看PhoneDemo.class文件的字节码内容中的常量池部分:
Constant pool:
#1 = Methodref #12.#35 // java/lang/Object."<init>":()V
#2 = Fieldref #11.#36 // j2seDemo/PhoneDemo.test1:I
#3 = Class #37 // j2seDemo/Phone
#4 = String #38 // Apple
#5 = Methodref #3.#39 // j2seDemo/Phone."<init>":(Ljava/lang/String;I)V
#6 = String #40 // Huawei
#7 = Fieldref #41.#42 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Fieldref #3.#43 // j2seDemo/Phone.price:I
#9 = Methodref #44.#45 // java/io/PrintStream.println:(I)V
#10 = Fieldref #11.#46 // j2seDemo/PhoneDemo.test2:I
#11 = Class #47 // j2seDemo/PhoneDemo
#12 = Class #48 // java/lang/Object
#13 = Utf8 test1
#14 = Utf8 I
#15 = Utf8 ConstantValue
#16 = Integer 123
#17 = Utf8 test2
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lj2seDemo/PhoneDemo;
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 args
#28 = Utf8 [Ljava/lang/String;
#29 = Utf8 p1
#30 = Utf8 Lj2seDemo/Phone;
#31 = Utf8 p2
#32 = Utf8 <clinit>
#33 = Utf8 SourceFile
#34 = Utf8 PhoneDemo.java
#35 = NameAndType #18:#19 // "<init>":()V
#36 = NameAndType #13:#14 // test1:I
#37 = Utf8 j2seDemo/Phone
#38 = Utf8 Apple
#39 = NameAndType #18:#49 // "<init>":(Ljava/lang/String;I)V
#40 = Utf8 Huawei
#41 = Class #50 // java/lang/System
#42 = NameAndType #51:#52 // out:Ljava/io/PrintStream;
#43 = NameAndType #53:#14 // price:I
#44 = Class #54 // java/io/PrintStream
#45 = NameAndType #55:#56 // println:(I)V
#46 = NameAndType #17:#14 // test2:I
#47 = Utf8 j2seDemo/PhoneDemo
#48 = Utf8 java/lang/Object
#49 = Utf8 (Ljava/lang/String;I)V
#50 = Utf8 java/lang/System
#51 = Utf8 out
#52 = Utf8 Ljava/io/PrintStream;
#53 = Utf8 price
#54 = Utf8 java/io/PrintStream
#55 = Utf8 println
#56 = Utf8 (I)V
其中主要内容如下:
- 字面量:
- final定义的常量(Integer, Double, Float)
- 字符串(Utf8):
- 包含变量名:如p1,p2,price,brand
- 方法名:如init,main,println
- 字符串常量:如"Apple","Huawei"
- 类名:如Object, PhoneDemo, Phone
- 其他关键字:如this
- 符号引用:对于字面量的引用组合
- 方法名称和返回值类型声明:NameAndType
- 方法与类的关系:Methodref
- 常量与类的关系:Fieldref
- 字符串常量:String
- 类:Class
静态常量池和运行时常量池的区别
静态常量池:类似于class文件的字典
存储的是当class文件被java虚拟机加载进来后存放在方法区的一些字面量和符号引用,其中符号引用使用的是字面量的索引,使用时需要用索引再查询一次字面量。
运行时常量池:程序运行时所使用过的字典中的数据的存储,需要使用时才存入
- 在静态常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用
- 而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。
【4】操作数栈
数据运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈。
还是以代码举例:
public int test(){
int a = 5 + 10; //验证直接相加在编译阶段已合并完结果
int b = a + 3; //探究变量与常量的相加过程
return b;
}
其对应的字节码如下:我已通过注释详解了操作数栈弹栈和压栈的过程。
( int a = 5 + 10; )
0: bipush 15 // 5+10的计算结果压栈
2: istore_1 // 弹栈拿到计算结果后,存入本地变量表
( int b = a + 3; )
3: iload_1 // 读取本地变量表中a的数据,压栈
4: iconst_3 // 数据3压栈准备参加计算
5: iadd // 弹栈拿到3和a的值,计算后将结果压栈
6: istore_2 // 弹栈拿到计算结果,存入本地变量表
(return b;)
7: iload_2 // 读取本地变量表
8: ireturn // 返回
参考
1.Java内存管理机制剖析
2.命令查看java的class字节码文件
3.刘意Java教程-三个对象的内存图
4.Java虚拟机站之操作数栈