理解Java虚拟机体系结构
Posted 奔跑-起点
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了理解Java虚拟机体系结构相关的知识,希望对你有一定的参考价值。
1 概述
众所周知,Java支持平台无关性、安全性和网络移动性。而Java平台由Java虚拟机和Java核心类所构成,它为纯Java程序提供了统一的编程接口,而不管下层操作系统是什么。正是得益于Java虚拟机,它号称的“一次编译,到处运行”才能有所保障。
1.1 Java程序执行流程
Java程序的执行依赖于编译环境和运行环境。源码代码转变成可执行的机器代码,由下面的流程完成:
Java技术的核心就是Java虚拟机,因为所有的Java程序都在虚拟机上运行。Java程序的运行需要Java虚拟机、Java API和Java Class文件的配合。Java虚拟机实例负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例就诞生了。当程序结束,这个虚拟机实例也就消亡。
Java的跨平台特性,因为它有针对不同平台的虚拟机。
1.2 Java虚拟机
Java虚拟机的主要任务是装载class文件并且执行其中的字节码。由下图可以看出,Java虚拟机包含一个类装载器(class loader),它可以从程序和API中装载class文件,Java API中只有程序执行时需要的类才会被装载,字节码由执行引擎来执行。
当Java虚拟机由主机操作系统上的软件实现时,Java程序通过调用本地方法和主机进行交互。Java方法由Java语言编写,编译成字节码,存储在class文件中。本地方法由C/C++/汇编语言编写,编译成和处理器相关的机器代码,存储在动态链接库中,格式是各个平台专有。所以本地方法是联系Java程序和底层主机操作系统的连接方式。
由于Java虚拟机并不知道某个class文件是如何被创建的,是否被篡改一无所知,所以它实现了一个class文件检测器,确保class文件中定义的类型可以安全地使用。class文件检验器通过四趟独立的扫描来保证程序的健壮性:
-
class文件的结构检查
-
类型数据的语义检查
-
字节码验证
-
符号引用验证
Java虚拟机在执行字节码时还进行其它的一些内置的安全机制的操作,他们作为Java编程语言保证Java程序健壮性的特性,同时也是Java虚拟机的特性:
-
类型安全的引用转换
-
结构化的内存访问
-
自动垃圾收集
-
数组边界检查
-
空引用检查
1.3 Java虚拟机数据类型
Java虚拟机通过某些数据类型来执行计算。数据类型可以分为两种:基本类型和引用类型,如下图:
但boolean有点特别,当编译器把Java源码编译为字节码时,它会用int或byte表示boolean。在Java虚拟机中,false是由0表示,而true则由所有非零整数表示。和Java语言一样,Java虚拟机的基本类型的值域在任何地方都是一致的,不管主机平台是什么,一个long在任何虚拟机中总是一个64位二进制补码的有符号整数。
对于returnAddress,这个基本类型被用来实现Java程序中的finally子句,Java程序员不能使用这个类型,它的值指向一条虚拟机指令的操作码。
2 体系结构
在 Java虚拟机规范中,一个虚拟机实例的行为是分别按照子系统、内存区、数据类型和指令来描述的,这些组成部分一起展示了抽象的虚拟机的内部体系结构。
2.1 class文件
Java class文件包含了关于类或接口的所有信息。class文件的“基本类型”如下:
u1 | 1个字节,无符号类型 |
u2 | 2个字节,无符号类型 |
u4 | 4个字节,无符号类型 |
u8 | 8个字节,无符号类型 |
如果想了解更多,Oracle的JVM SE7给出了官方规范:The Java? Virtual Machine Specification
class文件包含的内容:
ClassFile {u4 magic; //魔数:0xCAFEBABE,用来判断是否是Java class文件u2 minor_version; //次版本号u2 major_version; //主版本号u2 constant_pool_count; //常量池大小cp_info constant_pool[constant_pool_count-1]; //常量池u2 access_flags; //类和接口层次的访问标志(通过|运算得到)u2 this_class; //类索引(指向常量池中的类常量)u2 super_class; //父类索引(指向常量池中的类常量)u2 interfaces_count; //接口索引计数器u2 interfaces[interfaces_count]; //接口索引集合u2 fields_count; //字段数量计数器field_info fields[fields_count]; //字段表集合u2 methods_count; //方法数量计数器method_info methods[methods_count]; //方法表集合u2 attributes_count; //属性个数attribute_info attributes[attributes_count]; //属性表}
2.2 类装载器子系统
类装载器子系统负责查找并装载类型信息。其实Java虚拟机有两种类装载器:系统装载器和用户自定义装载器。前者是Java虚拟机实现的一部分,后者则是Java程序的一部分。
-
启动类装载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自java.lang.ClassLoader。
-
扩展类装载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
-
应用程序类装载器(application class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
除了系统提供的类装载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类装载器,以满足一些特殊的需求。
类装载器子系统涉及Java虚拟机的其它几个组成部分以及来自java.lang库的类。ClassLoader定义的方法为程序提供了访问类装载器机制的接口。此外,对于每一个被装载的类型,Java虚拟机都会为它创建一个java.lang.Class类的实例来代表该类型。和其它对象一样,用户自定义的类装载器以及Class类的实例放在内存中的堆区,而装载的类型信息则位于方法区。
类装载器子系统除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及解析符号引用。这些动作还需要按照以下顺序进行:
-
装载(查找并装载类型的二进制数据)
-
连接(执行验证:确保被导入类型的正确性;准备:为类变量分配内存,并将其初始化为默认值;解析:把类型中的符号引用转换为直接引用)
-
初始化(类变量初始化为正确初始值)
2.3 方法区
在Java虚拟机中,关于被装载的类型信息存储在一个方法区的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件并将它传输到虚拟机中,接着虚拟机提取其中的类型信息,并将这些信息存储到方法区。方法区也可以被垃圾回收器收集,因为虚拟机允许通过用户定义的类装载器来动态扩展Java程序。
方法区中存放了以下信息:
-
这个类型的全限定名(如全限定名java.lang.Object)
-
这个类型的直接超类的全限定名
-
这个类型是类类型还是接口类型
-
这个类型的访问修饰符(public, abstract, final的某个子集)
-
任何直接超接口的全限定名的有序列表
-
该类型的常量池(一个有序集合,包括直接常量[string, integer和floating point常量]和对其它类型、字段和方法的符号引用)
-
字段信息(字段名、类型、修饰符)
-
方法信息(方法名、返回类型、参数数量和类型、修饰符)
-
除了常量以外的所有类(静态)变量
-
指向ClassLoader类的引用(每个类型被装载时,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的)
-
指向Class类的引用(对于每一个被装载的类型,虚拟机相应地为它创建一个java.lang.Class类的实例。比如你有一个到java.lang.Integer类的对象的引用,那么只需要调用Integer对象引用的getClass()方法,就可以得到表示java.lang.Integer类的Class对象)
2.4 堆
Java程序在运行时创建的所有类实例或数组(数组在Java虚拟机中是一个真正的对象)都放在同一个堆中。由于Java虚拟机实例只有一个堆空间,所以所有线程都将共享这个堆。需要注意的是,Java虚拟机有一条在堆中分配对象的指令,却没有释放内存的指令,因为虚拟机把这个任务交给垃圾收集器处理。Java虚拟机规范并没有强制规定垃圾收集器,它只要求虚拟机实现必须“以某种方式”管理自己的堆空间。比如某个实现可能只有固定大小的堆空间,当空间填满,它就简单抛出OutOfMemory异常,根本不考虑回收垃圾对象的问题,但却是符合规范的。
Java虚拟机规范并没有规定Java对象在堆中如何表示,这给虚拟机的实现者决定怎么设计。一个可能的堆设计如下:
一个句柄池,一个对象池。一个对象的引用就是一个指向句柄池的本地指针。这种设计的好处有利于堆碎片的整理,当移动对象池中的对象时,句柄部分只需更改一下指针指向对象的新地址即可。缺点是每次访问对象的实例变量都要经过两次指针传递。
2.5 Java栈
每当启动给一个线程时,Java虚拟机会为它分配一个Java栈。Java栈由许多栈帧组成,一个栈帧包含一个Java方法调用的状态。当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中,当该方法返回时,这个栈帧就从Java栈中弹出。Java栈存储线程中Java方法调用的状态–包括局部变量、参数、返回值以及运算的中间结果等。Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。这样设计的原因是为了保持Java虚拟机的指令集尽量紧凑,同时也便于Java虚拟机在只有很少通用寄存器的平台上实现。另外,基于栈的体系结构,也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。
2.5.1 栈帧
栈帧由局部变量区、操作数栈和帧数据区组成。当虚拟机调用一个Java方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并根据此分配栈帧内存,然后压入Java栈中。
2.5.1.1 局部变量区
局部变量区被组织为以字长为单位、从0开始计数的数组。字节码指令通过从0开始的索引使用其中的数据。类型为int, float, reference和returnAddress的值在数组中占据一项,而类型为byte, short和char的值在存入数组前都被转换为int值,也占据一项。但类型为long和double的值在数组中却占据连续的两项。
2.5.1.2 操作数栈
和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。它通过标准的栈操作访问–压栈和出栈。由于程序计数器无法被程序指令直接访问,Java虚拟机的指令是从操作数栈中取得操作数,所以它的运行方式是基于栈而不是基于寄存器。虚拟机把操作数栈作为它的工作区,因为大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。
2.5.1.3 帧
以上是关于理解Java虚拟机体系结构的主要内容,如果未能解决你的问题,请参考以下文章