运行时数据区
一、程序计数器(PC寄存器)
1.作用
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
2.相关面试问题
- 使用PC寄存器存储字节码指令地址有什么用呢?
为什么使用PC寄存器记录当前线程的执行地址呢?
cpu在并发的执行每个线程时,需要不停的切换线程,切换到一个新的线程就可以从pc寄存器中得知在执行那一步指令
- PC寄存器为什么会被设定为线程私有?
很多个线程如果都用一个PC寄存器的话,在cpu高速切换一轮又到第一次执行的线程时不知该从上次的第几个指令执行,造成执行混乱
二、虚拟机栈
1.Java中堆与栈的区别
栈是运行时的单位,而堆是存储的单位。 即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿
2.虚拟机栈基本内容
2.1 虚拟机栈是什么
早期称为Java栈,每一个线程创建时都会创建一个虚拟机栈,栈中为一个个栈帧(一个个栈帧对应一个个方法调用),线程私有
2.2 生命周期
与线程共身亡
2.3 作用
保存方法的局部变量、部分结果,并参与方法的调用与返回
2.4 特点
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
- JVM直接对Java栈的操作只有两个:
- 每个方法执行,伴随着进栈(入栈、压栈)
- 执行结束后的出栈工作
- 对于栈来说不存在垃圾回收(GC)问题,但存在内存溢出问题(OOM)
2.5 常见异常
首先我们要明确Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的
- 对于Java栈的大小是是固定不变的,如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,会抛出StackOverflowError异常。
- 对于Java栈的大小是动态的,在尝试扩展的时候无法中请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
2.6 设置栈大小
以idea为例,参考官方文档参数调优,在run中选择编辑配置,然后找到虚拟机大小设置
3.虚拟机栈的储存结构和运行原理
3.1 栈运行原理
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧( Current Frame) 。
方法的结束方式有两种:
- 正常的return返回
- 未经过处理的异常抛出
注意:
不同栈中的栈帧不能相互调用
3.2 栈帧的内部结构
每个栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)(或表达式栈)
- 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
- 一些附加信息
3.2.1 局部变量表(Local Variables)
- 栈帧的大小就是由五个结构决定着,其中局部变量表占主导
- 局部变量表可以理解为一个数组,其数组的长度在编译后就决定下来运行时不再改变
- 局部变量表中储存的是方法参数和定义在方法体内的局部变量
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
- 局部变量表中的变量只在当前方法调用中有效。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
关于Slot:
- 在局部变量中最基本的单位就是Slot(变量槽),32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
- Slot中以索引的方式来查找,如果访问两个Slot的变量只需要第一个Slot的索引即可
- 如果当前帧是由构造加法或者实例方法(非静态方法)创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。即可以解释静态方法中是没有this关键字的,因为压根没在局部变量表中存进去
- 对于局部变量如果出了作用的范围,且又有新的局部变量生成,则新的变量会将旧的变量的Slot槽进行占位复用,即Slot复用
关于局部变量表中的变量问题:
- 类变量赋值:Linking的准备(prepare)阶段:给类变量默认赋值—>初始化(initial)阶段:给类变量显式赋值即静态代码块赋值
- 实例变量赋值:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
3.2.2 操作数栈(Operand Stack)
- 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
- 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。其所需的最大深度在编译期就已经决定
- 栈中的数据类型大小同理Slot,32位占一个栈单位深度;64位占两个栈单位深度
- 虽然物理层面上用的是数组,但逻辑上是一个栈,所以对于数据的操作只有入栈和出栈之说
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
3.2.3 动态链接(Dynamic Linking)指向运行时常量池的方法引用
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如: invokedynamic指令
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
关于常量池的作用:
就是为了节省资源,提供一些常用的符号及常量,便于指令的识别
3.2.4 方法返回地址(Return Address)
- 存放调用该方法的pc寄存器的值。
- 前面我们知道方法结束有两种方式:一是正常return,二是出现异常。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。
- 方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
- 通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息即不会给上层任何返回信息。
关于成功执行后返回指令:
- 一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。
- 在字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char.short和int类型时使用)、lreturn(long类型)、freturn(float类型)、dreturn(double类型)以及areturn(引用类型)
- 另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始
化方法使用。
3.2.5 一些附加信息(说明:不一定有附加信息)
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
3.2.6 顺便一提栈顶缓存技术
为了解决字节码代码频繁的进栈出栈代码重复冗余及内存的读/写次数,栈顶缓存(ToS,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理cpu的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
3.3 方法的调用
3.3.1 静态链接和动态链接
- 静态链接:被调用的目标方法在编译期可知,且运行期间保持不变
- 动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用
3.3.2 早期绑定和晚期绑定
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。 绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
- 静态链接对应的绑定就是早期绑定
- 动态链接对应的绑定就是晚期绑定
以上两种绑定的定义与两种链接的定义相似,只不过以更加宏观的绑定角度来阐述
3.3.3 虚方法和非虚方法
- 虚方法:与动态链接、晚期绑定相对应
除了静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。其他方法称为虚方法。
- 非虚方法:与静态链接、早期绑定相对应
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的这样的方法称为非虚方法。
- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
3.3.4 虚方法表
- 目的:对了解决在面向对象编程中频繁的使用动态分派(即查找方法重写的过程),JVM会在类的方法区中建立一个虚方法表来提高性能,以索引表来代替查找
- 创建时间:虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
由图看到Father与Son是继承关系,Son类重写了hardChoice(QQ)、hardChoice(_360)两个方法,各自重写的方法指向各自本身。然后没重写的方法都是指向Father与Son类的父类来做到节省资源,提高效率
3.3.5 调用指令
普通调用指令:
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
动态调用指令:
- invokedynamic:动态解析出需要调用的方法,然后执行
我们都知道在jdk7之前Java一直是静态语言即在声明变量之前已经确定了变量类型,在jdk7以后以及jdk8中出现的lambda表达式让Java体现出了动态语言的特性即根据变量值判断变量类型 像js、python语言都是典型的动态语言
var iii = 10.0;
var jjj = "你好";
iii = 10.0;
前四条指令固化在虚拟机内部,方法的调用执行不可人为千预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
3.3.6 小结
我们都知道子类对象的多态性体现的前提是:
- 类的继承关系
- 方法的重写
对于方法的重写,在Java中的本质就是在操作数栈顶中找到对象对应的实际类型,如果找到所有都相匹配的方法则进行权限校验,如果通过则调用直接引用,如果不通过则报出java.lang.IllegalAccessError异常(很多jar包冲突都会报这个异常)。然后继续按照继承关系向上寻找方法以此类推,最后的最后还是没有找到对应的方法则报出java.lang.AbstractMethodError异常
静态链接、早期绑定与非虚方法都没有体现多态性;相反动态链接、晚期绑定与虚方法调用目的就是为了体现Java语言的多态性
4.关于虚拟机的面试题
4.1 举例栈溢出的情况
栈溢出(StackOverflowError)在Java栈中大小保持固定不变时,如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,会抛出StackOverflowError异常。通过-Xss参数能设置栈大小
4.2 调整栈大小,就能保证不溢出吗
不能,因为就算栈再大,更多的线程分配更多的栈也有会溢出的情况,调整栈大小理论上就是延缓StackOverflowError异常出现的时间
4.3 分配的栈内存越大越好吗
不是,一味的只增加内存,只是减少单位时间内出现溢出的情况变少,长久来看治标不治本;而且单个栈内存太大会挤占别的线程的空间
4.4 垃圾回收是否会涉及到虚拟机栈
不会的
4.4 方法中定义的局部变量是否是线程安全的
对于内部产生内部消亡的局部变量我们说是线程安全的,不是从外部传进来的参数或是需要返回的参数。
三、本地方法栈
1.概述
- Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
- 本地方法栈,也是线程私有的。
- 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
- 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。 它甚至可以直接使用本地处理器中的寄存器,即体现了c的效率 直接从本地内存的堆中分配任意数量的内存。
2.注意
这些只是针对HotSpot虚拟机来说的
本地方法库(C库)
一、本地方法接口
1.本地方法
简单地讲,一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如c。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "c"告知C++编译器去调用一个c的函数。
public class IHaveNativesi{
public native void Native1(int x);
native static public long Native2();
native synchronized private float Native3(Object o);
native void Native4(int[] ary) throws Exception;
}
2.本地方法接口
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。是没有方法体的
3.现状
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用socket通信,也可以使用web service等等,不多做介绍。
|