目录
第一章 JVM与Java体系结构
1.字节码
- 不同的编译器,可以编译出相同的字节码文件,字节码文件也可以在不同的jvm上运行
- Java虚拟机与Java语言并没有必然联系,它只与特定的二进制文件格式——class文件格式所关联,class文件中包含了Java虚拟机指令集(字节码)和符号表,和其他辅助信息
2.虚拟机
虚拟机就是一台虚拟的计算机,是一款软件,用来执行一系列虚拟的计算机指令
- 系统虚拟机:VMware,对物理计算机的仿真,提供一个可运行完整操作系统的软件平台
- 程序虚拟机:Java虚拟机,专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令称为Java字节码/JVM字节码指令
3.JVM的位置
JVM运行在操作系统之上,与硬件没有直接交互
4.JVM的整体结构
首先明确解释器与编译器的概念:
- 编译器是一种计算机程序,负责把一种编程语言编写的源码转换成另外一种计算机代码,后者往往是以二进制的形式被称为目标代码(object code)。这个转换的过程通常的目的是生成可执行的程序
- 解释器是一种计算机程序,它直接执行由编程语言或脚本语言编写的代码,并不会把源代码预编译成机器码
详细图
5.Java代码执行流程
第一次编译是把源文件编译成字节码(javac),第二次编译是把字节码指令编译成机器指令(JIT),并把常用的指令缓存起来
6.JVM的架构模型
由于跨平台性的设计,Java指令都是根据栈来设计的。因为不同平台的cpu架构是不同的,所以不能设计为基于寄存器的
栈:跨平台性、指令集小、指令多、编译器容易实现,执行性能比寄存器差
7. JVM的生命周期
虚拟机的启动:通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类由虚拟机的具体实现指定
虚拟机的执行:
- 一个运行中的Java虚拟机有一个清晰的任务,执行Java程序
- 程序开始执行时它才执行,程序结束时它就停止
- 执行一个所谓的Java程序的时候,真正执行的是一个叫Java虚拟机的进程
虚拟机的退出:
- 程序正常执行结束
- 程序在执行过程中遇到异常或错误而终止执行
- 操作系统出现错误而导致Java虚拟机进程终止
- 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法
第二章 类加载子系统
1.类加载器与类的加载过程
- 类加载器子系统负责从文件系统或网络中加载Class文件,class文件在文件开头有特定的文件标识
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
- 加载的类信息存放于一块称为方法区的内存空间。除了类的信息,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
类的加载过程,包含以下三步
一、加载
- 通过一个类的全限定名获取定义此类的二进制字节流(可能是一个存储在物理磁盘中的文件,以流的方式加载到内存中)
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
二、链接
- 验证
- 目的在于确保Class文件的字节流中包含的信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
- 准备
- 为类变量(static)分配内存并设置该变量的默认初始化值,即零值(初始化阶段才赋值为真正的值)
- 这里不包括用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化(该是多少是多少)
- 这里不会为实例变量分配初始化(因为此时还没创建对象),类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
- 解析
- 将常量池内的符号引用转换为直接引用
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
- 符号引用就是一组符号来描述所引用的目标;直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
三、初始化
- 初始化阶段就是执行类构造器方法<clinit>()的过程
- 此方法不需要定义,是javac编译器自动收集类中的所有的类变量的赋值动作和静态代码块中的语句合并而来
- 构造器方法中指令按语句在源文件中出现的顺序执行
- <clinit>()不同于类的构造器
- 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
- 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁
2.类加载器的分类
JVM支持两种类型的类加载器,分别为引导类加载器和自定义类加载器。JVM规范将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器(Extension Class Loader、System Class Loader),其中Bootstrap Class Loader是C++写的,其他的都是Java写的
- 对于用户自定义类来说,默认使用系统类加载器进行加载
- Java的核心类库都是使用引导类加载器进行加载的(String)
一、启动类加载器(引导类加载器,Bootstrap ClassLoader)
-
这个类加载器使用C/C++语言实现,嵌套在JVM内部
-
它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自java.lang.ClassLoader,没有父加载器
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
- 出于安全考虑,BootStrap启动类加载器只加载包名为java、javax、sun等开头的类
二、扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
三、应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 通过ClassLoader#getSystemClassLoader()方法
- 该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载的
3.ClassLoader的使用说明
ClassLoader是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
4.双亲委派机制
Java虚拟机对class文件采用按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用双亲委派机制,即把请求交由父类处理,它是一种任务委派模式
一、工作原理
- 如果一个类收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
二、优势
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
5.沙箱安全机制
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制
- 在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须一致,包括包名
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
第三章 运行时数据区及线程
1.运行时数据区
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁
程序计数器、本地方法栈、虚拟机栈是一个线程对应一份,方法区、堆一个虚拟机实例对应一份
每个JVM只有一个Runtime实例,即为运行时环境,相当于内存结构的中间部分——运行时数据区
//每个栈帧对应一个方法
2.线程
- 线程是一个程序里的运行单元,JVM允许一个应用程序有多个线程并行执行
- 在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射
- 当一个Java线程准备好执行以后(每个线程有栈、本地方法栈、PC),此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收
- 操作系统负责所有线程的安排调度到任何一个可用的CPU上,一旦本地线程初始化成功,它就会调用Java线程中的run()方法
第四章 程序计数器(pc寄存器)
PC寄存器用来存储指向下一条指令的地址,即将要执行的指令代码,由执行引擎读取下一条指令
- 它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域
- 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;如果是在执行native方法,则是未指定值(undefined)
- 它是唯一一个在JVM虚拟机规范中没有规定任何OutOfMemoryError情况的区域
- 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
1.相关面试题
一、使用PC寄存器存储字节码指令地址有什么用呢?(为什么使用PC寄存器记录当前线程的执行地址呢?)
因为CPU需要不停的切换各个线程,这时候切换回来后,就得知道从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条该执行什么样的字节码指令
二、PC寄存器为什么会被设定为线程私有?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程不会互相干扰
第五章 虚拟机栈
1.虚拟机栈概述
一、基本内容
- 栈时运行时的单位,而堆是存储的单位
- Java虚拟机栈,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应一次次的Java方法调用。其生命周期与线程一致,主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回
- 栈不存在垃圾回收问题,但存在OOM
- Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverFlowError异常
- 如果Java虚拟机可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存区创建对应的虚拟机栈,那么会抛出一个OutOfMemoryError异常
2.栈的存储单位
一、栈中存储什么?
- 每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在
- 在这个线程正在执行的每个方法都各自对应一个栈帧
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
二、栈运行原理
- JVM直接对Java栈的操作只有两个,就是对栈帧的入栈和出栈
- 在一条活动的线程中,一个时间点上,只会有一个活动的栈帧,即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧,与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
- 如果在该方法中调用了其它方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧
- 不同线程中所包含的栈帧是不允许相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
- Java方法有两种返回函数的方法,一种是正常的函数返回,使用return指令;另外一种是抛出异常,不管使用哪种方式,都会导致栈帧被弹出
三、栈帧的内部结构
每个栈帧中存储着:
- 局部变量表
- 操作数栈(或表达式栈)
- 动态链接(或指向运行时常量池的方法引用)
- 方法返回地址(或方法正常退出或者异常退出的定义)
- 一些附加信息
3.局部变量表
一、局部变量表结构的认识
- 局部变量表定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型、对象引用、以及returnAddress类型
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不会存在数据安全问题
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表大小的
- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增多,进而函数调用就会占用更多的栈空间,导致嵌套调用次数减少
- 局部变量表中的变量只在当前方法中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也销毁
二、关于slot的理解
- 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
- 局部变量表最基本的存储单元是Slot(变量槽),32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot,引用类型也占32位
- byte、short、char、float在存储前被转换为int,boolean也被转换为int,0false1true
- JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上
- 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照顺序继续排列
- 栈帧中的局部变量表的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后声明的新的局部变量就很可能会复用过期局部变量的槽位,从而达到节省资源的目的
三、静态变量与局部变量对比
复习变量的分类:
- 按照数据类型分:
- 基本数据类型
- 引用数据类型
- 按照在类中的声明位置分:
- 成员变量:在使用前,都经历过默认初始化赋值
- 类变量(static):Linking的prepare阶段,给类变量默认赋值——>initial阶段,给类变量显式赋值
- 实例变量:随着对象的创建,在堆空间中分配实例变量空间,并进行默认赋值
- 局部变量:在使用前,必须进行显式赋值,否则编译不通过
- 成员变量:在使用前,都经历过默认初始化赋值
4.操作数栈
- 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈\出栈
- 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
- 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之创建出来,这个方法的操作数栈是空的
- 每一个操作数栈都会有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack值
- 操作数栈并非采用访问索引的方式来进行数据访问的(栈——push\pop)
- 如果被调用方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中
- 我们说Java虚拟机的解释引擎基于栈的执行引擎,其中的栈指的是操作数栈
5.代码追踪
public class OperStackTest{
public void testAddOperation(){
byte i = 15;
int j = 8;
int k = i + j;
}
}
在刚调用这个方法时,操作数栈和局部变量表都是空的,PC寄存器记录下一步要执行的指令地址0
执行0,把byte型15压入操作数栈,紧接着PC寄存器的下一步执行指令地址为2
执行2,istore_1,byte被转换为int,把15出栈,放到局部变量表索引为1的位置(0位置存储的是this),PC寄存器记录指令3
执行3,把8压入操作数栈,紧接着执行5,把8出栈,放到局部变量表索引为2的位置
接下来执行6、7,iload_1、iload_2,把局部变量表中索引为1和2的数据依次压入操作数栈
下面执行8,iadd操作,把上一步操作数栈中的15和8出栈,执行加运算。(iadd这个字节码指令,需要执行引擎把它翻译成机器指令,然后CPU进行运算),然后把结果23再压入操作数栈
再把23出栈,放到局部变量表索引为3的位置,最后return结束
6.栈顶缓存技术
通常一条指令包括两方面的内容:操作码和操作数,操作码决定要完成的操作,操作数指参加运算的数据及其所在的单元地址。零地址指令只有操作码,没有操作数
基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作必然需要使用更多的入栈出栈指令,同时意味着需要更多的指令分派次数和内存读写次数
由于操作数是存储在内存中的,因此频繁的执行内存读写必然影响执行速度。为了解决这个问题,HotSpot JVM设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,依次降低对内存的读写次数,提升执行引擎的执行效率
7.动态链接(指向运行时常量池方法的引用)
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的是为了支持当前方法的代码能够实现动态链接
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。比如:描述一个方法调用了另外的方法,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是将这些符号引用转换为调用方法的直接引用
- 常量池的作用,就是为了提供一些符号和常量,便于指令的识别
8.方法的调用:解析与分派
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
- 静态链接:当一个字节码文件被装载进JVM中时,如果被调用的目标方法在编译期可知,且运行期保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接
- 动态链接:如果被调用的方法在编译器无法确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种转换过程具备动态性,因此称为动态链接
一、虚方法与非虚方法
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法。如:静态方法、私有方法、final方法、实例构造器、父类方法
- 其他方法称为虚方法
二、方法调用指令
- 普通调用指令
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用<init>方法、私有方法及父类方法,解析阶段确定唯一方法版本
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
- 动态调用指令
- invokedynamic:动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中,invokestatic指令和invokespecial指令调用的方法为非虚方法,其余的(final修饰的除外,final给算到invokevirtual中了)称为虚方法
三、Java语言方法重写的本质
- 当调用一个对象的方法时,首先将该对象压入操作数栈,需要找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C
- 如果在类型C中找到与常量中的描述简单名称相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常
- 如果没找到,按照继承关系从下往上依次对C的各个父类进行上一步的搜索和验证过程
- 如果始终没找到合适的方法,则抛出java.lang.AbstractMethodError异常
四、虚方法表
- 从重写本质可知,当调用某个对象的方法时,会依次对其父类进行搜索。如果每次调用都进行这样的操作,会影响执行效率。可在方法区建立一个虚方法表,使用索引表来代替查找
- 每个类中都有一个虚方法表,表中存放着各个方法的实际入口
- 虚方法表在类加载的链接阶段(解析)创建并初始化
9.方法返回地址(return address)
- 一个方法的结束,有两种方式:
- 正常执行完成
- 出现未处理的异常,非正常退出
- 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而异常退出时,返回地址要通过异常表来确定,栈帧中一般不会保存这部分信息
10.一些附加信息
没什么好说哒
第六章 本地方法接口与本地方法栈
1.本地方法接口
简单地讲,一个Native Method就是一个Java调用非Java代码的接口
标识符native可以与所有其他的Java标识符连用,abstract除外
2.本地方法栈
- 本地方法栈用于管理本地方法的调用
- 本地方法栈也是线程私有的
- 本地方法是C语言实现的
- 具体做法是本地方法栈中登记native方法,在执行引擎执行时加载本地方法库
- 允许被实现成固定或者可动态扩展的内存大小
- StackOverflowError
- OutOfMemoryError
来源:CSDN
作者:cszjeveryday
链接:https://blog.csdn.net/weufengwangshi_/article/details/104084779