Java Virtual Machine
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现,java虚拟机有自己完善的硬件架构,如处理器,堆栈,寄存器等,还具有相应的指令系统

一次编译,到处运行

Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行

开篇

程序在执行之前,先要把Java代码编译成字节码(.class文件),JVM首先需要把字节码通过类加载器把文件加载到内存中的运行时数据区中。因为字节码文件是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要执行引擎将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用本地库接口来实现整个程序的功能。

完整构成

基本构成

类加载器-ClassLoader

ClassLoader 负责加载字节码文件即 class 文件

魔数:0X CA FE BA BE()
如果一个文件不是以0xCAFEBABE 开头的,那它肯定不是java class文件

ClassLoader 只负责class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。

执行引擎-Execution Engine

执行引擎,也叫 Interpreter。Class 文件被加载后,会把指令和数据信息放入内存中,Execution Engine 则负责把这些命令解释给操作系统,即将 JVM 指令集翻译为操作系统指令集。

本地接口-Native Interface

本地接口的作用是融合不同的编程语言为Java 所用,初衷是融合C/C++ 程序

运行时数据区-Runtime Data Area

Runtime Data Area 是存放数据的,分为五部分:Stack(虚拟机栈),Heap(堆),Method Area(方法区),PC Register(程序计数器),Native Method Stack(本地方法栈)

线程私有的内存区域

程序计数器(Program Counter Register)

它是一块较小的内存空间(可能位于cpu的寄存器,有待确认),可以看做是当前字节码指令执行的行号指示器,记录了当前正在执行的虚拟机字节码指令地址,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。

虚拟机栈(VM Stack)

虚拟机栈也是线程私有的,它描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。

  1. StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度,特别是方法的递归调用时(有空程序模拟一下)
  2. OutOfMemoryError:虚拟机栈无法满足线程所申请的空间需求,即使经过动态扩展仍然无法满足时抛出(有空程序模拟一下)

本地方法栈(Native Method Stack)

本地方法栈与虚拟机栈相似,区别是虚拟机栈为执行 Java 方法服务,而本地方法栈则为 Native 方法服务,有些虚拟机将这两个区域合二为一。

  • 在Execution Engine执行引擎执行时,通过Native Interface 本地接口调用已登记的native library
  • 本地方法栈中抛出异常的情况与虚拟机栈相同,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常

使用场景:与一些底层系统如操作系统或某些硬件交换信息,如打印机

线程共享的内存区域

堆(Heap)

对于java应用程序来讲,堆是jvm所管理的内存中最大的一块。是被所有线程共享的一块区域,并在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,java程序里“几乎”所有的对象实例都会在这里创建并分配内存。

  • 所有的对象实例以及数组都应当在堆上分配

堆也是垃圾收集器所管理的主要区域,因此很多时候也被称作GC堆

从内存回收的角度来看

由于现在收集器基本都采用分代收集算法,因此堆还可以被细分为:新生代和老年代

从内存分配的角度来看

线程共享的堆中还可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)

  • 堆的空间大小不满足时将抛出OutOfMemoryError异常
    方法区(Method Area)用于存储已被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等数据。

在方法区中有一部分区域用来存储编译期产生的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。这里需要说明一点,常量并不是只能在编译期产生,运行期间也会产生新的常量并被发在常量池中,如 String 类的 intern() 方法。

直接内存

注意直接内存不属于虚拟机运行时数据区的一部分,也不是 JVM 规范中定义的内存区域,其主要用于 JDK1.4 引入的基于通道(Channel)和缓冲区(Buffer)的 NIO 类,可以避免在 Native 堆和 Java 堆之间来回复制数据从而提高性能。

栈帧构成(Stack Frame)

一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现

每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息,方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程

局部变量表(Local Variable Table)

局部变量表(Local Variable Table)是一组变量值存贮空间,用于存放方法参数和方法内定义的局部变量

变量槽(Variable Slot)

每个变量槽都可以存储32位长度的内存空间,例如boolean、byte、char、short、int、float、reference和returnAddress

操作数栈(Operand Stack)

操作数栈(Operand Stack)也常称为操作栈,是一个后入先出栈。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的

jvm对操作数栈的优化

在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递
操作栈调用其它有返回结果的方法时,会把结果 push 到栈上(通过操作数栈来进行参数传递)

动态链接(Dynamic Linking)

每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)

返回地址(Return Address)

当一个方法开始执行以后,只有两种方法可以退出当前方法

  1. 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
  2. 当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。

当方法返回时,可能进行3个操作

  1. 恢复上层方法的局部变量表和操作数栈
  2. 把返回值压入调用者调用者栈帧的操作数栈
  3. 调整 PC 计数器的值以指向方法调用指令后面的一条指令
    附加信息(Additional information)虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。