学习笔记:Java虚拟机JVM
- Qingfeng Zhang
- Java
- April 5, 2020
Sections
1.JVM的生命周期
- JVM的生命周期包括虚拟机的启动 、执行 和退出 ;
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial
class)来完成的,这个类是由虚拟机的具体实现确定的;
程序开始执行Java虚拟机才运行,程序执行结束或出现异常Java虚拟机就停止,而执行一个程序其实是在执行一个Java虚拟机进程 ;
下图描述的是虚拟机的结构:
2.类加载器子系统Class Loader
2.1 类加载过程
类加载器子系统负责从文件系统或网络中加载class文件,class文件在文件开头有特定的文件标识(CAFEBABE);
类加载器只负责class文件的加载,运行由执行引擎决定;
加载的类信息存放在一块称为方法区的内存空间,此外方法区还会存放运行时常量池的信息,可能还包括字符串字面量和数字常量(class文件中的常量池的内存映射);
加载 :通过一个类的全限定名获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
链接 :验证:确保Class文件的正确性和安全性;准备:为类变量分配内存并设置初始值(不包括final修饰的static),但不会为实例变量分配初始化;解析:将常量池的符号引用转化为直接引用;
- 初始化 :初始化阶段是执行类构造器方法()的过程,此方法不用定义,是javac编译器自动收集类中的类变量赋值动作 和静态代码块中的语句 合并而来,不同于类的构造器(JVM中的()),并且父类的()在子类前执行
2.2 类加载器
Java支持两种类加载器:引导类加载器(启动类加载器)(Bootstrap ClassLoader)和自定义类加载器(User-
Defined ClassLoader);
一般将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器;
- 引导类加载器 :
引导类加载器由c/c++实现,加载Java核心类库,加载扩展类加载器(Extension ClassLoader)和应用程序类加载器(AppClassLoader),并指定为他们的父类加载器;
- 自定义类加载器 :
类的加载时一般由上面三种类加载器完成,有必要时可以自定义类加载器:隔离加载类、修改类加载的方式、扩展加载源、防止源码泄露;
2.3 双亲委派机制
Java虚拟机对于class文件采用的是按需加载 的模式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式 ,即把请求交由父类处理,它是一种任务委派模式;
- 双亲委派模式原理 :
(1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是将加载请求委托给父类的加载器去执行;
(2)如果父类的加载器还存在父类加载器,则进一步向上委托,依次递归,最终请求达到顶层的启动类加载器;
(3)如果父类加载器可以完成类加载任务,就成功返回,否则子加载器才会尝试自己去加载;
双亲委派机制可以避免类的重复加载,保护程序的安全,防止核心API被随意篡改;
举个栗子,如果自定义一个String()类,并对其进行加载,由于双亲委派机制
,加载任务委托给了引导类加载器,最后加载的并不是自定义的String()类,这样可以保证对于Java核心源代码的保护,这就是沙箱安全机制 ;
3.运行时数据区Runtime Data Area
运行时数据区包括方法区Method Area 、程序计数器Program Counter Register 、本地方法栈Native Method Stack 、堆heap 、虚拟机栈Java Virtual Machine Stack ,其中方法区和堆是线程共享的;
3.1 程序计数器(PC寄存器)
JVM中的PC寄存器是对物理的PC寄存器的一种抽象模拟,用于存储指向下一条指令的地址,也即将执行的指令代码,由执行引擎读取下一条指令;
程序计数器占用空间很小,每个线程都有自己的程序计数器,是线程私有的,与线程的生命周期保持一致,程序计数器会存储当前线程正在执行的Java方法的JVM指令地址,是程序控制流的指示器,并且是JVM中唯一没有规定OutOfMemoryError的区域;
使用PC寄存器存储字节码指令地址有什么用?
答:因为CPU需要不停地切换线程,切换回来之后从程序计数器中明确下一条执行的指令,这也解释了为什么程序计数器要设定为线程私有的;
3.2 虚拟机栈
栈是运行时的单位,而堆是存储的单位,即栈解决程序的运行问题,堆解决数据存储的问题;
每个线程在创建的时候都会创建一个虚拟机栈,是线程私有的且没有垃圾回收,其内部保存一个个的栈帧
,对应着一次次的Java方法调用,虚拟机栈的作用是主管Java程序的运行,保存方法的局部变量(基本数据类型和对象的引用地址)、部分结果,并参与方法的调用和返回;
- 栈中可能出现的异常:
JVM规范允许Java栈的大小是动态的或是固定不变的。如果大小固定,则需要在线程创建的时候确定虚拟机栈的容量,如果超出最大容量,Java虚拟机会抛出StackOverflowError异常;如果是动态扩展的,并且在扩展的时候无法申请到足够的内存,或是在创建新线程的时候没有足够的内存来创建对应的虚拟机栈,则Java虚拟机会抛出OutOfMemoryError异常;
- 栈帧:
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack
Frame)的格式存在的,在此线程上执行的方法都会对应一个栈帧,栈帧是一块内存区域,是一个数据集,维系着方法执行过程中的各种数据信息;
方法返回函数有两种方式:一种是正常的函数返回,使用return指令,另一种是抛出异常,这两种都会导致方法对应的栈帧被弹出;
栈帧的结构:
- 局部变量表
- 操作数栈(或表达式)
- 动态链接(或指向运行时常量池的方法引用)
- 方法返回地址(或方法正常退出或者异常退出的定义)
- 一些附加信息
I.局部变量表local variables
定义为一个数字数组,主要用于存储方法参数 和定义在方法体内的局部变量
(基本数据类型、对象的引用和返回值类型),表的大小在编译期就已确定并不会改变。
局部变量表中的变量只在当前方法调用中有效,方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程;
局部变量表中最基本的存储单元是Slot(变量槽)
,32位以内的类型占用一个Slot(包括引用类型),64位以内的类型占用两个Solt。如果当前帧由构造方法或实例方法创建,那么该对象引用this将会存放在index为0的Slot中(因此才能使用this,否则不行);
栈帧中的局部变量表中的槽位可以是重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会使用过期的槽位,达到节省资源的目的;
II.操作数栈Operand Stack
操作数栈在方法执行的过程中根据字节码指令,往栈中写入数据或提取数据,主要用于保存计算过程中的中间结果
,同时作为计算过程中变量临时的存储空间,和局部变量表一样其大小在编译期就已确定;
如果上一个被调用的方法带有返回值的话,其返回值就会被压入当前栈帧的操作数栈中;
Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈;
基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但是完成一项操作的时候必然需要使用更多的入栈和出栈指令,这意味着需要更多的指令分派次数和内存读写次数,栈顶缓存技术(ToS)
是将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率;
III.动态链接Dynamic Linking
每一个栈帧内部都包含一个指向运行时常量池(方法区)中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接;
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件中的常量池里,比如一个方法调用了其他方法,是通过常量池中指向方法的符号引用来表示的,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用;
方法的调用:
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关;
静态链接 :当字节码文件被装载进JVM内,如果被调用的目标方法在编译期可知,且运行期不变,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接;
动态链接 :如果被调用的方法在编译器无法被确定,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这个过程具有动态性,因此称之为动态链接;
静态链接和动态链接分别对应早期绑定 和晚期绑定 。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,仅发生一次,Java具备封装、继承、多态等面向对象特性,因为有多态特性,所以具备早期绑定和晚期绑定两种方式;
虚方法 和非虚方法 :非虚方法是在编译期就确定了方法具体的调用版本,这个版本在运行时是不可变的;静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法(意思是这些方法在子类和父类中可以直接确定,就算有同名的静态方法,因为静态方法不能被重写,所以也可以确定调用的版本),其他方法称为虚方法;
虚方法表: 在面向对象编程中,会频繁使用动态分配,如果在每次动态分配的过程中都要重新在类的方法元数据中搜索合适的目标,会影响到执行的效率,为了提高新能,JVM采用在类的方法区建立一个虚方法表,使用索引表来代替查找;
IV.方法返回地址return address
方法返回地址存放该调用方法的PC寄存器的值,交给执行引擎继续执行;
无论方法是否正常结束,方法退出后都会返回到该方法被调用的位置,方法正常退出时,调用者的PC寄存器的值作为返回地址,即方法被调用指令的下一条指令的地址,如果是异常退出,返回地址是通过异常表来确定,栈帧中一般不保存此信息;
3.3 本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法(调用非Java代码的接口)的调用,本地方法栈是线程私有的;
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界,它和虚拟机拥有同样的权限,本地方法可以通过本地方法接口访问虚拟机内部的运行时数据区,甚至可以直接使用本地处理器的寄存器,也可以直接从本地内存的堆中分配任意数量的内存。
4.本地方法接口Native Method Interface[
一个本地方法Native Method是一个Java调用非Java代码的接口,是由非Java语言实现的Java方法,其作用是融合不同编程语言为Java所用,在定义一个native method的时候,并不提供实现体(有点像定义Java接口,但不同于抽象方法),thread中的start()就是一个native method;