Java虚拟机原理解析系列
目录
1. 整体架构
- 既然叫运行时数据区,那么肯定就是JVM定义的程序在运行期间需要使用的内存区,来支持Java程序的执行。
- 这种数据区存在多种,其中一些会随着虚拟机启停而创建和销毁;而另外一些则是与线程一一对应,会随着线程的开始和结束而创建和销毁。
运行时数据区架构图:
其中黄色区域为线程私有的,绿色为线程共享的。
2. 程序计数器
线程私有的,每个线程都有自己的程序计数器。
任意时刻,线程只能执行一个方法的代码,这个正在执行的方法就是当前方法。
程序计数器中存储的内容,和当前方法的类型有关:
- 如果当前方法是非native方法,那么程序计数器存储的是正在执行的字节码指令的地址(可以简单理解成,线程当前执行到哪一行代码了)。
- 如果当前方法是navtive1方法,那么程序计数器中存储的就是undefined。
再进一步思考一下:
-
为什么要存储线程当前执行到哪行代码了呢?
很好理解,因为多线程环境下,线程不是一直持续占用CPU执行的,而是根据时间片的轮换,时而执行时而挂起。那么当再次执行的时候,需要知道上次执行到哪行代码了,所以必要有有个地方去记录。 -
为什么是native方法的时候,是undefined呢?
因为native方法内部的代码,是使用其他语言编写的,其内部代码的执行与JVM是无关的,因此记录执行到哪行是无意义的。
3. Java虚拟机栈
一些概念
-
当前方法
对于单独的与一个线程来讲,任意时刻只能执行一个方法,不可能多个方法一起执行,也就是说方法的执行是串行的。那么这个正在执行的方法就叫做当前方法。 -
当前类
当前方法所属的类就是当前类。 -
栈
后入先出的一个数据结构。Java虚拟机栈就是这种栈结构,通过它就模拟了一个线程调用多个方法的过程模型。是由栈帧组成的。 -
栈帧
栈中的每一个元素就叫做栈帧。Java虚拟机栈中的栈帧就代表一次方法的执行,每个方法调用在Java虚拟机栈中都有一个栈帧与之向对应。 -
当前栈帧
Java虚拟机栈顶的栈帧,当前栈帧对应的方法就是当前方法。
Java虚拟机栈运行的机制
- Java虚拟机栈是线程私有的,线程创建的同时就会创建虚拟机栈。
- 当线程调用某一个方法的时候,也会对应创建一个栈帧与之对应。
- 大概的过程:线程执行的当前方法为A,会在Java虚拟机栈中有一个当前栈帧A与之对应。当当前方法A执行过程中,又调用了另一个方法B,那么栈帧A就会执行压栈操作,同时方法B,对应的栈帧B就会成为当前栈帧。当方法B执行完成后,对应的栈帧B就会被丢弃。程序返回方法A继续执行,同时对应的栈帧A又成为了当前栈帧。
代码执行到①处:
创建createUser
方法对应的栈帧A,并进行压栈操作,当前栈帧指向栈帧A。
代码执行到②处:
创建sendEmail
方法对应的栈帧B,栈帧A下移。栈帧B入栈,当前栈帧指向栈帧B。
代码执行到③处:
方法sendEmail
方法执行完成,对应的栈帧B执行出栈操作,并抛弃。当前栈帧指向栈帧A。
3.1 栈帧
栈帧随着方法的调用而创建,方法结束而销毁。
栈帧中存储的内容包括,方法的局部变量表、操作数栈和指向方法所属类的运行时常量池的引用。
用图来表达更直接一些,如下图:
3.1.1 局部变量表
- 局部变量表使用索引进行定位访问,局部变量表第一个元素的索引值为0
- 一个局部变量表中的元素可以保存一个基本类型的数据(long和double除外,它们需要两个局部变量来保存)
示例:
public String getUserInfo(){
int age = 18;
boolean isMan = false;
long changdu = 209;
double kuandu = 1928;
return null;
}
当线程执行到上述方法的时候,Java虚拟机栈中的局部变量表情况:
上图是对局部变量表存储结构的一个大概描述,实际存储的值应该是二进制方式表达的,大家能知道这个结构就可以了。有以下几点需要知道:
1.索引是从0开始的,按照局部变量在代码中声明的先后顺序,存储到这个变量表中。
2.索引是0的元素值存储的是,指向调用该方法的实例对象的引用。
3.可以看到long和double数据类型占用两个表格位。
3.1.2 操作数栈
1.每个Java虚拟机栈的栈帧都包含一个操作数栈,操作数栈是一个后进先出的结构。
2.在栈帧刚被创建的时候,操作数栈是空的。
3.JVM提供了指令向操作数栈存入和获取值。存入的值得来源就是局部变量表中的值。
4.在JVM进行计算的时候,从操作数栈中获取值,然后进行计算,把计算的结果重新放入操作数栈中。比如,要进行两个int数据类型进行+的操作,那么先会从操作数栈中,获取变量的值,然后进行计算,然后把结果再放入操作数栈中。
我们大概模拟一下操作数栈的运作过程:
1.代码中有如下方法:
public void sumAge(){
int age1 = 20;
int age2 = 30;
int ageSum = age1+ age2;
}
2.当程序调用该方法,Java虚拟机栈的栈帧被创建,同时局部变量表为空。
3.当程序执行到int ageSum = age1+ age2;
的时候,JVM执行字节码指令将涉及到的变量的值拷贝到操作数栈的,栈顶的栈帧
4.当执行完int ageSum = age1+ age2
后
以上是大概的模拟,理解意思就可以了。
3.1.3 动态链接
先明确两个概念:
- 符号引用:在Class文件中,描述一个方法调用了其他方法,或访问其成员变量,是通过符号引用来标示的。说白了就是用符号来表述所引用的目标对象,类似于“com.wt.study.User”这种描述,引用的目标并不一定已经加载到内存中。
- 直接引用:可以是直接指向目标对象的指针,偏移量。也就是是直接指向目标对象在JVM内存中的地址。在类加载的“解析阶段”,会把符号引用转换为直接引用。
1.每个JAVA虚拟机的栈帧,都有一个指向运行时常量池的应用来支持当前方法的代码实现动态链接。
2.动态链接的作用就是把符号引用转换为直接应用的。
3.2 Java堆
1.是供各个线程共享的运行时内存区域。
2.在虚拟机启动的时候就被创建。内部存储了能够被GC管理的对象(注意,这里不是对象的引用,而是真正为对象开辟的内存空间)
3.3 方法区
1.也是线程共享的。
2.它内部存储了类的结构信息,比如运行时常量池、字段和方法数据,构造函数及普通方法的字节码内容。
3.方法区在虚拟机启动的时候被创建。
3.3.1 运行时常量池
1.是每一个类或接口的常量池的运行时的表达形式。
举个例子,比如有如下的类:
public class Test {
public static final String URL = "www.baidu.com";
}
我们经过编译后,查看Class文件的内部信息:
上图中,我们可以看到:
1.major version
就代表了,我们编译该Java文件的时候用的JDK版本。
2.Constant pool
就代表了类的常量池,我们在代码中声明的常量 URL
就在这个常量池中。#7 代表了这个URL
这个引用类型,后面标注了,它指向#17,而#17就是代表了 www.baidu.com
这个字面量。除了这些还有如,类的名称等常量信息。
3.当类被加载到JVM中,运行的时候,类自身的常量池就会进入运行时常量池。
3.4 本地方法栈
1.在Java对方法的定义中,有一种方法是native方法,它自身的实现不是使用java语言的,来作为对JVM虚拟机的一个扩展。
2.本地方法栈就是用于 支持native方法执行的。
关于native方法,在Java并发问题域中的"Unsafe"类的底层就是使用很多native方法来实现的。 ↩︎