Java虚拟机(JVM)系列三
运行时数据区
一.运行时数据区整体架构
运行时数据区.png- JVM定义了若干种程序在运行期间会使用到运行时数据区,其中有一些会随着JVM启动而创建,退出而销毁(进程),另一些则与线程一一对应,这些与线程对应的数据区会随着线程的开始和结束而创建和销毁(线程)
- 方法区和堆是进程级别的,属于被共享的
- 栈、本地方法栈、程序计数器是每个线程独有一份
- 每个JVM只有一个运行时实例,即Runtime实例
- 在HotSpot虚拟机中,每个线程与操作系统的本地线程直接映射
二.程序计数器(也叫PC寄存器 不存在GC、OOM)
1.作用
- PC寄存器是用来存储指向下一条指令的地址,由执行引擎读取指令并执行
2.简介
- PC寄存器是一块很小的空间,几乎可以忽略不计,也是运行速度最快的存储区域
- 在JVM规范中,每个线程都有自己的的一个程序计数器
- 任何时间一个线程都只有一个方法在执行,即当前方法
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
-
它是唯一一个在JVM规范中没有规定任何OutOfMemoryError的情况的区域
PC寄存器使用举例.png
3.面试时的问题
- 使用PC寄存器存储字节码指令地址有什么用呢(为什么使用PC寄存器记录当前线程的执行地址呢)?
- 1)因为CPU需要不停的切换各个线程,切换回来后,需要知道该执行哪个指令了
- 2)JVM解释器需要通过改变PC寄存器的值来获取下一条应该执行什么样的字节码指令
- PC寄存器为什么被设定为私有的
为了可以准确记录各个线程正在执行的当前字节码指令地址,确保各线程间独立计算,不互相干扰
4.额外
- 并行:一个时刻同时执行,并行的关键是同时做很多事情
- 并发:一个时间段内交替执行,并发的关键是同时管理很多事情
三.栈(虚拟机栈 存在OOM,不存在GC)
1.简介
- Java虚拟机栈在每个线程创建的时候被创建,其内部是一个个的栈帧,对应一次次的Java方法调用
- 主管Java的运行,它保存局部变量(8种基本数据类型、对象的阴影)、部分结果,并参与方法的调用和返回
2.栈的特点
- 栈是一种快速有效的分配存储结构,速度仅次于PC寄存器
- 只有进栈和出栈的操作,具有先进后出的特点
3.栈与堆
栈解决的是数据运行的问题(也存储局部变量,中间结果),堆解决的是数据存储问题(主要存储的是对象)
4.面试中遇到的问题
- 开发中遇到的异常
栈中可能出现的异常(StackOverflowError:自己调用自己 OutOfMemoryError)
5.设置栈参数
- 设置栈大小( -Xss256k ----设置栈大小为256k)
6.栈的存储单位
- 栈的存储单位是栈帧,每个方法对应一个栈帧(Stack Frame)
- 三个名词:当前栈帧 当前方法 当前类
- 执行引擎运行的所有字节码指令只对当前栈帧进行操作
7.栈桢
每个栈帧中存储着
-
局部变量表(LC Local Variable)
-
操作数栈(Operant Stack 或表达式栈)
-
动态链接(Dynamic Linking 或指向运行时常量池的方法引用)
-
返回地址(Return Address 或方法正常退出或者异常退出的定义)
-
一些附加信息
7.1局部变量表(Local Variable)
- 局部变量表也被称为是局部变量数组或本地变量表
- 定义为一个数字数组,主要存储的是方法参数以及在方法内声明的局部变量(八大基本数据类型,对象的 引用以及返回地址类型)
- 局部变量表是创建在线程的栈上面的,是线程的私有数据,因此不存在数据安全问题
- 局部变量表的大小(即数组长度)是在编译的时候就定下来的,并且不可以改变
- 局部变量表中的变量只在当前方法内有效
- 局部变量表的基本存储单元是Slot(变量槽),32位以内的数据类型占用一个Slot(byte、short、int、float、char在存储前被转换为int、boolean也被转换为int、以及返回地址类型),64位的类型(long和double)占据两个Slot
- JVM会为局部变量表中的每一个Slot分配一个访问索引,以便访问到局部变量表中指定的局部变量值
- 当一个实例方法被调用的时候,它的方法参数以及内部的局部变量都会被按照顺序复制到局部变量表中的Slot上
- 如果当前栈帧是由构造方法或者实例方法(即非静态方法)创建的,那么该对象的引用this将会被存储到index为0的slot处(所以当我们在静态方法中使用this时会报错)
- 栈帧中局部变量表中的Slot(槽位)是可以重复利用,如果一个局部变量过了作用域,那么在后面声明的局部变量很可能会复用过期的局部变量的槽位,从而达到节省资源的目的
- 局部变量中的变量也是重要的垃圾回收根节点,只要被局部变量中直接或间接引用的对象都不会被回收
7.2 操作数栈(Operand Stack)
栈可以使用数组或者链表实现,只允许进栈和出栈操作
-
每一个独立的栈帧中除了包括局部变量表,还包含一个先进后出的操作数栈
-
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或取出数据,即入栈和出栈
-
操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中变量的临时存储空间
-
每一个操作数栈,都有一个确定的栈深,在编译时就确定(max_stack的值)
-
操作数栈数据的访问是通过入栈和出栈实现,而不是通过索引进行访问,这与指定局部变量值的访问不同
-
被调用的方法如果有返回值,其返回值会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令
-
我们所说的Java虚拟机的解释引擎是基于栈的执行引擎,这个栈就是值操作数栈
-
栈顶缓存技术(为了解决数据频繁进栈出栈,即内存对数据频繁的读/写操作进而影响速度,提出的此技术,它的核心思想是将栈顶元素全部缓存在物理CPU的寄存器中)
7.3 动态链接(Dynamic Linking 也叫做 指向运行时常量池的方法引用)
- 每一个栈帧内部都包含一个指向运行时常量池的该栈帧所属方法的引用。其目的是为了当前方法的代码能实现动态链接(如 invokerdynamic指令)
- java源文件被编译成字节码文件的时候,所有的变量和方法都作为符号引用【类加载子系统链接阶段的解析步骤】保存在class文件的常量池里。动态链接的作用就是将这些符号引用转换为调用方法的直接引用
7.4 方法返回地址(Return Address)
- 存放调用该方法的pc寄存器的值(pc寄存器存储的是该方法要执行的下一条指令的值),即调用该方法的指令的下一条指令的地址。
- 方法的正常退出和异常推出的区别是:通过异常完成出口退出的 给他的上层调用者产生任何的返回值。
- 一个方法在正常调用完成后酒精返回哪一一个返回指令需要根据具体的返回值类型确定。
- 在字节码指令中,返回指令包括
ireturn:返回值类型是 byte、char、short、int、boolean
freturn:返回值类型是float
dreturn:返回值类型是double
areturn:返回值类型是引用类型
return:该方法为void方法、实例初始化方法【构造器】、类和接口的初始化方法【构造器】)
7.5 一些附加信息
栈帧中还允许携带与JVM实现相关的一些附加信息。如对程序调试提供支持的信息
8.方法调用
- 在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关
绑定:一个字段、方法或类的符号引用被替换为直接引用的过程,仅发生一次
静态链接:当一个字节码文件被装载到JVM内部时,如果被调用的目标方法,在编译期可知,且运行期保持不变。这种情况下将调用方法的符号引用转换为直接引用的过程叫静态链接,对应的绑定机制是早期绑定
动态链接:如果在被调用的方法在编译期不确定,只能在程序运行的期将调用方法的符号引用转换为直接引用。这种转换过程具备动态性,因此称之为动态链接,对应的绑定机制是晚期绑定 - 非虚方法与虚方法
如果方法在编译期就确定了调用的版本,在运行时保持不变,则该方法叫非虚方法
静态方法、final修饰的方法、私有方法、实例构造器、父类方法都是非虚方法,其他都是虚方法 - 虚拟机中提供了以下方法的调用指令
invokestatic:调用静态方法,解析阶段确定唯一版本
invokespecial:调用<init>方法、私有及父类方法、解析阶段确定唯一版本
invokevirtual: 调用所有虚方法
invokeinterface: 调用接口方法
invokedynamic:动态解析出需要调用的方法,然后执行。Java8中的Lamda表达式的出现,使得此指令的生成,在Java中才有了直接的生成方法
invokestatic和invokespecial指令调用的方法叫非虚方法,其余的(final修饰的方法除外)称为虚方法 - 静态类型语言与动态类型语言
静态类型语言与动态类型语言的区别在于对于类型的检查是在编译期还是在运行期,满足前者的是静态类型语言。静态类型语言是判断变量本身的类型信息,动态类型语言是判断变量值的类型信息。变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特性(如JS) - 重写(是动态分派的典型代表)
1)当调用一个对象的方法的时候,我们会将对象压入操作数栈,根据字节码指令(invokevirtual),寻找该对象的实际类型,记做C
2)如果在类型C中找到与常量池中描述符合简单名称都符合的方法,则进行访问权限的校验,如果通过,则返回该方法的直接引用,若不通过,则返回java.lang.IllegalAcessEror异常
3)若在常量中未找到符合的方法,按照继承关系从下往上依次对C 的父类进行第二步的搜索和验证。
4)如果始终未找到,则抛出java.lang.AbstrackMethodError异常 - 虚方法表
在面向对象编程中,会频繁的使用动态分派,如果每次在动态分派过程中都进行搜索和验证的步骤,必然会影响到执行效率,为了提升性能,JVM在类的方法区建立一个虚方法表
每个类中都有一个虚方法表,表中存放各个方法的实际入口,即真正调用的放方法
虚方法表在类加载的链接阶段的解析步骤中被创建并初始化
虚方法表1.png
如图,cat()类中的toString()是重写Object类中方法,所以toString()实际所属类型是Cat类的
9.虚拟机栈的面试题
- 举例栈溢出的情况(StackOverFlowError)?
通过 -Xss设置栈的大小 - 调整栈大小,就能保证不出现栈溢出么?
不能保证。可以设置栈大小确保出现的几率降低,但不能保证不初选栈溢出 - 分配的栈内存越大越好么?
不是的。总内存数是一定的,如果某个栈内存设置太大,会挤压其他内存结构的空间 - 垃圾回收是否会涉及到虚拟机栈
不会。
名称 | error | gc |
---|---|---|
程序计数器 | 不出现 | 不出现 |
虚拟机栈 | 出现 | 不出现 |
本地方法栈 | 出现 | 不出现 |
方法区 | 出现 | 出现 |
堆 | 出现 | 出现 |
- 方法中定义的局部变量是否线程安全?
具体问题具体分析。(如果局部变量是内部产生的,并且在方法内消亡的,则是线程安全的。如果是作为参数传进来的,或者作为方法返回值返回,则会存在线程安全问题)
何为线程安全?
--如果只有一个县城去操作此数据,则此数据必是线程安全的。
--如果多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题
四.本地方法栈
Java虚拟机栈是用来管理Java程序的调用,而本地方法栈是用来管理本地方法的调用
- 本地方法栈也是线程私有的
- 当我们调用本地方法的时候,会将此本地方法压入本地方法栈中,在执行引擎执行时加载本地方法库
五.本地方法接口(不属于运行时数据区)
- 一个Native Mehod 就是 Java调用非Java代码的接口
- 在定义一个Native Mehotd的时候,不需要提供 实现体,因为实现体是由非Java语言在外面实现的
thread、Object等类中有多个本地方法 - 标识符native可以与所有其他的java标识符连用, abstract除外(abstract是没有实现体,但是native是有实现体的,只不过是用非java语言实现的 )
- 为什么要使用本地方法?
- 1.与Java外环境交互
有时Java应用要与Java外环境交互,这是本地方法存在的主要原因 - 2.与操作系统交互
Java代码是运行在JVM上的,但是JVM毕竟不是一个完整的系统,经常依赖一些底层系统的支持,使用本地方法,我们得以使用Java实现了jre与底层系统的交互。还有我们可以通过本地方法使用java语言本身没有提供封装的操作系统的特性。 - 3.Sun's Java
sun的解释器本身使用C语言编写的
六.堆
- 1.与Java外环境交互
- 堆的知识点多,单独拿一小节细说
网友评论