JVM(Java Virtual Machine(Java虚拟机))
JVM(Java Virtual Machine(Java虚拟机))JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要..
JVM(Java Virtual Machine(Java虚拟机))
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
Java字节码是在JRE中运行(JRE: Java 运行时环境)。JVM则是JRE中的核心组成部分,承担分析和执行Java字节码的工作。
虚拟机
JRE由Java API和JVM组成,JVM通过类加载器(Class Loader)加类Java应用,并通过Java API进行执行。
虚拟机(VM: Virtual Machine)是通过软件模拟物理机器执行程序的执行器。最初Java语言被设计为基于虚拟机器而非物理机器,从而实现WORA(一次编写,到处运行)的目的,尽管这个目标几乎被世人所遗忘。所以,JVM可以在所有的硬件环境上执行Java字节码而无须调整Java的执行模式。
JVM的基本特性:
基于栈(Stack-based)的虚拟机:不同于Intel x86和ARM等比较流行的计算机处理器都是基于寄存器(register)架构,JVM是基于栈执行的。
符号引用(Symbolic reference):除基本类型外的所有Java类型(类和接口)都是通过符号引用取得关联的,而非显式的基于内存地址的引用。
垃圾回收机制:类的实例通过用户代码进行显式创建,但却通过垃圾回收机制自动销毁。
通过明确清晰基本类型确保平台无关性:像C/C++等传统编程语言对于int类型数据在同平台上会有不同的字节长度。JVM却通过明确的定义基本类型的字节长度来维持代码的平台兼容性,从而做到平台无关。
网络字节序(Network byte order): Java class文件的二进制表示使用的是基于网络的字节序(network byte order)。为了在使用小端(little endian)的Intel x86平台和在使用了大端(big endian)的RISC系列平台之间保持平台无关,必须要定义一个固定的字节序。JVM选择了网络传输协议中使用的网络字节序,即基于大端(big endian)的字节序。
Sun 公司开发了Java语言,但任何人都可以在遵循JVM规范的前提下开发和提供JVM实现。所以目前业界有多种不同的JVM实现,包括Oracle Hostpot JVM和IBM JVM。Google公司使用的Dalvik VM也是一种JVM实现,尽管其并未完全遵循JVM规范。与基于栈机制的Java 虚拟机不同的是Dalvik VM是基于寄存器的,Java 字节码也被转换为Dalvik VM使用的寄存器指令集。
Java 字节码
JVM使用Java字节码—一种运行于Java(用户语言)和机器语言的中间语言,以达到WORA的目的。Java字节码是部署Java程序的最小单元。
Java 字节码是JVM的基本元素,JVM本身就是一个用于执行Java字节码的执行器。Java编译器并不会把像C/C++那样把高级语言转为机器语言(CPU执行指令),而是把开发者能理解的Java语言转为JVM理解的Java字节码。因为Java字节码是平台无关的,所以它可以在安装了JVM(准确的说,是JRE环境)的任何硬件环境执行,即使它们的CPU和操作系统各不相同(所以在Windows PC机上开发和编译的class文件在不做任何调整的情况下就可以在Linux机器上执行)。编译后文件的大小与源文件大小基本一致,所以比较容易通过网络传输和执行Java字节码。
Java class文件本身是基于二进制的文件,所以我们很难直观的理解其中的指令。为了管理这些class 文件, JVM提供了javap命令来对二进制文件进行反编译。执行javap得到的是直观的java指令序列。
类加载器把Java字节码载入到运行时数据区,执行引擎负责Java字节码的执行。
类加载
Java提供了动态加载的特性,只有在运行时第一次遇到类时才会去加载和链接,而非在编译时加载它。JVM的类加载器负责类的动态加载过程。Java类加载器的特点如下:
层次结构:Java的类加载器按是父子关系的层次结构组织的。Boostrap类加载器处于层次结构的顶层,是所有类加载器的父类。
代理模型:基于类加载器的层次组织结构,类加载器之间是可以进行代理的。当一个类需要被加载,会先去请求父加载器判断该类是否已经被加载。如果父类加器已加载了该类,那它就可以直接使用而无需再次加载。如果尚未加载,才需要当前类加载器来加载此类。
可见性限制:子类加载器可以从父类加载器中获取类,反之则不行。
不能卸载:类加载器可以载入类却不能卸载它。但是可以通过删除类加载器的方式卸载类。
每个类加载器都有自己的空间,用于存储其加载的类信息。当类加载器需要加载一个类时,它通过FQCN(Fully Quanlified Class Name: 全限定类名)的方式先在自己的存储空间中检测此类是否已存在。在JVM中,即便具有相同FQCN的类,如果出现在了两个不同的类加载器空间中,它们也会被认为是不同的。存在于不同的空间意味着类是由不同的加载器加载的。
类加载器的代理模型
当JVM请示类加载器加载一个类时,加载器总是按照从类加载器缓存、父类加载器以及自己加载器的顺序查找和加载类。加载器会先从缓存中判断此类是否已存在,如果不存在就请示父类加载器判断是否存在,如果直到Bootstrap类加载器都不存在该类,那么当前类加载器就会从文件系统中找到类文件进行加载。
Bootstrap加载器:Bootstrap加载器在运行JVM时创建,用于加载Java APIs,包括Object类。不像其他的类加载器由Java代码实现,Bootstrap加载器是由native代码实现的。
扩展加载器(Extension class loader):扩展加载器用于加载除基本Java APIs以外扩展类。也用于加载各种安全扩展功能。
系统加载器(System class loader):如果说Bootstrap和Extension加载器用于加载JVM运行时组件,那么系统加载器加载的则是应用程序相关的类。它会加载用户指定的CLASSPATH里的类。
用户自定义加载器:这个是由用户的程序代码创建的类加载器。
像Web应用服务器(WAS: Web Application Server)等框架通过使用用户自定义加载器使Web应用和企业级应用可以隔离开在各自的类加载空间独自运行。也就是说可以通过类加载器的代理模型来保证应用的独立性。不同的WAS在自定义类加载器时会有略微不同,但都不外乎使用加载器的层次结构原理。
类加载步骤
每一步的具体描述如下:
加载(Loading):从文件中获取类并载入到JVM内存空间。
验证(Verifying): 验证载入的类是否符合Java语言规范和JVM规范。在类加载流程的测试过程中,这一步是最为复杂且耗时最长的部分。大部分JVM TCK的测试用例都用于检测对于给定的错误的类文件是否能得到相应的验证错误信息。
准备(Preparing):根据内存需求准备相应的数据结构,并分别描述出类中定义的字段、方法以及实现的接口信息。
解析(Resolving):把类常量池中所有的符号引用转为直接引用。
初始化(Initializing):为类的变量初始化合适的值。执行静态初始化域,并为静态字段初始化相应的值。
JVM规范定义了规则,但也允许在运行时灵活处理。
运行时数据区
运行时数据区是JVM程序运行时在操作系统上分配的内存区域。运行时数据区又可细分为6个部分,即:为每个线程分别创建的PC寄存器、JVM栈、本地方法栈和被所有线程共用的数据堆、方法区和运行时常量池。
PC 寄存器:每个线程都会有一个PC(Program Counter)寄存器,并跟随线程的启动而创建。PC寄存器中存有将执行的JVM指令的地址。
JVM 栈:每个线程都有一个JVM栈,并跟随线程的启动而创建。其中存储的数据元素称为栈帧(Stack Frame)。JVM会每把栈桢压入JVM栈或从中弹出一个栈帧。如果有任何异常抛出,像printStackTrace()方法输出的栈跟踪信息的每一行表示一个栈帧。
栈帧:在JVM中一旦有方法执行,JVM就会为之创建一个栈帧,并把其添加到当前线程的JVM栈中。当方法运行结束时,栈帧也会相应的从JVM栈中移除。栈帧中存放着对本地变量数组、操作数栈以及属于当前运行方法的运行时常量池的引用。本地变量数组和操作数栈的大小在编译时就已确定,所以属在运行时属于方法的栈帧大小是固定的。
本地变量数组:本地变量数组的索引从0开始计数,其位置存储着对方法所属类实例的引用。从索引位置1开始的保存的是传递给该方法的参数。其后存储的就是真正的方法的本地变量了。
操作数栈:是方法的实际运行空间。每个方法变换操作数栈和本地变量数组,并把调用其它方法的结果从栈中弹或压入。在编译时,编译器就能计算出操作数栈所需的内存窨,因此操作数栈的大小在编译时也是确定的。
本地方法栈:为非Java编写的本地代程定义的栈空间。也就是说它基本上是用于通过JNI(Java Native Interface)方式调用和执行的C/C++代码。根据具体情况,C栈或C++栈将会被创建。
方法区:方法区是被所有线程共用的内存空间,在JVM启动时创建。它存储了运行时常量池、字段和方法信息、静态变量以及被JVM载入的所有类和接口的方法的字节码。不同的JVM提供者在实现方法区时会通常有不同的形式。在Oracle的Hotspot JVM里方法区被称为Permanent Area(永久区)或Permanent Generation(PermGen, 永久代)。JVM规范并对方法区的垃圾回收未做强制限定,因此对于JVM实现者来说,方法区的垃圾回收是可选操作。
运行时常量池:一个存储了类文件格式中的常量池表的内存空间。这部分空间虽然存在于方法区内,但却在JVM操作中扮演着举足轻重的角色,因此JVM规范单独把这一部分拿出来描述。除了每个类或接口中定义的常量,它还包含了所有对方法和字段的引用。因此当需要一个方法或字段时,JVM通过运行时常量池中的信息从内存空间中来查找其相应的实际地址。
数据堆:堆中存储着所有的类实例或对象,并且也是垃圾回收的目标场所。当涉及到JVM性能优化时,通常也会提及到数据堆空间的大小设置。JVM提供者可以决定划分堆空间或者不执行垃圾回收。
执行引擎
JVM通过类加载器把字节码载入运行时数据区是由执行引擎执行的。执行引擎以指令为单位读入Java字节码,就像CPU一个接一个的执行机器命令一样。每个字节码命令包含一字节的操作码和可选的操作数。执行引擎读取一个指令并执行相应的操作数,然后去读取并执行下一条指令。
尽管如此,Java字节码还是以一种可以理解的语言编写的,而不像那些机器直接执行的无法读懂的语言。所以JVM的执行引擎必须要把字节码转换为能被机器执行的语言指令。执行引擎有两种常用的方法来完成这一工作:
解释器(Interpreter):读取、解释并逐一执行每一条字节码指令。因为解释器逐一解释和执行指令,因此它能够快速的解释每一个字节码,但对解释结果的执行速度较慢。所有的解释性语言都有类似的缺点。叫做字节码的语言人本质上就像一个解释器一样运行。
即时编译器(JIT: Just-In-Time):即时编译器的引入用来弥补解释器的不足。执行引擎先以解释器的方式运行,然后在合适的时机,即时编译器把整修字节码编译成本地代码。然后执行引擎就不再解释方法的执行而是通过使用本地代码直接执行。执行本地代码较逐一解释执行每条指令在速度上有较大的提升,并且通过对本地代码的缓存,编译后的代码能具有更快的执行速度。
然而,即时编译器在编译代码时比逐一解释和执行每条指令更耗时,所以如果代码只会被执行一次,解释执行可能会具有更好的性能。所以JVM通过检查方法的执行频率,然后只对达到一定频率的方法才会做即时编译。
即时编译器
即时编译器先把字节码转为一种中间形式的表达式(IR: Itermediate Representation),并对之进行优化,然后再把这种表达式转为本地代码。
Oracel Hotspot VM使用的即时编译器称为Hotspot编译器。之所以称为Hotspot是因为Hotspot Compiler会根据分析找到具有更高编译优先级的热点代码,然后所这些热点代码转为本地代码。如果一个被编译过的方法不再被频繁调用,也即不再是热点代码,Hotspot VM会把这些本地代码从缓存中删除并对其再次使用解释器模式执行。Hotspot VM有Server VM和Client VM之后,它们所使用的即时编译器也有所不同。
Hotspot ClientVM 和Server VM
Client VM和Server VM使用相同的运行时环境,如上图所示,它们的区别在于使用了不同的即时编译器。Server VM通过使用多种更为复杂的性能优化技术从而具有更好的表现。IBM VM在他的IBM JDK6中引入了AOT(Ahead-Of-Time) 编译器技术。通过此种技术使得多个JVM之间能通过共享缓存分享已编译的本地代码。也就是说通过AOT编译器编译的代码能被其他JVM直接使用而无须再次编译。另外IBM JVM通过使用AOT编译器把代码预编译为JXE(Java Executable)文件格式从而提供了一种快速执行代码的方式。大多数的Java性能提升都是通过优化执行引擎的性能实现的。像即时编译等各种优化技术被不断的引入,从而使得JVM性能得到了持续的优化和提升。老旧的JVM与最新的JVM之间最大的差异其实就来自于执行引擎的提升。
JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。
永久代是用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类,永久代中一般包含:
- 类的方法(字节码…)
- 类名(Sring对象)
- .class文件读到的常量信息
- class对象相关的对象列表和类型列表 (e.g., 方法对象的array).
- JVM创建的内部对象
- JIT编译器优化用的信息
更多推荐
所有评论(0)