JVM运行时数据区

尽管不同平台上独立实现的JVM会稍有不同,但每一个JVM都必须提供如图1-3所示的运行时组件。

 

图1-3  运行时数据区

1. 堆

堆是一个自由内存区域,常用于内存的动态分配或临时分配。堆是一种运行时数据区,为类和数组对象提供内存。JVM在启动时创建堆,然后当Java中创建类或数组对象时,就从堆中分配所需内存。当对象或数组不再存在后,由一个称为垃圾收集的自动存储管理系统来回收堆内存,稍后将会详细介绍该系统。

JVM规范中未指明堆的实现方式,以便创造各种不同的JVM实现。堆的尺寸可以是固定不变的,也可以在需要时增大或在当前尺寸过大时减小。程序员可以指定堆的初始化大小,如在Win32和Solaris系统中利用-mx命令行选项进行指定。堆内存并不一定是连续的。如果堆内存用完,并且无法为堆分配额外的内存,系统就会产生OutOfMemoryError异常。

2. 栈

Java的栈帧用于存储方法调用的状态。栈帧中存储了数据和部分结果,以及方法的执行环境、方法调用所需的所有局部变量和方法的操作数栈。操作数栈中存储着大部分字节码指令的参数和返回值。执行环境中则包含了指向方法调用的指针。

帧是构成JVM栈的组件。帧中存储了部分结果、数据和方法的返回值,帧还用于动态链接以及抛出运行时异常。调用方法时就会创建一个帧,而一旦该方法结束,该帧也会销毁。帧中包含有一组局部变量、操作数栈以及对当前方法所在类的运行时常量池的引用。

在JVM运行Java代码的任意时刻,JVM中只有对应当前执行方法的帧是活动的。该帧称为当前帧,所代表的方法就是当前方法,而包含该方法的类则为当前类。当线程调用一个方法时(每个线程都有其自身的栈),JVM也会创建一个新帧,使该帧成为当前帧,并压到该线程的栈中。

与堆一样,JVM规范把栈帧的实现方式留到具体的JVM实现方式中。栈的尺寸可以固定,也可以根据需求扩大或缩小。程序员能够控制栈的初始化尺寸和最大、最小尺寸。Win32和Solaris平台上通过命令行选项-ss和-oss来指定尺寸。如果计算所需的栈超出范围,那么就会产生StackOverflowError异常。

3. 方法区

方法区是由所有JVM线程共享的一个公共存储区。该存储区常用于存储运行时常量池、方法数据、字段数据以及方法和构造函数的对应字节码。JVM规范仅仅描述了方法区的一般特性,并未规定该区域的位置,也没有指明该区域的实现方式。方法区的尺寸可以固定,也可以扩大或缩小。程序员能够指定方法区的初始尺寸,并且该区域不必是连续的。

4. 寄存器

JVM中的寄存器与其他计算机系统中的寄存器类似,用于反映虚拟机的当前状态。寄存器会在字节码执行时更新。其中,主寄存器是程序计数器(pc寄存器),用于指示JVM当前执行指令的地址。如果当前执行的是本地方法(用非Java语言编写的方法),则pc寄存器的值不定。JVM中的其他寄存器包括指向当前方法的执行环境的指针、指向当前执行方法的第一个局部变量的指针、以及指向操作数栈顶的指针。

5. 运行时常量池

运行时常量池相当于其他编程语言中所用的符号表。顾名思义,常量池中包含的是数值文字和字段常量。每个运行时常量池的内存是从方法区中分配的, JVM为类或接口加载类文件时会构造一个运行时常量池。

1.2.4 垃圾收集器

过去的语言,如C语言,要求程序员显式地分配内存、释放内存。程序在需要时分配内存,不需要时释放内存。但是这种做法常常引起“内存泄漏”,即由于某种原因使分配的内存始终没有得到释放。如果该任务不断地重复,程序最终会耗尽内存并异常终止,至少无法继续运行。相比之下,Java不要求程序员显式地分配内存和释放内存,避免了很多潜在问题。Java在创建对象时会自动分配内存,并当该对象的引用不存在时释放这块内存。

Java中使用称为垃圾收集器的技术来监视Java程序的运行,当对象不再使用时,就自动释放对象所使用的内存。Java使用一系列软指针来跟踪对象的各个引用,并用一个对象表将这些软指针映射为对象的引用。之所以称为软指针,是因为这些指针并不直接指向对象,而是指向对象的引用。使用软指针,Java的垃圾收集器能够以单独的线程在后台运行,并依次检查每个对象。通过更改对象表项,垃圾收集器可以标记对象、移除对象、移动对象或检查对象。

垃圾收集器是自动运行的,一般情况下无需显式地请求垃圾收集器。程序运行时,垃圾收集器会不时检查对象的各个引用,并回收无引用对象所占用的内存。调用System类中的静态gc()方法可以运行垃圾收集器,但这样并不能保证立即回收指定对象。

1.2.5 JVM:加载、链接和初始化

JVM要解释Java字节码,就必须对所需的类和接口执行如下3步操作:

(1) 加载:JVM在加载类时,会查找该类或该接口的二进制表示,并根据找到的二进制表示(通常是由Java编译器创建的类文件)创建一个Class对象。该Class对象中封装了类或接口的运行时状态。

(2) 链接:链接这一过程是指取得已加载的类或接口、结合JVM运行时环境、准备执行该类或该接口。

(3) 初始化:初始化是指JVM调用该类或该接口的初始化方法。

1. 第一步

启动一个单机Java程序时,JVM首先做的是另外创建一个Class对象,用于表示包含public static void main(String [ ] args)方法的Java类。然后JVM会链接并初始化该Java类,调用main()方法,并用main()方法驱动所引用的其他类和接口的加载、链接和初始化过程。

2. 加载

加载过程是由类加载器完成的,该加载器是ClassLoader的子类,并且该类加载器会对所加载的类或接口进行一些校验检查。当表示已编译类或接口的二进制数据有错,则类或接口使用的类文件格式版本不被支持,类加载器找不到类或接口的定义,或者如果出现类循环,都会抛出异常。类循环是指类或接口的父类是其自身的情况。

类加载器一般有两种类型:由JVM提供的引导类加载器(bootstrap class loader)和用户定义的类加载器。用户定义的类加载器也是Java的ClassLoader类的子类,用于从非标准的、用户定义的源创建Class对象,以便提高安全性。例如,从加密文件中提取Class对象。一个加载器可以将部分甚至整个加载过程委托给另一个加载器。最终生成Class对象的加载器称为定义加载器(defining loader),而开始该加载过程的加载器称为启动加载器(initiating loader)。

使用默认引导类加载器的加载过程如下:根据所要加载的类文件,引导类加载器会判断自身是否已经成为该类的启动加载器。如果是,则Class对象存在,加载器停止(注意,加载一个类并不等于创建该类的一个实例,这一步骤仅仅是在JVM中加入该类)。如果类还没有加载,则加载器会搜索对应的类文件,并在找到后根据该类文件创建Class对象。如果找不到类文件,那么就会产生NoClassDefFoundError异常。

使用用户定义类加载器时,整个加载过程稍有不同。与引导加载器一样,用户定义的加载器首先判断自身是否已经成为目标类文件的启动加载器。如果是,则Class对象已经存在,加载器停止,而如果不是,用户定义的加载器会调用loadClass()方法。loadClass()方法返回所需的类文件并将表示类的二进制字节装配成ClassFile结构,然后调用defineClass()方法,由该方法从ClassFile结构创建Class对象。另外,loadClass()方法也可以将加载过程委托给另一个类加载器。

3. 链接

链接过程的第一步是校验需要链接的类文件。

Java类文件校验

由于JVM与Java编译器是完全分离的,因此,用来解释类文件的JVM无法保证类文件的形式正确,甚至无法保证该文件确实由Java编译器所生成。另一个问题在于继承与类兼容性。如果给定类文件所表示的类继承自另一个类文件表示的父类,那么JVM必须确保该子类的类文件与父类的类文件兼容。

JVM会校验每个类文件是否满足Java语言规范对类文件的约束,不过Java类校验器与Java语言无关。用某些其他语言编写的程序同样也能编译成类文件格式,编译之后,该类文件也能通过校验过程。

校验过程分为4个步骤:

(1) 第一步由JVM加载类文件并检查文件是否符合类文件的基本格式。类文件的长度必须准确。类文件必须确实表示类(检查其中一个特殊数字)。常量池中不能包含任何不可识别的信息,并且每个属性的长度正确。

(2) 校验过程的第二步在文件链接时进行。这一步执行的操作包括确保final关键字约束的保留。这表示final类不能派生子类,final方法也不能被重写。然后确保常量池中的元素符合Java语言的规定。验证常量池中的所有字段和方法引用,并检查每一个类(Object类除外)是否具有直接父类。

(3) 第三个校验步骤也在链接阶段进行。这一步检查类文件中引用的每一个方法,确保符合Java语言对方法的规定。方法调用中参数的数量和类型必须正确。操作数栈必须总保持相同大小,并包含相同类型的值。局部变量在访问前应当包含合适的值。必须为字段指定正确类型的值。

(4) 校验的最后一步是处理第一次调用方法时出现的事件,并保证一切按规范进行。这些检查包括:确保给定类中存在某个引用的字段或引用的方法,确认引用的字段或引用的方法具有正确的描述符,并确保一个方法在运行时能够访问该引用字段或引用方法。

准备

在校验类文件之后,JVM准备初始化类,包括为类变量分配内存空间并设置为默认初始值。这些值是标准的默认值,例如int类型为0,Boolean类型为false等。在初始化阶段,这些值会设为程序相关的默认值。

解析

在这一可选的步骤中,JVM把运行时常量池中引用的符号解析成具体值。

4. 初始化

链接过程完成后,会调用静态字段和静态初始化器。静态字段的值即使在类没有实例化时也能够访问得到,而静态初始化器用于单个表达式无法表示的静态初始化。JVM把所有这类初始化器收集到一个特殊的方法中。例如,类所有初始化器的集合就是初始化方法<clinit>。

不过,JVM在初始化一个类时不仅需要调用该类的初始化方法(只有JVM能够调用),而且需要初始化所有的父类(即需要调用这些父类的<clinit>)。结果就是,总是需要最先初始化Object类。另外,包含应用程序main()方法的类总是要初始化。

1.2.6 执行字节码

类文件中的字节码由一系列单字节操作码指令构成,操作码指定所要执行的操作。操作码可以带有任意个数的操作数,操作数是该操作所用的参数或数据。JVM解释程序本质上使用do…while循环来加载每一个操作码以及所有相关操作数,然后执行操作码所表示的任务。字节码根据JVM指令集转换成操作,JVM指令集将字节码对应于JVM规范指定的相应操作。该过程一直进行到解释完所有操作码为止。

出于紧凑原因(但使性能稍有下降),JVM操作码采用单字节实体。使用单字节操作码能够最小化JVM指令集的长度。超过1个字节大小的数据在运行时用多个单字节实体来构造。

JVM指令集的第一组指令包括对基本数据类型和对象的基本操作。指令使用的命名形式通常是数据类型加操作。例如,iload指令(iload仅仅是实际指令的助记符)表示将一个int类型的局部变量加载到操作数栈中,而fload指令则是将一个float类型变量加载到操作数栈中,依此类推。这一组指令的功能还包括将操作数栈中的特定数据类型存储到一个局部变量中、将一个常量加载到操作数栈中、以及访问多个局部变量。

第二组指令涉及到算术运算,算术运算通常会用到当前操作数栈中的两个值,并将运算结果放入操作数栈。命名法与之前相同,例如iadd操作是将两个整型值相加,dadd操作则是将两个双精度型值相加。

类似地,还有一些操作表示基本的数学函数(加、减、乘、除),另一些操作表示逻辑运算(按位或、按位与、按位非)和一些特殊功能,包括求余、取反、移位、递增和比较。

JVM的浮点数运算和向零舍/入运算遵循IEEE 754标准。某些整型运算(如被零除)会引发ArithmeticException异常,但浮点运算符不会引发运行时异常,而是在出现溢出情况时,返回NaN(Not a Number,无效数学运算的结果)。

JVM指令集中还包含不同类型之间的转换操作。JVM支持加宽转换,例如float类型转换为double类型。转换指令的命名是前一类型加上“2”再加上后一类型。例如,i2l指令是int类型向long类型的转换。指令集中还包含了一些变窄操作,如int类型向char类型的转换。这类操作的命名形式与加宽转换的命名法相同。

类与数组对象的创建和操作也存在相应的指令。new指令用于创建一个新的类对象,newarray、anewarray和multilinearray指令分别用来创建相应的数组对象。还存在相应的指令用于访问类的静态变量和实例变量、将数组组件加载到操作数栈中、从操作数栈中取值并存储到数组组件中、返回数组长度以及检查类对象或数组的特定属性。

JVM指令集中为方法调用提供了invokevirtual、invokeinterface、invokespecial和invokestatic指令,其中invokevirtual为普通的方法调度模式。另外3个指令分别用于调用接口方法、调用要求特殊处理的方法,如私有方法或父类方法、以及调用静态方法。JVM为每一种数据类型都定义了方法的返回指令。

最后,还有一些指令用于其他各种操作,包括操作数栈管理、传输控制、异常抛出、finally关键字的实现以及同步。

例如,思考下面这个简单的Java类:

class Hello {

public static void main(String[] args) {

System.out.println("Hello World!");

}

}

编译该类并用javap工具加上-c开关量(稍后介绍)将所得的类文件反汇编,就能得到用助记符表示的字节码:

Compiled from Hello.java

class Hello extends java.lang.Object {

Hello();

public static void main(java.lang.String[]);

}

Method Hello()

0 aload_0

1 invokespecial #1 <Method java.lang.Object()>

4 return

Method void main(java.lang.String[])

0 getstatic #2 <Field java.io.PrintStream out>

3 ldc #3 <String "Hello World!">

5 invokevirtual #4 <Method void println(java.lang.String)>

8 return

上面这段助记符中比较重要的是main()方法下面的3行,这3行用于转换Java语言中的代码行System.out.println(“Hello World”);。

第一个指令getstatic从java.lang.system对象的out字段中检索PrintStream对象,并放到操作数栈中。接下来的一行,ldc将字符串“Hello World!”压到操作数栈中。最后,invokevirtual执行java.io.PrintStream类中的println方法。该方法的正确执行要求操作数栈中存在一个String以及java.io.PrintStream类的一个实例,并依次排列。执行完成后会从栈中移除这两个参数。

1.3 Java类文件格式

JVM无法直接解释Java编程语言,因此要先编译Java代码,生成1个或多个JVM能够解释的类文件。类文件中包含了字节码、符号表、以及关于类或接口的其他信息。类文件的结构是精确定义的二进制格式,确保任何JVM都能加载并解释任意的类文件,而无需考虑类文件是在哪里生成的。

类文件本身包含一个8位字节流。所有高位量(16位、32位或64位)都是通过组合读取8位字节创建而成,多字节量按高位优先顺序(big-endian)存储,即高字节存储在低地址。Java语言中提供了I/O流(由java.io包中的DataInput、DataInputStream、DataOutput和DataOutputStream接口提供)来读写类文件。

类文件中的数据类型分别为无符号型1字节量、2字节量和4字节量。分别用u1、u2和u4语法来表示。类文件中还可以包含一系列连续的、固定大小的项,它们可以像数组一样设置索引,用方括号[]指定具体项。

类格式中包含一个ClassFile结构,该结构中包含了JVM所需的关于该类或该接口的全部信息。ClassFile的结构通常如下文所示:

ClassFile {

u4 magic;

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_into attributes[attributes_count];

}

magic参数是指定类文件格式的特殊数字。这里的值为0xCAFEBABE,表示这段代码是一个类文件。

major_version和minor_version项分别代表类文件格式的主、辅版本号。对JVM而言,版本号表示类文件所遵循的格式。JVM通常只能加载一定版本范围内的类文件(例如,由一个主版本号和一系列辅版本号构成的版本范围)。

constant_pool_count项的值等于常量池中元素的个数加1。这一变量决定constant_pool索引是否有效。constant_pool[]项是一张cp_info结构表,其中包含了constant_pool中各元素的信息。

access_flags项是标志(flag)掩码,指明该文件是类还是接口,以及该类或接口的访问权限。掩码可以关闭,也可以是public、final、super、interface或abstract标志的组合。

this_class参数指向constant_pool表中的CONSTANT_Class_infor结构,该结构表示由该类文件定义的类或接口。super_class项则指向constant_pool中另一个类似的元素,表示直接父类或父接口,零表示没有父类。

interfaces_count参数表示该类或该接口的直接父接口的个数。interfaces[]项中包含了这些父接口在constant_pool表中的位置。

fields_count变量给出了ClassFile中field_info结构的个数。field_info结构表示由该类或接口声明的所有字段,包括静态字段与实例字段。methods[]项是包含method_info结构的表。

最后还有attributes_count变量给出了类或接口中attributes表中属性的个数。attributes[]项是包含attribute结构的表。

<!-- page --><!-- page -->

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值