书本简介
书名:《深入理解java虚拟机 JVM高级特性与最佳实践》
作者:周志明
版本:第二版
pdf下载链接:csdn下载该电子书
运行时数据区域
关键词:运行时
即在从java虚拟机(JVM)启动到其停止的一段时间内,JVM用来存放必要数据(类信息,对象,常量等等)
画了这么一副图:
程序计数器(Program Counter Register)
与相应线程关联的一个较小的内存空间,用作存放程序下一条指定的位置。属于线程私有的空间,每个线程都需要一个独立的程序计数器。
java虚拟机栈(Java Virtual Machine Stack)
虚拟机栈描述的是 : java方法执行的内存模型(书中原话),我对这句话的理解是:
虚拟机栈,故名思意,它是一个栈结构(先进后出),它里面的元素是一个个的栈桢(Stack Frame),栈桢中存放的是一个方法中的运行必要信息(局部变量表,操作数栈,动态链接,方法出口等)。
想想方法的执行过程:在一个线程中,方法的执行是从上到下执行每一行代码(在虚拟机中是字节码),当遇到一个方法时,将会进入这个方法,执行这个方法的代码,当方法结束时,回到本方法的调用方法的调用点的下一行代码(即程序计数器指向这行代码)。看下面的代码:
public class Test {
public static void main(){
System.out.println("main:Hello World"); // 1
print(); // 2
// 3
}
public static void print(){
System.out.println("print:Hello World"); //4
}
}
JVM加载完Test类后,执行main方法。从方法开头向下执行 , 从 1 到 2 ,2 是一个print()方法,进入这个方法执行4,这个方法执行完之后返回到 3 。
在java虚拟机栈中会有两个栈桢进出,分别代表main方法和print方法。
上图,表示了上面示例代码虚拟机栈中栈桢的进出情况。橙色代表栈顶栈桢(正在执行的方法)。
方法区(Method Area)
方法区用于存放已被虚拟机加载的类的类信息,常量,静态变量(类变量)等数据。
运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分,是存放各种字面值常量和符号引用的内存区域。
Java堆
主要用于存放对象实例和数组对象的内存区域。在虚拟机启动时创建(但是书中没有提到,方法区是不是也是这个时候启动,而讲了方法区被实现为“永久代”)
对象的创建
public class Student {
private String name;
private char gender;
public Student(String name,char gender){
this.name = name;
this.gender = gender;
}
}
public class Main{
public static void main(String[] args){
Student student = new Student("Paul",'m');
}
}
以上代码创建了一个Student类的实例。但在java虚拟机的内部是怎样的一个过程呢?
使用一个命令将Main中的main方法的字节码(可以看懂的形式,并不是01代码串)得到。
javap -verbose Main
Code:
stack=4, locals=2, args_size=1
0: new #2 // class Student
3: dup
4: ldc #3 // String Paul
6: bipush 109
8: invokespecial #4 // Method Student."<init>":(Ljava/lang/String;C)V
11: astore_1
12: return
抓主要矛盾,创建对象实例的关键是new指令(这里的new是java字节码中的new)
- 检查其参数#2(代表常量池中的2号常量,这里是一个类常量,代表Student)在常量池中是否可以找到对应的常量
- 检查该对应类是否已经被加载,解析,初始化过,如果没有则需要先进行这些操作
- 按照对象的大小(在类加载完成后可以确定)从堆中划分出一块内存空间
- 设置对象的一些必要信息(如对象是哪个类的实例,如何找到类的元信息,对象哈希码等信息),这些信息保存在对象的对象头(Object Header)中
- 将新开辟的这块空间(也就是这个对象)的首地址压入操作数栈的栈顶
下面看一看详细每一个字节码都干了什么
字节码 | 参数 | 隐含参数 | 目标 | 备注 |
dup | 操作数栈顶的一个元素 | 操作数栈顶 | 将操作数栈顶的元素复制一份再将其压入栈顶(不知道是什么作用) | |
ldc | #3 常量池中的第三个常量 | 操作数栈顶 | 将#3这个常量的值压入栈顶,即常量Paul | |
bipush | 109 代表字符'm' | 操作数栈顶 | 将109压入栈顶 | |
invokespecial | #4 常量池中的第四个常量 | 栈顶的三个元素(这里是三个,对象地址,第一个参数Paul,第二个'm') | 操作数栈顶 | 执行该对象的<init>方法进行初始化,把初始化后的对象地址压入栈中 |
astore_1 | 栈顶元素,即已经初始化的对象的地址 | 局部变量表 | 将栈顶元素保存到本地变量表的Slot 1(理解为第一个空间)中 |
画了个图来帮助理解new指令
图中3 , 4 类加载如果方法区中已经有Student类信息的话会跳过。
对象的内存布局
对象在内存中的存储布局可以分为三个部分:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
对象的访问定位