JVM整体架构
可以分为三层:
1 JVM外部,从源文件到Class文件,再装载到JVM
2 JVM运行时数据区,相当于就是避风港、运行的大后方。给执行程序提供后勤。
3 执行引擎层。和运行时数据区交互,完成执行任务。
运行时数据区
终于到了这里,从前段编译器开始,到类加载初始化终于是通过了JVM的第一层。现在是JVM的第二层,也是主体部分。
所谓运行时数据区,实际上是JVM管理内存的一种划分。
JVM对内存的管理,可以从生命周期的角度进行划分。一种内存是伴随着JVM启动开始,直到JVM退出结束的内存,这种就是共享内存。另一种是随着线程创建时创建,线程销毁时释放的内存,这种就是线程私有内存。
共享内存比较简单,所以我们这里从线程角度出发,了解JVM的内存管理。
共享区域
包括方法区,堆区
线程私有
每个线程都有自己的
-
程序计数器(用来记录现在执行到的代码位置)
-
虚拟机栈(存储栈帧,栈帧里面存储线程执行过程中的方法调用数据,局部变量,等等)
-
本地方法栈,存储本地方法的栈
在HotSpot虚拟机中,常见的守护线程主要包括以下3种。
(1)垃圾回收线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
(2)编译线程:这种线程在运行时会将字节码编译成本地代码。
(3)手动创建守护线程:在调用start()方法前调用setDaemon(true)可以将线程标记为守护线程。
程序计数器
实际上记录的是当前线程执行代码所在的行数位置。字节码解释器就是通过改变这个计数器的值来选取下一条指令来执行。它是程序控制流的指示器。
分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成。
如果线程执行的是一个Java方法,那么程序计数器指令的地址就是下一次执行的代码。如果执行的是本地方法,那么程序计数器中的值是空(undefined)
程序计数器既没有垃圾回收也没有内存移除。因为程序计数器就是很简单的一个存放数值的区域。
程序计数器和执行引擎直接交互。
程序计数器存储的数据结构就是偏移地址,执行引擎根据得到的指令地址,然后操作JVM局部变量表,操作数栈,进行操作。
程序计数器的设计使得多线程执行中,CPU知道当前线程之前执行到那条指令了。
线程私有,使得每个线程之间相互不影响,可以有条不紊的并发执行。
虚拟机栈
栈由栈帧组成,每个栈帧又包括局部变量表、操作数栈、动态链接、方法返回地址和一些附加信息。
因为java语言要实现跨平台性,因为不同平台的CPU架构不同,所以JVM不能基于寄存器,是基于栈架构(操作数栈)设计的Java指令。这样设计的优点是可以跨平台,指令集小,编译器容易实现。但是实现相同功能所需要更多的指令。
虚拟机栈,早起也叫做Java栈,每个线程创建的时候都会创建一个虚拟机栈,虚拟机栈中存放栈帧。
每个栈帧代表一个方法的调用,存放方法中的局部变量、操作数栈(没错栈中栈)、动态链接、方法出口等信息。
栈帧入栈和出栈的过程就是一个方法被调用到执行完毕的过程。
虚拟机栈解决的事程序运行的问题,每个方法之间的调用顺序.....
特点
-
快速有效,速度仅次于程序计数器
-
不存在垃圾回收,但是内存存在溢出(栈溢出)
-
栈的操作先进先出。方法调用入栈,方法结束出栈。
Java虚拟机规范中允许栈是可动态大小或者固定的。但是现在HotSpot虚拟不支持栈动态扩展。
虚拟机栈可能出现的异常有两个1 栈溢出异常stackoverflowerror 2 outOfMemoryERROR 无法申请到内存
可以通过虚拟机启动参数 -Xss (也就是 stacksize缩写) 来设置最大栈空间
栈帧
栈帧是Java方法的运行环境,栈帧是一个内存区块,也是一个数据集,维系着方法执行中的各种数据信息。
局部变量表(Local Variables)方法中的局部变量内存分配就在栈帧中
操作数栈,每个栈帧有自己的操作数栈(或者是表达式栈)
动态链接(指向运行时常量池的方法引用,代码所在地)
方法返回地址(方法正常返回的定义和异常的定义)
一些附加信息。
局部变量表
也叫做局部变量数组,或者本地变量表。只有基本数据类型的数据,会创建在局部变量表中,引用数据类型通过new分配内存都是在堆中。
局部变量实际上定义为一个数字数组,用于存储方法的参数,和方法內的局部变量。对于基本数据类型,则直接存储其值,对于引用类型的存储指向对象的引用。returnAddress也是一样的。
局部变量表是线程私有的,所以没有数据安全问题。
局部变量表的大小是在编译期间就定下来的,保存在方法的code属性表中的maxinum local variables数据项中。
局部变量表影响栈帧的大小,栈帧的大小影响虚拟机栈能够容纳栈帧的次数,也就是影响栈能够的方法嵌套数。
局部变量表中的变量只在房钱方法中有效,在方法执行结束后,局部变量表就会随这栈帧被销毁。
局部变量表中的基本的存储单位是Slot(变量槽),局部变量表中存放8中基本数据类型,引用类型,returnAddress类型。
32位及其以下的数据类型,占用一个slot(byte,short,boolean,char,包括引用类型,returnAddress都转换成int进行存储),而double,long这些都是占据2个slot。
每个slot会分配一个索引,通过索引号即可访问到局部变量表中的数据。
slot可以被重复利用,当slot中的局部变量过了作用域,那么就可以将slot重复利用。
public void test(){{int a = 1;System.out.println(a);}//当a变量的作用域结束,b会重用a的slotint b =1;}
局部变量没有系统初始化过程,这意味着我们必须要手动赋值才能使用,否则会报错。
局部变量表中的returnAddress完成方法的传递,局部变量表还是垃圾回收的根节点,众多栈中的众多栈帧中的局部变量表中直接或者间接引用的对象都不会被回收。
操作数栈
栈帧中还有一个栈,就是操作数栈,也叫做表达式栈。是用来正确解析表达式执行顺序的一个重要结构。
主要用于保存计算过程的中间结果,同时作为计算过程中的变量临时存储的空间。
在方法的实行过程中,字节码指令会将数据写入操作数栈,或从操作数栈中取出,使用后再把结果压入栈。
例如执行2+3的时候,会现将2,3压入栈,然后弹栈,得出结果5再压入栈
每个操作数栈深度是从编译阶段就确定好的,定义在方法的code属性表中的maxinum stack size 数据项中。
栈中的任何一个元素都可以是任意的Java数据类型。32位的类型占用一个栈单位深度,64位的类型占用两个栈单位深度。这个和局部变量表都有点像了。
方法在返回值的时候就是将返回值,压入操作数栈(按理说这个时候操作数栈之中只会出现一个数据)
JVM的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
栈顶缓存技术
也就是因为操作数栈的访问太过频繁,为了提升效率,将操作数栈栈顶附近的几个操作数放入寄存器。因为寄存器的空间有限,所以只能作为几个操作数的缓存。这个缓存技术和各种缓存中间件有同工之处。
动态链接
每个栈帧中都会存储一个在运行时常量池中,该栈帧所属方法的引用。(简单提一下,运行时常量池是在方法区中的,方法区中的各种信息,通过运行时常量池来寻址)
包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。
Current Class Constant Pool Reference区域为动态链接,method references区域代表着方法的引用地址,即直接引用。
在运行时,将方法的符号引用,替换成了直接引用。
方法的调用
之前在虚拟机栈,中栈帧保存的动态链接就是方法调用的一种。
动态链接实际上就是将符号引用转换成调用方法的直接引用。在JVM中,除了动态链接,还有一种叫做静态链接。
JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关,方法的绑定机制有两种,分别是静态链接和动态链接。
封装、继承和多态等面向对象特性,既然编程语言具备多态特性,那么自然也就具备静态链接和动态链接两种绑定方式。
静态链接
在字节码载入JVM之后,如果一个方法在编译阶段可知,运行时不变(也就是方法所要执行的代码是已知且不变的),这种情况下,调用方法的符号引用转换成为直接引用的过程叫做静态链接。
动态链接
执行的具体代码无法确定下来,只有在程序运行时将方法的符号引用转换成直接引用,这种引用转换具有动态性。
区分
静态链接和动态链接一般还会被称为早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定的意思就是一个字段、方法或者类的符号引用被转换为直接引用的过程,这仅仅发生一次。
静态链接是指方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,一般称这样的方法为非虚方法。除去非虚方法的都叫作虚方法。
实际上和C++中的虚方法类似,虚方法就是可以被重写的方法。这类方法在调用的时候并不知道调用的具体会是虚方法的哪个重写版本。只有在运行时才能确定。而final修饰的方法不能再被重写,所以不算是虚方法了。静态方法也没有重写。
有些时候如果不能很好的区分虚方法和非虚方法,可以通过字节码文件的指令来区分。
虚拟机中提供了以下5条方法调用指令。
(1)invokestatic:调用静态方法,解析阶段确定唯一方法版本。
(2)invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本。
(3)invokevirtual:调用所有虚方法。
(4)invokeinterface:调用接口方法。
(5)invokedynamic:动态解析出需要调用的方法,然后执行。
这里有另一种解释
-
invokestatic:用于调用静态方法。
-
invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
-
invokevirtual:用于调用非私有实例方法。
-
invokeinterface:用于调用接口方法。
-
invokedynamic:用于调用动态方法。
JVM系列之:JVM是怎么实现invokedynamic的?_jvm invokedynamic-CSDN博客
JVM系列之:关于方法句柄的那些事-CSDN博客
复习路线
JVM 复习1-CSDN博客