第七章 JVM体系结构与工作方式
JVM能跨计算机体系结构来执行Java字节码,主要是由于JVM屏蔽了与各个计算机平台的软件和硬件之间的差异。
7.1 JVM体系结构
7.1.1 何谓JVM
模拟一个计算机来达到一个计算机所具有的计算功能。
以计算为中心来看计算机的体系结构可以分为如下几个部分。
- 指令集 计算机能识别的机器语言的命令集合。
- 计算单元 能够识别并且控制指令执行的功能模块。
- 寻址方式 地址的位数,最小地址和最大地址范围,以及地址的运行规则。
- 寄存器定义 包括操作数寄存器,变址寄存器,控制寄存器等的定义,数量和使用方式。
- 存储单元 能够存储操作数和保存操作结构的单元,如内核级缓存,内存和磁盘等。
每一个汇编语句可以翻译成一条机器指令。
CPU架构是否影响指令集?因为在汇编语言中都是对寄存器和段的直接操作的命令,所以不同的芯片架构设计一定会对应不同的机器指令集合。
JVM和实体机有何区别?
- 一个抽象规范:这个规范约束了JVM到底是什么?有哪些组成部分
- 一个具体实现:不同的厂商按照抽象的规范实现
- 一个运行中的实例:当用其运行一个JAVA程序时,它就是一个运行中的实例,每一个运行中的实例,都是一个JVM实例。
7.1.2 JVM体系结构详解
- 类加载器:JVM启动时或者在类运行时将需要的class加载到JVM种。Class File 是平台无关的二进制文件,包含着能被JVM执行的字节码,其中多字节采用大端序,字符使用一种改进的UTF-8编码。Class文件精确的描述了一个类或接口的信息,其中包括: 常量池:数值和字符串字面常量,元数据如类名、方法名称、参数,以及各种符号引用
- 方法的字节码指令,参数个数,局部变量,最大操作数栈深度,异常等信息
- 执行引擎:执行class文件中包含的字节码指令,相当于实际机器上的CPU。
- 内存区:将内存分成若干个区以模拟实际机器上的存储、记录和调度功能模块,如实际机器上的各种功能的寄存器或PC指针的记录器等。
- 本地方法调用:调用C或C++实现的本地方法的代码返回结果。它的具体做法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库。
1 类加载器:每个被JVM加载的类都有一个对应的java.lang.Class类的实例来表示该类型,该实例可以唯一表示被JVM装载的class类,这个实例在JAVA堆中,
2 执行引擎:JVM核心,作用是解析JVM字节码指令,得到执行结果。JAVA虚拟机规范定义了执行引擎遇到每条字节码指令时应该处理什么,得到什么结果。但是
没有规定用何种方式。所以各JVM厂商自己决定。
执行引擎也就是执行一条条代码的一个流程,代码包含在方法体内,所以执行引擎本质上就是执行一个个方法所串起来的流程。JVM同时运行多个线程,
每个线程就是一个执行引擎的的实例。这些执行引擎有的执行用户程序,有的执行JVM内部程序(垃圾回收)
3 JAVA内存管理:方法区,Java堆,JAVA栈,PC寄存器和本地方法区。其中方法区和JAVA堆是所有线程共享的,可以被所有执行引擎实例共享,每个新的执行引擎实例被
创建的时候会创建JAVA栈和一个PC寄存器。如果当前正在执行一个JAVA方法,那么当前这个JAVA栈中保存的是该线程中方法调用的状态,包括方法参数,局部
变量,方法返回值以及运算的中间结果。PC指向下一条指令。
每个线程都有一个程序计数器pc,是线程私有的,就是一个指针,指向方法区中的方法字节码,有执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以 忽略不计。
(补充:方法区是线程共享的内存区域,主要用来存放:类加载的类型信息、常量、静态变量、即时编译器编译的代码缓存等等。
总结:
1、类型信息:(对应class文件的访问标志(或者说访问修饰符),类的全名(类全名之间用.连接,全限定名用/连接)、父类全名、类的修饰符、实现接口的集合。)
2、运行时常量池(对应class文件的常量池表,包括字面量和符号引用)
3、字段信息(对应class文件的字段表,包括:声明的顺序、字段的修饰符、类型、名称)
4、方法信息(对应class文件的方法表,包括:方法的修饰符、返回类型、参数列表、方法的字节码、操作数栈和局部变量表的大小)
5、static变量(对应class文件的字段表中的静态字段)
当然,上面的内容并非方法区的全部内容,这里只是阐述了一下方法区中部分数据和class文件的对应关系。
7.2 JVM工作机制
JVM如何执行字节码命令的,也就是执行引擎是如何工作的 。
不管是何种指令集都是只有几种基本的元素:加 减 乘 求余 求模等。这些又可以进一步分解成二进制位运算:与 或 异或等。这些运算都是通过指令来完成,而指令的核心目的就是确定需要运算的种类(操作符)和运算需要的数据(操作数),以及从哪里(寄存器或栈)获取操作数,将操作结果放在什么地方(寄存器或栈)。这种不同的操作方式又将指令划分成:一地址指令,二地址指令,三地址指令和零地址指令。相应的指令集会有对应的架构实现,如基于寄存器的架构实现或者基于栈的架构实现,这里的基于什么实现是指在一个指令中的操作数是如何存取的。
7.2.1 机器如何执行代码
JVM执行字节码指令是基于栈的结构, 也就是所有的操作数必须先入栈,然后根据指令中的操作码选择从栈顶弹出若干个元素进行计算后再将结果入栈。
在JVM中操作数可以存放在每一个栈帧的本地变量集中,每个方法在调用时,给分配本地变量集,这个本地变量集在编译的时候就确定了,所以操作数入栈就是常量入栈或者从本地变量集中取变量入栈。这和基于寄存器的操作不同:操作数要频繁的入栈和出栈,比如进行加法,如果两个操作数都在本地变量集中,那么一个加法操作需要5次栈操作;如果是基于寄存器的话,只需要将两个操作数放入寄存器进行加法运算后再将结果放入寄存器就可以。不需要这样多的数据移动操作。那么JVM为什么基于栈来设计呢?
7.2.2 JVM为何选择基于栈的结构
理由:一是JVM要设计成平台无关的,在有很少或没有寄存器的机器上同样可以正确的执行JAVA代码,基于寄存器的架构很难做到通用。
二是为了指令的紧凑性,
补充:这里举一个例子。
解释:Car.class 是由 .java 文件 经过编译而得来的 .class文件,存在本地磁盘; ClassLoader: 类加载器,作用就是加载并初始化 .class文件 ,得到真正的 Class 类,即模板; Car Class : 由 Car.class 字节码文件,通过ClassLoader 加载并初始化而得,那么此时 这个 Car 就是当前类的模板,这个Car Class 模板就存在 【方法区】 car1,car2,car3 : 是由Car模板经过实例化而得,即 new出来的 --> Car car1 = new Car() , Car car2 = new Car() ,Car car3 = new Car() , 因此可知,由一个模板,可以得到多个实例对象,即模板一个,实例多个 所以,拿car1举例,car1.getClass 可以得到其模板Car 类,Car.getClassLoader() 可得到其装载器,这些实例是放在堆中的。
7.2.3 执行引擎的结构设计
每当创建一个新的线程时,JVM会为这个线程创建一个JAVA栈,同时分配一个PC寄存器,并且这个PC寄存器会指向这个线程第一行可执行代码。每当调用一个新方法时,会创建一个新的栈帧结构,栈帧会保存这个方法的一些元信息,如在这个方法中定义的局部变量,一些用来支撑常量池的解析,正常方法返回和异常处理。
常量区: 类型信息 类型的常量池( constant pool)
域(Field)信息
方法(Method)信息
除了常量外的所有静态(static)变量
7.2.4 执行引擎的执行过程
7.2.5 JVM方法调用栈
JVM的方法调用分为两种:一种是JAVA方法调用;另一种是本地方法调用。
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
类的加载过程:JVM将javac编译好的class字节码文件加载到内存中,并对该数据进行验证、解析和初始化、形成JVM可以直接使用的JAVA类,最终回收(卸载)的过程。
字节码(.class)文件来源:
- – 从本地系统中直接加载
- – 通过网络下载.class文件
- – 从zip,jar等归档文件中加载.class文件
- – 从专有数据库中提取.class文件
- – 将Java源文件动态编译为.class文件
1、加载:加载阶段其实就是JVM通过一个类的全限定名来获取其定义的二进制字节流,并将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构且在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。在该阶段我们开发人员可以干预,例如:我们可以指定类加载器来加载该字节数组或者自定义类加载器来加载。
2、链接:将java类的二进制代码合并到JVM的运行状态中的过程
- a、验证:验证是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- b、准备:该阶段是在方法区中为类变量(static变量)分配内存并设置类变量初始值。例如:public static int flag=1;该阶段初始化值为0。
- c、解析:虚拟机将常量池中的符号引用替换为直接引用的过程。(直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄)
3、初始化:初始化为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
- 初始化阶段就是执行类构造器<clinit>()的过程,类构造器<clinit>()是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先初始化其父类。
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
- 当访问一个java 类的静态域时,只有正真申明这个域的类才会被初始化。
4、使用:程序使用JVM加载的类
5、卸载
- 执行了System.exit()方法
- JVM垃圾回收机制触发回收
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止