JVM - 运行时数据区

1. 运行时数据区内部结构

在这里插入图片描述
内存是非常重要的系统资源,是硬件和CPU的中间仓库和桥梁,承载着操作系统和应用程序的实时运行;
JVM内存布局规定了Java在运行过程中内存申请,分配,管理的策略,保证了JVM的高效稳定运行,不同的JVM对于内存的划分方式和管理机制存在部分差异(比如方法区的不同)

在这里插入图片描述
一个JVM的实例对应着一个Runtime实例,一个Runtime实例就相当于对应着一个运行时数据区

2. JVM中线程说明

在Hotspot JVM中,每个线程都与操作系统的本地线程直接映射,当一个线程准备好执行以后,此时一个操作系统的本地线程耶同时创建,Java线程执行终止后,本地线程也会回收;

3. 程序计数器(程序钩子)

它的作用可以看做是当前线程所执行的字节码的行号指示器

在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成

我们称这类内存区域为“线程私有”的内存

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储

对于Natvie方法

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)

OutOfMemoryError

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

为什么需要PC寄存器

① Java字节码解释器需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令;
② CPU在不断切换线程,这个时候切换回来以后,就得知道接着从哪里开始继续执行

在这里插入图片描述

4. 虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型

4.1 基本概念

栈是运行时的单位,堆是存储的单位

栈解决的是程序运行时问题,即程序如何执行,或者说如何处理数据
堆解决的是数据存储的问题,即数据怎么放,放在哪里(栈也可以放,放基本类型数据,对象引用,returnAddress类型)

线程私有

该区域也是线程私有的,其生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型,主管Java程序的运行,它保存方法的局部变量(8中基本数据类型,对象的引用地址),部分结果并参与方法的调用和返回

OutOfMemoryError
存在OOM不存在GC

栈中可能出现的异常
Java虚拟机规范允许Java栈的大小是动态的或者固定不变

StackOverFlowError:如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机容量可以在线程创建的时候独立选择;如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,就会抛出StackOverFlowError(比如我疯狂递归没有出口,那么就会疯狂创建栈帧不出战,自然会栈内存异常)

OutOfMemoryError:如果Java虚拟机栈是动态扩展的,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新线程的时候没有足够的内存去创建对应的虚拟机栈,那么会抛出OutOfMemoryError

4.2 栈运行原理

每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在的,一个方法对应一个栈帧,栈帧是内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
在这里插入图片描述

4.3 栈的存储结构

在这里插入图片描述

  • 局部变量表
  • 操作数栈(表达式栈)
  • 动态链接(指向运行时常量池的引用)
  • 方法的返回地址(方法正常退出或异常退出的定义)
  • 一些附加信息
① 局部变量表
  1. 局部变量表也被称为局部变量数组或者本地变量表

  2. 定义为一个数字数组,主要用于存储方参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型对象引用(reference),以及returnAddress类型

    对于为什么是数字数组,首先对于byteshortchar在存储前会被转换为intboolean也会被转换为int,0标识false,1标识true

  3. 由于局部变量表是建立在线程的栈上的是线程的私有数据,所以不存在数据安全问题

  4. 局部变量表所需容量大小是在编译期间就确定下来的,并保存在方法的Code属性的maximun local variables数据项中,在方法的运行期间是不会改变局部变量表大小的

public class LocalVariablesTest {
    public static void main(String[] args) {
        LocalVariablesTest test = new LocalVariablesTest();
        int a = 10;
        int b = 12;
        int c = 13;
    }
}
//javap -v LocalVariablesTest.class

反编译后局部变量表的结果如下
在这里插入图片描述
在这里插入图片描述
关于slot的理解

  • 局部变量表,最基本的存储单元是slot,在局部变量表中,32位以内的类型只占一个Slot(包括returnAddress),64位的占2个Slot(long,double)

  • JVM会为局部变量表的每一个Slot都分配一个索引,通过索引访问局部变量值

  • 如果当前栈帧是由构造方法或者实例方法创建的,那么该对象的引用this会放在index为0的slot处
    在这里插入图片描述
    这也就变相解释了为什么static方法中不能使用this

slot的重复利用
栈帧中的局部变量表中的槽位时可以重复利用的,如果一个局部变量表过了其作用域,那么在其作用域知之后声明的新的局部变量会复用过期局部变量的槽位,从而达到节省资源的目的

public void SlotTets(){
	{
		int a = 0;
		System.out.println(a);
	}
    int b = 0;//b会和a公用槽位
}

在这里插入图片描述
补充说明
局部变量表中的变量是重要的垃圾回收根节点,只要被直接变量表中直接或者间接引用的对象都不会被回收

② 操作数栈(表达式栈)

每一个栈帧中除了局部变量表以外还包含操作数栈,操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间

操作数栈就是JVM执行引擎的一个工作区,我们所说的Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈

操作数栈在方法执行的过程中,根据字节码指令,往栈中写入数据(入栈)或提取数据(出栈)

某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用后再把值压入操作数栈(比如复制,交换,相加等操作)
在这里插入图片描述
针对下面的代码看一下

public void testAddOperation(){
	byte i = 15;
	int j = 8;
	int k = i+j;
}
 0 bipush 15 //把i 15压入操作数栈,byte,short,char存放的时候都以int来保存
 2 istore_1  //把操作数栈顶的数出栈放入局部变量表slot1(slot0放了this)
 3 bipush 8  //把j 8压入操作数栈
 5 istore_2  //把操作数栈顶的数出栈放入局部变量表slot2
 6 iload_1	 //从局部变量表取索引为1的入栈
 7 iload_2	 //从局部变量表取索引为2的入栈
 8 iadd		 //+ 需要执行引擎翻译成机器指令让Cpu运算
 9 istore_3	 //把操作数栈顶的数出栈放入局部变量表slot3
10 return

**从字节码角度看i++++i **

public void testAboutPlusPlus(){
	int i = 10;
	int j = i++;

	int n = 10;
	int m = ++n;
}
//-------------i++ 先把i压入操作数栈,再在局部变量表进行++,再把操作数栈的值出栈
 0 bipush 10
 2 istore_1
 3 iload_1	  //先load
 4 iinc 1 by 1//iinc是在局部变量表的slot上进行自增运算
 7 istore_2	  //存的是最初load的值
//-------------++i 先在局部变量表进行++,再把i压入操作数栈,再把操作数栈的值出栈
 8 bipush 10
10 istore_3
11 iinc 3 by 1//先iinc
14 iload_3	  //再load
15 istore 4	  //存的是iinc以后的值
17 return

从字节码分析x = x++
永远等于0
① 先从局部变量表取x放到操作数栈
② 局部变量表的x自增
③ 把操作数栈的x放到局部变量表x的槽位

③ 动态链接

在这里插入图片描述
每个栈帧中都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码可以实现动态链接,比如invokedynamic指令

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中;比如:描述一个方法调用了另外的方法,就是通过常量池中指向方法的符号引用来标识的,那么动态链接的作用就是为了将这些符号引用准换为调用方法的直接引用

看下面的代码

public class DynamicLinkingTest {
    int num = 0;
    public void methodA(){
        methodB();
        num++;
    }
    public void methodB(){

    }
}

在methodA中调用了methodB
在这里插入图片描述
在这里插入图片描述
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接

方法的调用

深入理解Java虚拟机(七)字节码执行引擎(栈帧、动态连接、方法调用)
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。

一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址,这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用

所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)

在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。

在JVM中,将调用方法的符号引用转换为调用方法的直接引用与方法的绑定机制有关
绑定是一个字段,方法或者类在符号引用被替换为直接引用的过程

静态链接(早期绑定)
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期间可知且运行期间保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接

在静态链接早期绑定中的方法是非虚方法,静态方法,私有方法,final方法,实例构造器(this.init),父类方法(super.method)都是非虚方法(编译期间就确定下来的方法)

动态链接(晚期绑定)
如果被调用的方法在编译期间无法被确定下来,也就是说只有在程序运行期将调用方法的符号引用转换为直接引用(多态),由于这种引用转换过程具备动态性,因此被称为动态链接

虚拟机中提供了以下几条方法调用指令

|--调用非虚方法
|---① `invokestatic`: 调用静态方法
|---② `invokespecial`:调用`<init>`方法,私有,父类方法
|--调用虚方法(finalc除外,是非虚方法,但也是用invokevirtual调用)
|---③ `invokevirtual`:调用所有虚方法
|---④ `invokeinterface`:调用接口方法
|---⑤ `invokedynamic`:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,- - invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的

前4条指令固化在虚拟机内部,方法调用执行不可人为干预,而第5条则支持由用户确定方法版本

静态语言和动态语言
动态语言和静态语言的区别在于对类型的检查是在编译期还是运行期间,前者是静态类型语言(Java),后者是动态类型的语言(js)

//java
String info = "minifull";
//js
var name = "minifull"
④ 方法返回地址

存储调用该方法的PC寄存器的值
一个方法的结束有两种方式:① 正常执行结束 ② 出现未处理的异常非正常退出

无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置,方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令地址;
而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中不保存这部分信息

本质上,方法的退出就是当前栈帧出栈的过程,此时,需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器的值等,让调用者的方法继续执行下去

⑤ 附加信息

可选

4. 本地方法库

在这里插入图片描述
什么是本地方法
简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数

在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体是由非java语言在外面实现的

public class IHaveNatives{
	native public void Native1( int x ) ;
    native static public long Native2() ;
    native synchronized private float Native3( Object o ) ;
    native void Native4( int[] ary ) throws Exception ;
}

为什么要使用native方法
java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
① 与java环境外交互:
有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。

② 与操作系统交互:
JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎 样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。

③ Sun’s Java
Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread 的 setPriority()方法是用java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用

5. 本地方法栈

Java虚拟机栈用于管理Java方法的调用,本地方法栈式用于管理本地方法的调用,是线程私有的

发布了201 篇原创文章 · 获赞 4 · 访问量 7631
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 数字20 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览