JVM

329 阅读14分钟

虚拟机

何为虚拟机?

一款模拟计算机的软件,可以执行一系列虚拟的计算机指令。

分类

系统虚拟机

对物理计算机的仿真,提供了一套可以完整运行操作系统的软件平台

程序虚拟机

专门用于执行某个计算机程序而设计的软件,如JVM执行的字节码就是它的指令

Java虚拟机

是什么?

Java虚拟机是一台运行java字节码的虚拟计算机,它能够独立运行,运行的java字节码不一定由java语言编译而成,只要符合JVM规范即可执行

有什么用?

Java虚拟机是java字节码的运行环境,JVM负责将字节码加载到JVM内部,解释或者编译为对应平台上的机器指令。每一条java指令,在JVM规范中都有明确详细的定义,比如如何取数,如何处理操作数,结果放在哪里等

特点

  1. 一次编译到处执行
  2. 自动的内存管理
  3. 自动垃圾回收器

JVM在计算机体系中的位置

图片.png JVM在操作系统之上,与硬件没有直接的交互

JVM的整体结构

HotSpot 的整体结构

图片.png

Java代码执行流程

图片.png

JVM 的架构模型

Java 编译器输入的指令流有两种

基于栈的指令集架构

  • 设计和实现简单,适用于资源受限的系统
  • 避开了寄存器分配难题;使用零地址指令方式分配
  • 指令流的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器更容易实现
  • 不需要硬件支持,可以移植性更好

基于寄存器的指令集架构

  • 性能优秀
  • 指令集架构依赖于硬件,可移植性差
  • 花费更少的指令取完成一项操作

HotSpot VM 采用的是指令集架构 总结: 跨平台性、指令集小、指令多、执行性能比寄存器架构差

JVM 的生命周期

启动

通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成,这个类由虚拟机的具体实现决定。

执行

  1. 程序开始执行时财运行,程序结束时他就停止
  2. 执行一个 Java 程序的时候,真真正正在执行的时一个叫做 Java 虚拟机的进程。

结束

  1. 程序正常执行结束
  2. 程序在执行过程中遇到了异常或者错误导致异常终止
  3. 由于操作系统出现错误导致 Java 虚拟机进程终止
  4. 某线程调用 Runtime 类或者 System 类的 exit 方法,或 Runtime 类的 halt 方法,并且 Java 安全管理器也允许这次 exit 或者 halt 操作。

JVM 的发展历程

Sun Classic VM

  1. 1996年 java1.0时 Sun 公司发布,是世界上第一款商用的 Java 虚拟机,JDK1.4时被完全淘汰。
  2. 这款虚拟机内部只提供解释器(执行效率底下)
  3. 如果需要使用JIT 编译器,就需要进行外挂,一旦使用了 JIT 编译器,JIT 就会接管虚拟机的执行系统。解释器就不再工作。 也就是说解释器和编译器不能同时工作。
  4. hotspot 内置了此虚拟机

Exact VM

  1. 解决了上一个虚拟机问题,jdk1.2时提供该虚拟机
  2. Exact Memory Management:准确实内存管理虚拟机可以指导内存中某个位置的数据具体时什么类型
  3. 热点探测
  4. 编译器与解释器混合工作模式 5 只在 solaris 平台短暂使用,其他平台还是 classic vm

HotSpot VM

  1. JDK默认的虚拟机
  2. 通过计数器找到最具编译价值代码,触发即时编译或者栈上替换
  3. 通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡。

JRockit

  1. 专注于服务器端应用
  2. 内部不包含解析器实现,全部都由即时编译器编译后执行。
  3. 世界上最快的 JVM

J9

  1. 与 HotSpot 接近
  2. 在 IBM 自己的产品中性能优秀

类加载子系统

JVM结构图 图片.png

类加载器子系统的作用

  1. 负责从文件系统或者网络中加载 Class 文件,class 文件在文件开头有特定的文件标识。
  2. ClassLoader 只负责 class 文件的加载,至于是否可以运行由执行引擎决定
  3. 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是 Class 文件中常量池部分的内存映射)

类加载的过程

图片.png 字节码文件由JVM(类加载器)加载进运行时数据区中的方法区,此时被加载到JVM的这个字节码文件叫称为DNA元数据模板(元数据),此时调用元数据的.getClassLoader()方法可以得到它对应的类加载器,调用元数据的构造方法可以得到他的实例对象存放到堆空间中,对应的对象调用.getClass()方法可以得到对应的元数据。

类加载过程的三个环节

图片.png

加载

  1. 通过类的权限全限定名获取定义此类的二进制字节流
  2. 将这个字节流中所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口。 加载.class 文件的方式
  • 本地文件
  • 网络环境
  • 压缩包
  • 计算生成(反射)
  • 由其他文件生成(JSP)
  • 从加密文件中

链接

  1. 验证(Verify) 保证字节文件中字节流中包含的信息符合虚拟机的要求,不会危害虚拟机 主要包括了
    • 文件格式验证
    • 元数据验证
    • 字节码验证
    • 符号引用验证
  2. 准备(Prepare) 为该类变量分配内存并且设置默认值
    注意
  • 这里不包含用 final 修饰 static 变量,因为final 在编译时就会分配
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到 Java 堆中
  1. 解析(Resolve)代码中的引用建立连接

初始化

  • 初始化阶段就是执行类构造器方法clinit()的过程
  • 此方法不需要定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
  • 构造器方法中的指令顺序按语句在源文件中出现的顺序执行。
  • clinit()不同于类的构造器,没有静态变量就不会生成该方法(构造器是虚拟机视角下的init
  • 若该类具有父类,JVM 会保证 子类的clinit()执行前,父类的clinit()已经执行完毕。
  • JVM 保证一个类的 clinit()方法在多线程下被同步加锁。 图片.png 之所以可以先赋值再声明,是因为在类加载的准备阶段已经为类变量分配了空间被设定了默认值,在初始化阶段才进行了赋值操作。 图片.png

类加载器的分类

JVM的类加载器支持两种

  • 引导类加载器(BootStrap ClassLoader)
  • 自定义类加载器(User-Defined ClassLoader) 自定义类加载器,JVM 定义所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器。
  • 最常见的类加载器由三种
    • BootStrap ClassLoader
    • Extension ClassLoader
    • System ClassLoader 以上三种加上自定义类加载器之间的关系是包含关系不是上下层关系。也不是继承关系。

不同类所使用的加载器

  • 用户自定义类默认使用系统类加载器进行加载
  • 系统的核心类使用的是引导类加载器进行加载

启动类加载器(引导类加载器,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 语言编写
    • 派生于 ClassLoader
    • 父类加载器为扩展类加载器
    • 它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库
    • 该类加载是程序中默认的类加载器,一般来说,Java 应用的类都是由它完成加载
    • 通过 ClassLoader#getSystemClassLoader()方法可以获取到该类加载器

用户自定义类加载器

为什么需要自定义类加载器?

  • 隔离加载类
  • 修改类加载方式
  • 扩展加载源
  • 防止源代码泄露

用户自定义类加载器实现步骤

  1. 继承抽象类 java.lang.ClassLoader 类,实现自己的类加载器,以满足一些特殊需求
  2. 在 JDK1.2之前,在自定义类加载器时,需要继承 ClassLoder 类并重写 loadClass()方法,但是在1.2之后,跟以前不同,建议把自定义类加载逻辑写在 findClass()方法中
  3. 如果没有过于复杂的需求,可以直接继承 URLClassLoader 类,这样可以避免自己去编写 findClass()方法,以及获取字节流码的方式,使得自定义类加载器编写更加简洁

ClassLoader

抽象类,所有类加载器都继承自 ClassLoader(除了启动类加载器)

获取 ClassLoader 的途径

双亲委派机制

Java 虚拟机对 class 文件采取的是按需加载的方式,当需要使用该类时才会将它的 class 文件加载到内存生成 class 对象。而且加载某个类的 class 文件时,JVM 采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

工作原理 当一个类加载器收到了类加载请求,如果存在父类加载器,就需要将请求委托给父类,如果父类可以完成,就由父类完成,请求完成以后不再下派。如果不能,则请求下派,直至最后自己加载。

示例1 由于双亲委派机制的存在,这里加载的 String 核心 API 中的 String,这其中没有 main 方法

示例2 接口由引导类加载器加载,具体接口的实现类,由系统类加载器加载

双亲委派机制的优势

  • 避免类的重复加载
  • 保证程序安全,防止核心 API 被篡改

沙箱安全机制

通过双亲委派机制,保证 java 核心源代码的保护,就是沙箱安全机制。示例2中的例子即可说明,也就是系统不会因为恶意注入导致崩溃。

常见问题

一. 在 JVM 中表示两个 class 对象是否为同一个类存在的两个必要条件
1. 类的完整类名必须完全一致,包括包名
2. 加载这个类的ClassLoader(指ClassLoader 示例对象)必须相同。
也就是说,JVM 中即使这两个类来源于同一个 Class 文件,被同一个虚拟机所加载,只要它们加载它们的 ClassLoader 不一样,那么这两个对象就不相等。
二. 对类加载器的引用
JVM 必须知道一个类型是由启动加载器还是由用户加载器加载的。如果一个类型是由用户类加载器加载的,那么 JVM 会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用时,JVM 需要保证这两个类型的类加载器是相同的。

Java 程序对类使用方式分为:主动使用和被动使用

它们的区别在于,会不会导致类的初始化
主动使用

  1. 创建类的示例
  2. 访问某个类或者接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射
  5. 初始化一个类的子类
  6. Java 虚拟机启动时被标明为启动类的类
  7. JDK7开始提供动态语言支持: java.lang.invoke.MethodHandle 示例的解析结果

运行时数据区

JVM 定义了若干种程序运行期间会使用到的运行时数据区,其中有一些生命周期和 JVM 相同,另外一些则是与线程一一对应。
每个线程:独立包括,程序计数器,虚拟机栈,本地方法栈
线程间共享:堆、堆外内存(永久代或者元空间、代码缓存)

整体结构

按照线程的使用情况分成两大部分

线程独享区域(程序执行区域)

线程共享区域(程序存储区域)

线程

在 HotspotJvm中,每个线程都与操作系统的本地线程直接映射。当一个 Java 线程准备好后,此时一个操作系统的本地线程也同时创建。Java 线程执行结束后,本地线程也会回收。
操作系统负责所有线程的安排调度到任何一个可用的 CPU 上。一旦本地线程初始化成功,它就会调用 Java 线程中的 run()方法。
在 java 中,即使用户没有定义线程,在运行的过程中也会又许多线程运行,这些线程由 main 线程自己创建,在 HotspotJVM 中主要包括以下几个:

  • 虚拟机线程
  • 周期任务线程
  • GC 线程
  • 编译线程
  • 信号调度线程

程序计数器 (PC Register)

作用:用于存储指向下一条指令的地址

PC 寄存器是什么?

  • 是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域。
  • 在 JVM 的规范中,每个线程都有自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的JVM 指令地址;如果是 native 方法,这是未定值。
  • 程序控制流的指示器,完成分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要被执行的字节码指令
  • 它是唯一一个在 JVM 中没有规定任何 GC、OOM(垃圾回收、溢出)情况的区域

常见问题

  • 使用 PC 寄存器存储字节码地址有什么用呢?
  • 为什么使用 PC 寄存器记录当前线程的执行地址呢?

因为程序在运行的过程中会不停的切换各个线程,切换之后执行引擎需要知道从哪里开始继续执行。 2.

  • PC 寄存器被设定为线程私有? 为了能够记录每个线程正在执行的当前字节码指令地址,因为 CPU 时间片轮转机制,多线程执行的过程中必然经常出现中断和恢复,为了恢复现场,每个线程在创建后都产生自己的程序计数器。

虚拟机栈