JVM

小龙 827 2020-04-17

Java虚拟机是进行Java程序执行的唯一通道,现在的Java程序本质上是属于虚拟机语言,所有的语法都按照虚拟机的标准进行编写,在执行的时候由虚拟机负责与操作系统底层进行交互。

一、Java的体系结构

1.1、java体系描述

Java体系包括四个方面

  1. Java编程语言,编辑的文件为*.java的源码
  2. Java类文件格式,javac编译后的文件格式为(*.class)字节码文件
  3. Java虚拟机,既通讯说的JVM
  4. Java应用程序接口(Java API)

其四者的运行关系如下:

  1. Java编译环境:程序员利用Java编程语言编写Java源代码(.java文件),利用Java编译器,将其编译为Java字节码文件(.class文件);
  2. Java平台运行环境:Java字节码被装载进Java虚拟机,被解释器(或者被即时编译器)解释执行,最终变为机器码进行处理。

1.2、Java平台描述

Java平台有Java虚拟机和Java应用程序接口构建,只要符合Java字节码 编译规则的程序均可以运行到Java平台上,包括应用程序和小应用程序;

jvm2.png

JVM承接程序和底层操作系统,它实现了程序和操作系统的分离,是两者无关联的关键,通过JVM,实现Java的平台无关性;

  1. JVM的下方是移植接口,移植接口由两部分组成:适配器(依赖于平台部分)和Java操作系统,JVM通过移植接口在具体的平台和操作系统上实现
  2. JVM的上方是Java的基本类库和API,利用Java API编写的应用程序可以在任何Java平台上运行而无需考虑底层平台

二、JVM体系结构

2.1、JVM基本概念

(1) 基本概念:
JVM是可运行Java代码的家乡计算机,其包括一套字节码指令、一组寄存器、一个栈、一个堆、一个垃圾收集器和一个存储方法域。JVM是运行在操作系统之上的,它与硬件没有直接的交互。
(2) 运行过程
Java源代码文件通过编译器能够编译成相应的*.class文件,也就是字节码文件,而字节码文件又通过虚拟机中的解释器,编译成特定机器上的机器码。

Java源文件 ——> 编译器 ——> 字节码文件
字节码文件 ——> JVM ——> 机器码

不同平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是Java为什么能够跨平台的原因了,当运行一个程序就会实例化一个虚拟机,启动了多个程序就会存在多个虚拟机实例。当程序退出或者关闭了,虚拟机实例就会消亡,不同的虚拟机实例之间数据不能共享。
(3) Java的三种JVM

  1. Sun公司的HotSpot
  2. BEA公司的JRockit
  3. IBM公司的J9 JVM

在JDK及其以前java中使用的是SUM公司的HotSpot,但是Sun和BEA被Oracle收购之后,Oracle将HotSpot和JRockit虚拟机的优势结合,形成了JDK1.8以后的JVM

2.2 Jav程序的执行流程

jvm4.png

Java本身属于编译型和解释型的编程语言,所以所有的“.java”原文件都必须通过Java虚拟机编译工具编译为“.class”字节码文件,随后再Java虚拟机上进行解释执行。

Java虚拟机如果要想进行“*.class”Java程序类的加载,那么就必须依靠ClassLoader(类加载器)来完成,在ClassLoader中默认的加载路径为程序类所在的目录,在使用ClassLader类进行类加载的时候采用的全部都是字节数据的形式,利用字节输入流的方式实现字节码文件的加载

当程序代码加载到JVM之中后会通过运行时数据区为该程序文件里的变量进行相应的内存空间的开辟,随后在JVM里面会依据此运行时数据区进行所需要数据内容的加载,在程序运行的时候除了程序的执行引擎要负责程序代码的执行之外,还有可能会牵扯到一些本地方法的调用,所有的本地方法在Java里面全部使用native关键字来进行定义,而其实现是通过操作系统本身的底层函数来完成的,这一个由JVM负责协调调度。

所以可以发现,在整个的Java虚拟机里面,是一个将JVM的代码与本地代码混合的步骤,由于不同的操作系统针对同一功能有不同的实现,所以考虑到所有系统的适配性,那么会在不同的平台上提供各种不同的JVM。

2.3 JVM运行原理

通过分析可以发现,实际上对于Java虚拟机的大部分操作都不要特别关注,关键性的问题在于运行时数据区的结构上面。
jvm6.png

Java字节码文件被类加载器装载进内存里,被执行引擎解释执行,最终变为机器码进行处理,在不同的JDK版本中,ClassLoader是有所不同的:

  • JDK 1.9 以前:AppClassLoader(应用类加载器)、ExtClassLoader(扩展类加载器)、Bootstrap(引导类加载器)
    |- ExtClassLoader:JDK-JRE目录下面最早有一个ext的目录,这个目录可以直接保存第三方**.jar**文件,从JDK 1.9开始考虑到安全性的问题,将此目录移除了;
  • JDK 1.9 开始:AppClassloader、PlatformClassLoader(平台类加载器)、Bootstrap

本地方法接口(Native Interface)

  • 保存以native关键字定义的方法,这些方法使用C/C++编写,在内存中专门开辟了一块区域处理这些native关键字标记的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine(解释器)执行的时候加载native libraies;native的方法可以直接与本地硬件交互。

执行引擎(Execution Engine)

  • 执行包在装载类的方法中的指令,也就是方法。这里包括了即时编译器和垃圾收集器(GC)
  • 解释器能够快速的解释字节码,但是执行却很慢。解释器的确定就是当一个方法被多次调用,每次都需要重新解释
  • JIT(即时编译器)消除了解释器的缺点。执行引擎利用解释器转换字节码,但如果出现重复代码时则使用JIT即时编译器将全部字节码编译成机器码。机器码直接用于重负调用方法,提高系统的性能。

运行时数据区(Runtime Data Area)

  • 虚拟机内存或JVM内存,从整个计算机内存中开辟一块存储存储JVM需要到的对象、变量等。
  • 运行时数据区分为几个小区:方法区(Method Area)、虚拟机栈(Stack)、本地方法栈(Native Method Stack)、堆(Heap)、程序计数器(Program counter Register)。

注意栈是运行时的单位,而堆是存储单元
因为是运行单位,里面存储的信息都是跟当前线程(或程序)相关的信息。包括局部变量、程序运行状态、方法返回值等
只是保存对象信息

2.4 JVM内存溢出问题

jvm7.png

涉及到内存的操作那么就首先必须考虑代码之中可能出现的OOM问题,实际上OOM分为两种情况:栈内存溢出(StackOverflowError)、堆内存溢出(OutOfMemoryError)

  • 使用递归不当就会出现栈内存溢出(StackOverflowError)
  • 创建的强引用对象太多就会出现堆内存溢出(OutOfMemoryError)

2.5 JVM运行时数据区定义

程序计数器

  • 所谓的程序计数器是一块小到不能再小的内存区域,该区域的主要作用就是对所有代码执行顺序进行一个编号的定义,在执行的时候按照编号的顺序来执行,在多线程情况下挂起的线程恢复执行也是依靠程序计数器来完成。
  • 用来指示执行那条命令。是一块很小的内存区域,小到可以忽略不计;
  • 在程序运行时,诸如循环、跳转、异常处理这些功能都必须依赖于字节码来完成
  • 在程序编译时会为字节码的每一行分配一个程序计数器;
    • 多线程并行执行的时候,每一个线程对象都会由自己独立的程序计数器,以保证CPU切换后可以取得当前程序的执行位置。
  • 在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中保存的值是undefined。
  • 由于程序计数器中存储的数据所占用的空间大小不糊随着程序的执行而发生改变,所以对于程序计数器是不会发生内存溢出现象的。

除了程序计数器之外,堆内存与栈内存中都有可能发生数据溢出问题,之所以所有的递归操作在执行的时候有可能产生栈溢出的问题,所以在项目编写之中往往都会使用while循环来代替递归操作(Java类集框架之中的一些列操作机会都是通过while循环的方式来完成的结构定义)

Java虚拟机栈(Java Virtual Machine Stacks)

  • 栈内存是线程私有的,其生命周期和线程相同,线程创建分配栈,先后消亡回收栈。
  • 栈是一个先进后出的队列
  • 虚拟机栈描述的是Java方法执行的内存模型:执行一个方法是会产生一个栈帧,保存在栈(压栈)的顶部,方法执行完毕后会自动将此栈帧进行出栈。最顶部的栈就表示当前的方法
    • 如果请求的栈的深度过大,虚拟机可能会抛出StackOverflowError异常
    • 如果虚拟机的实现中运行虚拟机栈动态扩展,当内存不足以扩展栈的时候,会抛出OutOfMemoryError异常
      jvm8.png

Java虚拟机提供有Stack栈结构,栈结构分为栈顶和栈底,所有的数据在进行保存的时候是通过栈顶压入栈中,而后通过栈顶进行出栈,所以使用的是先进后出(FILO)的结构。

public class demo {
	public void print(){
		// 递归调用,没有结束条件
		print();
	}
	public static void main(String[] args) {
		print();
	}
}

jvm9.png

在进行方法递归操作的过程之中,会将所有递归操作的结构保存在一个栈内存空间,那么在这样的情况下,如果要进行入栈的内容过多了,超过了预设的阈值,那么就会出现栈溢出的情况,但是如果没有溢出,只是某些 方面进行互相调用那么栈顶描述的就是当前方法。

public class demo {
	public void print(){}
	public void run(){
		print();
	}
	public static void main(String[] args) {
		run();
	}
}

jvm10.png
Java虚拟机栈 —— 栈帧

  • Java虚拟机栈会存放的是多个栈帧,栈帧包括以下的主要组成部分:
    |- 局部变量表(Local Variables):方法的局部变量或形参,其以变量槽(solt)为最小单位,只允许保存32位长度的变量,如果超过了32位,则会开辟两个连续的solt(64为长度,long和double);
    |- 操作数栈(Operand Stack):表达式计算在栈中完成;
    |- 指向当前方法所属类的运行时常量池的引用(Reference to runtime constant pool):引用其他类的常量或者使用String常量池中的字符串
    |- 方法返回地址(Return Address):方法执行完毕后需要返回调用此方法的位置,所以需要在栈帧中保存方法的返回地址

本地方法栈

  • 本地方法栈的功能与Java栈的功能类似,唯一的区别在于本地方法栈是为本地方法(Native Method)服务的。

栈内存描述的是每一个线程独享的操作区域,每一个线程都通过自己的栈内存实现相应的操作记录,所有的记录都依据FILO原则进行处理。
方法区
方法区市整个JVM中非常重要的一块内存,此块区域是所有线程对象共享的区域。在方法区中保存了每一个类的信息(类名称、方法信息、成员信息、接口信息等)、静态变量、常量、常量池信息。一般而言在方法区中很少执行垃圾收集操作。
jvm11.png

所有的Java垃圾收集的处理操作是针对堆内存空间进行的操作

  • 堆内存主要保存具体的数据信息,在JVM启动的时候将被自动创建。此内存空间为所有线程对象共享区域,但是在Java开发之中,开发人员可以不用去处理此区域的空间释放,会由垃圾收集器自动进行释放,所以此空间为垃圾收集主要管理区域。
  • 直接内存并不会受到JVM的控制,直接内存指的是在虚拟机之外的主机内存(例如:电脑上有8G内存,分配给虚拟机2G,那么直接内存就是6G)。在JDK之中提供有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,会将native函数库(c语言实现)分配在直接内存,并通过存储在JVM堆内存中的DirectByteBuffer来引用。由于直接内存会受到本机系统内存的限制,所以也有可能出现“Java.lang.OutOfMemoryError”错误

所有的程序都在运行时数据区中进行各种加载与处理结果的保存;