JVM-Java虚拟机

概念

  虚拟机(Virtual Machine指通过软件模拟物理机器,使其具有真实机器所具有的功能。根据应用范围与机器的相关性可以分为:

  • 系统虚拟机,提供可以运行完整操作系统的平台,如VirtualBox、VMware等
  • 程序虚拟机,为运行单个计算机程序设计,支持单个进程,如JVM、Dalvik(运行安卓)等

  JVM主要有以下三个层面的相关概念:

  • Specification:JVM的规范,包含定义以及架构设计。
  • Implementation:JVM的具体实现,任何一个供应商都可以开发自己的虚拟机,但是需要遵循JVM规范。市场上有很多种JVM,主流的有Oracle HotSpot JVM、IBM JVM,包括安卓操作系统中的Dalvik也是一种JVM,但是Dalvik并没有实现JVM的规范,它是基于寄存器的虚拟机,而JVM规范要求基于堆栈。字节码不能直接在Dalvik虚拟机中运行。
  • Instance:每启动一个Java程序,伴随着生成一个JVM实例,两者具有相同的生命周期,Java程序结束,JVM实例也随之结束。

JVM架构


image.png | center | 640x507

  整体上来看,可以把JVM分为三部分:类加载、运行时数据区、执行引擎。

Class Loader Subsystem

  Class文件不能被机器直接执行,在Class文件中描述的各种信息,最终都需要加载到JVM中才可以运行和使用。将描述类的数据从Class文件加载到内存,最终形成可以被JVM直接使用的Java类型,这个过程由类加载子系统(Class Loader Subsytem)来完成。
  JVM的类加载子系统不是在编译时加载类,而是在第一次运行引用一个类时,才进行类的加载、链接、初始化,由此来实现动态类加载的功能。

  在了解类加载细节之前,先来看一下类的生命周期。


image.png | center | 641x208

  一个类从加载到内存中,到最终的卸载,它的生命周期共有七个阶段,其中解析阶段可以和初始化阶段调整顺序从而来实现动态绑定。对于JVM的类加载机制,整体上可以看做三个阶段:Loading(加载)、Linking(连接)、Initializing(初始化)。

Loading

  加载只是类加载的一个阶段,可以翻译成“装载”来区分。Loading阶段的主要功能:

  • 获取类的二进制字节流,Class文件字节流的获取方式有很多种:

    • 本地磁盘直接加载Class文件
    • 从Zip压缩包中获取,JAR、EAR、WAR格式的基础
    • 通过网络下载,最典型的应用是 Applet
    • 运行时计算生成,这种场景使用得最多得就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理类的二进制字节流
    • 由其他文件生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类
    • 从专有数据库中读取
  • 将获取到的字节流中的静态存储结构转化为方法区的运行时存储结构。

  • 在内存中创建一个此类的java.lang.Class的对象(没有明确规定在Java堆中,HotSpot虚拟机比较特殊,Class对象存放在方法区中),作为方法区这个类的各种数据的访问入口。

  获取类的二进制字节流是通过类加载器(class loader)来实现的。对于任意一个类,都需要由加载它的类加载器以及其本身一同确立在JVM中的唯一性,每一个类加载器都拥有一个独一无二的类名称空间。只有由同一个类加载器加载,两个类是否“相等”才有意义。

  从JVM的角度来看,类加载器有两种:一是启动类加载器(Bootstrap Class Loader);二是其他类加载器。从程序员的角度来看,划分更为细致:

  1. 启动类加载器(Bootstrap Class Loader),JVM自身的一部分,使用C++实现,运行JVM时创建,加载Java API,比如lib目录下的rt.jar。
  2. 扩展类加载器(Extension Class Loader),加载\lib\ext目录中,标准API之外的扩展类。
  3. 系统类加载器(System Class Loader),也叫应用程序类加载器,负责加载用户类路径(ClassPath)上的指定类库,是程序中默认的类加载器。
  4. 用户自定义类加载器(User-defined Class Loader),如果前三种类加载器不能满足用户的需要,可以在程序中自定义类加载器。


image.png | center | 409x455

  如上图所示,这几种类加载器之间的层次关系称为类加载器的双亲委派模型(Parents Delegation Model),除去顶层的Bootstrap Class Loader,每一个类加载器都需要有自己的父类加载器,这里的父子关系通常使用组合(Composition)关系来实现,而非继承(Inheritance)。
  一个类加载器接受到类加载的请求,先不自己加载,而是把请求委派给父类加载器处理,如此类推,直到顶层的类加载器(Bootstrap Class Loader),如果父类的无法完成加载请求,子类才会去尝试。


image.png | center | 495x238

  类加载的查找顺序为:用户自定义->系统->扩展->启动,即使用户在自己的代码中添加了名为java.lang.Object的类,根据双亲委派模型,最终被JVM加载到内存的是\lib\rt.jar类库中的标准Object类,而不是用户自己添加的Ojbect类。

Linking

  • 验证(Verifying):连接阶段的第一步,验证Class文件中的字节流是否符合规范,防止恶意代码共计,保证JVM的安全。验证是整个类加载中最复杂的测试过程,花费较长的时间。

  • 准备(Preparing):为类变量分配内存并设置初始值,所使用的内存都在方法区中分配。比如

    public static int value = 100;

  准备阶段完成后,value的值为int类型的默认值0,而不是100。

  • 解析(Resolving):JVM将常量池内的符号引用替换为直接引用。

符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。
直接引用(Direct References):直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

  在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。解析阶段则是把符号引用替换为真实的地址,如同DNS中域名解析为ip地址一样。

Initializing

  初始化是类加载的最后一个阶段,JVM规范规定了在以下情况下必须立即对类进行初始化(加载、连接仍然需要在此之前开始)。

  1. 使用new关键字实例化对象
  2. 读或者写一个类的静态字段(static修饰,同时被final修饰的除外)
  3. 调用类的静态方法
  4. 使用java.lang.refelct包的方法对类进行反射调用
  5. 初始化一个类时,如果父类没有初始化,先触发父类的初始化
  6. 虚拟机启动时,用户需要指定一个要执行的主类(文件名与类名相同的那个类),虚拟机会先初始化这个主类
  7. 当使用 JDK.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

  在初始化阶段,才开始真正执行类中定义的Java代码,比如上面提到在准备阶段中value被赋予int类型的默认值0,初始化阶段value的值会变成100(真实赋值)。类的构造方法在初始化阶段执行。

Runtime Data Area


image.png | center | 444x130

  JVM在执行Java程序的过程中会把它管理的内存分为几个不同的数据区域,称为运行时数据区域。整体上Runtime Data Area可以分为5部分:方法区(Method Area)、堆(Heap Area)、栈(Stack Area)、程序计数器(PC Register)、本地方法栈(Native Method Stack)。


image.png | center | 492x634

  这五个区域,其中堆、方法区为线程之间共用,程序计数器、JVM栈以及本地方法栈为各个线程独有。

  • 程序计数器(Program Counter Register):一个线程拥有一个程序计数器,在线程开始时创建,为线程私有,保存线程正在执行的JVM指令地址。

  • Java虚拟机栈:与线程的生命周期相同,同样也为线程私有,描述Java方法执行的内存模型。栈中存放的基本单位为栈帧(Stack Frame),每一个方法从调用到完成的过程就对应一个栈帧在JVM栈中入栈到出栈的过程。


image.png | center | 536x278

  栈帧在方法执行的时候被创建并压入JVM栈,方法结束后栈帧出栈被移除。每个栈帧中保存了局部变量数组(Local Variable Array)、操作数栈(Operand Stack)以及常量池引用(Reference to Constant Poll)。

  • 本地方法栈:与JVM栈功能类似,JVM栈执行的是Java方法,而本地方法栈为虚拟机使用到的本地方法。换句话说,本地方法栈通过JNI(Java Native Interface)来执行C/C++代码调用。实际上在JVM规范中,没有强制规定本地方法栈中方法使用的语言、使用方式与数据结构,具体的虚拟机可以自由实现,比如HopSpot虚拟机就把本地方法栈和虚拟机栈合二为一。

  • :所有线程共享,虚拟机启动时创建,可以看做为JVM进程所管理的内存区域。堆主要存放对象实例,是垃圾收集(Garbage Collection)的主要目标,很多时候也被叫做“GC堆”。当讨论到JVM性能相关的问题时,堆是最容易被提到的内存区域。

  • 方法区:同样为所有线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量等数据。方法区中还有一个运行时常量池(Runtime Constant Pool),用来存放编译期生成的各种字面量(literal)和符号引用(上文有提到)。

    public static final int value = 100; //value为常量,100为字面量

Execution Engine

  执行引擎是JVM最为核心的部分之一。物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而JVM的执行引擎是自己实现的,可以自己制定指令集,能够执行不被硬件直接支持的指令集格式。总而言之,类加载完成后,保存在运行时数据区域中的字节码是由JVM执行引擎来执行的。

  字节码执行可以有两种方式:解释执行、编译执行。

  • Interpreter(解释器):字节码按照执行顺序一条一条的编译、执行。单条字节码指令编译很快,但是整体上程序执行较慢,同一方法多次调用会被多次编译。
  • JIT Compiler:编译整个字节码,编译时间较长,但是执行速度很快,可以与解释器互补。

  如果代码仅执行一次,解释执行比编译执行更有优势,反之编译执行效率更高。

参考资料

  1. The Java® Virtual Machine Specification
  2. Memory Architecture Of JVM(Runtime Data Areas)
  3. Understanding JVM Internals
  4. Java Class Loading and Distributed Data Processing Frameworks
  5. 深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)-周志明

猜你喜欢

转载自blog.csdn.net/u013201439/article/details/80215405