下图显示了符合Java SE 7 版本的Java虚拟机规范的一个典型JVM中的关键内部组件。
图中显示的组件将会在下面两部分中进行逐一的解释。第一部分涉及JVM为每一个线程都会创建的组件;第二部分则是独立于线程进行创建的组件。
1. Thread
Thread是一个程序中的一个执行线程。JVM允许一个应用程序有多个执行线程并发运行。在Sun的Hotspot JVM中,Java线程与本地操作系统线程间存在一个直接的一一映射。JVM先为Java线程准备好所有的状态,如线程局部存储、分配缓冲区、同步对象、栈和程序计数器,之后对应的本地线程被创建。Java线程终止以后其本地线程将会被回收利用,由操作系统负责调度所有的线程并将它们分派到可用的CPU。一旦本地线程已经初始化完成,则会调用Java线程的run()方法,当run()方法返回时,未捕获的异常将被处理,之后本地线程判断JVM是否需要终止运行(如此线程是最后一个非守护线程)。当线程终止以后,本地线程及Java线程占用的所有资源都会被释放。
1.1 JVM系统线程
如果你曾使用jconsole或其它的一些调试工具,也许你会注意到JVM中有许多的线程运行在后台。这些运行的后台线程,除了main线程以外,都是作为调用public static void main(String [])的一部分被创建,此外,还有一些是被main线程所创建。以Sun的Hotspot JVM为例,主要的后台系统线程有:
(1) VM Thread
此线程用于等待请求JVM到达一个“安全点”的操作的出现。之所以所有这类操作需要在一个单独的线程——VM Thread中进行,是因为它们都要求JVM处于“安全点”,当JVM位于“安全点”时,不能对heap执行修改操作。VM Thread执行的操作类型是会“stop-the-world”的垃圾回收、线程堆栈转储、线程挂起和偏向锁撤销。
注:stop-the-world,也即当执行此操作时,Java虚拟机需要停止运行。
(2) Periodic task thread(周期任务线程)
此线程负责用于调度周期性操作的执行的定时事件。
(3) GC threads(垃圾回收线程)
这些线程用于支持JVM中进行的不同类型的垃圾回收活动。
(4) Compiler threads
这些线程在运行时将字节码编译成本地代码。
(5) Signal dispatcher thread(信号分发线程)
此线程负责接收发送给JVM进程的信号,处理信号,根据系统设置调用合适的JVM方法。
1.2 Per Thread
每一个执行的线程都具有下列组件:
(1) 程序计数器(program Counter,PC)
除非是本地代码,否则PC指向当前指令(也即opcode)的地址。如果当前方法是本地的,则PC是未定义的。所有的CPU都有一个PC,一般情况下,PC会在每一个指令之后进行递增,以保存下一个执行指令的地址。JVM使用PC来记录当前指令执行的位置,PC实际上指向了方法区的一个内存地址。
(2) 栈(Stack)
每个线程都有自己的栈,栈为线程中执行的每个方法保存了一个对应的帧(Frame)。栈是后进先出(LIFO)的数据结构,因此当前执行的方法总是位于栈的顶端。当调用一个方法时,为其创建一个新的帧并压栈,当方法正常返回或者是由于执行期间抛出了未捕获的异常而退出时,其对应的帧将被删除(也即出栈)。除了执行帧的压栈与出栈,无法对栈进行其它的直接操作,因此帧对象也许是在堆(Heap)中进行内存分配,它们的内存也就无需是连续的。
(3) 本地栈(Native Stack)
并非所有的JVM都支持本地方法,然而,那些支持的JVM一般会为每个线程创建本地方法栈。如果JVM是使用“C链接”模型实现Java本地调用(JNI)的,则本地栈将是一个C栈。在这种情况下,本地栈中参数及返回值的顺序将与典型的C程序是一致的。一个本地方法一般会调回JVM,并调用Java方法,此类从本地方法到Java方法调用发生于正常的Java栈中,线程会离开本地栈,并在Java栈中为方法创建一个新的帧。
(4)栈的限制(Stack Restrictions)
栈可以是动态的,也可以是固定大小的。如果一个线程要求超过允许的大小的栈,则JVM会抛出StackOverflowError。如果线程要求创建一个新的帧,但是系统没有足够内存进行分配,则JVM会抛出OutOfMemoryError。
(5) 帧(Frame)
原文内容与栈部分大致一致,不冗述。
每个帧中包含有:1. 局部变量数组;2. 返回值;3. 操作对象栈;4. 到方法所属类的运行时常量池的引用。
(6) 局部变量数组(Loca Variables Array)
局部变量数组中包含了方法运行期间使用到的所有变量,包括到this变量的引用、所有的方法参数及其它方法中定义的局部变量。对于类方法(静态方法),方法参数从“零”开始,而对于实例方法,“零”位置预留给this变量。
一个局部变量可以是:
- boolean
- byte
- char
- long
- short
- int
- float
- double
- reference
- returnAddress
除了long和double,所有的类型都在局部变量数组中占据一个位置,long及double则占用两个连续的位置(slot),由于它们是双倍的宽度,也即64位大小。
(7) 操作对象栈
操作对象栈在字节码指令执行期间使用,作用类似于CPU使用的一般用途的寄存器。大多数的JVM操纵操作对象栈的方式有:压栈、出栈、复制、交换、通过执行某一操作产生或消耗成数值。因此,在字节码中,在局部变量数组与操作对象栈之间移动数值的指令出现的次数很多。例如,一个简单的变量就需要两条与操作对象栈进行交互的字节码。
- 0: iconst_0 // Push 0 to top of the operand stack
- 1: istore_1 // Pop value from top of operand stack and store as local variable 1
(8) 动态链接
每个帧包含了一个到运行时常量池的引用。此引用指向当前执行的方法对应的类的常量池,用于辅助支持动态链接功能。
当一个类被编译以后,所有到变量及方法的引用都被存储到类的常量池中作为一个符号引用。一个符号引用是一个逻辑引用而不是直接指向物理内存地址的引用。JVM的实现可以自由选择何时解析符号引用,可能的选择有:类文件被验证时亦或是被加载以后,这类方案被称为是急切的(eager)或静态的(static)解析;还有一种被称为懒加载或延时加载方案,它是在符号引用第一次被使用时进行解析。然而,JVM必须要表现得像是当引用第一次被使用时才进行解析,并且此时需要抛出任何的解析错误。
绑定是由符号引用标识的域、方法和类被直接引用替换的过程,此过程只发生一次。如果指向类的符号引用还没有被解析则此类会被加载。每一个直接引用都被存储为一个相对偏移值,以与变量或方法的运行时位置关联的存储结构作为基准。
1.3 Shared Between Threads
(1) 堆(Heap)
堆用于运行时分配类实例及数组。数组及对象无法存储在栈中,因为帧在创建以后,其大小就无法再改变。帧中只能存储指向堆中对象或数组的引用。与简单变量与在局部变量中的引用不同,数组对象总是存储在堆中,因此当方法退出时,它们没有被移除,对象只能被垃圾收集器移除。
为了支持垃圾回收,堆被分为三个部分:
- Young Generation(年轻代),通常被分为Eden和Survivor
- Old Generation(老年代,也叫Tenured Generation)
- Permanent Generation
(2) 内存管理
对象及数组不会被显示释放,垃圾回收器会自动回收它们。
此过程如下:
1. 新的对象和数组被创建并放到年轻代中;
2. 一次小的垃圾收集会在年轻代上执行,如果对象存活下来,则它们将被从eden空间移动到survivor空间;
3. 主垃圾收集(Major)执行,它会引起应用线程停止运行,也即stop-the-world,同时会在不同的“代”中移动对象。经过此次垃圾回收后,如果对象仍然存活,则它们会被从年轻代移动到老年代。
4. 在每次老年代被回收时永久代也被回收,当任何一个空间满时,它们都会被回收。
(3) 非堆内存
从逻辑上考虑,被认为是JVM结构的一部分的对象将不会在堆中进行创建。非堆内存包括有:1.包含方法区及interned字符串的永久代;2. 代码缓存,用于编辑与存储已经被JIT编译器编译为本地代码的方法。
(4) 即时编译
Java字节码采用解释执行方式,因此它没法像直接在JVM所在的主机上执行本地代码那么快。为了提高性能,Oracle Hotspot VM寻找字节码中被周期地执行的“热区域”,并将它们编译为本地代码。本地代码被存储到非堆内存的代码缓存中,通过这样的方式,Hotspot VM尝试选择最合适的方式来权衡编译代码花费的额外时间与执行解释代码花费的额外时间。
(5) 方法区(Method Area)
方法区中存储了每个类的元信息,如:
- 类加载器引用
- 运行时常量池
- 字段数据
- 方法数据
- 每个方法
- 名称
- 返回类型
- 参数类型
- Modifiers
- 属性值
- 方法代码
- 每个方法
- 字节码
- 操作对象栈大小
- 局部变量大小
- 局部变量表
- 异常表
- 每个异常处理器
- 起始点
- 结束点
- 处理器代码的PC偏移值
- 捕抓的异常类的常量池索引
所有的线程共享相同的方法区,因此方法区数据的存取及动态链接的过程必须是线程安全的。如果两个线程都企图存取同一个类中的字段或方法,但是,此类还没有被加载,则它必须只被加载一次,并且线程必须等到类被加载完成以后才能继续执行。
(6) 类文件结构
一个编译完成的类文件包含以下的结构:
- ClassFile {
- u4 magic;
- u2 minor_version;
- u2 major_version;
- u2 constant_pool_count;
- cp_info contant_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];
- }
如果你想看一个编译完成的类文件的字节码,可以使用命令行工具javap。
如果你编译下面这个简单的类:
- package org.jvminternals;
-
- public class SimpleClass {
-
- public void sayHello() {
- System.out.println("Hello");
- }
-
- }
之后你可以通过运行如下的javap命令,获得字节码信息,如:
javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class
- public class org.jvminternals.SimpleClass
- SourceFile: "SimpleClass.java"
- minor version: 0
- major version: 51
- flags: ACC_PUBLIC, ACC_SUPER
- Constant pool:
- #1 = Methodref #6.#17 // java/lang/Object."<init>":()V
- #2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
- #3 = String #20 // "Hello"
- #4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
- #5 = Class #23 // org/jvminternals/SimpleClass
- #6 = Class #24 // java/lang/Object
- #7 = Utf8 <init>
- #8 = Utf8 ()V
- #9 = Utf8 Code
- #10 = Utf8 LineNumberTable
- #11 = Utf8 LocalVariableTable
- #12 = Utf8 this
- #13 = Utf8 Lorg/jvminternals/SimpleClass;
- #14 = Utf8 sayHello
- #15 = Utf8 SourceFile
- #16 = Utf8 SimpleClass.java
- #17 = NameAndType #7:#8 // "<init>":()V
- #18 = Class #25 // java/lang/System
- #19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
- #20 = Utf8 Hello
- #21 = Class #28 // java/io/PrintStream
- #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
- #23 = Utf8 org/jvminternals/SimpleClass
- #24 = Utf8 java/lang/Object
- #25 = Utf8 java/lang/System
- #26 = Utf8 out
- #27 = Utf8 Ljava/io/PrintStream;
- #28 = Utf8 java/io/PrintStream
- #29 = Utf8 println
- #30 = Utf8 (Ljava/lang/String;)V
- {
- public org.jvminternals.SimpleClass();
- Signature: ()V
- flags: ACC_PUBLIC
- Code:
- stack=1, locals=1, args_size=1
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."<init>":()V
- 4: return
- LineNumberTable:
- line 3: 0
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 5 0 this Lorg/jvminternals/SimpleClass;
-
- public void sayHello();
- Signature: ()V
- flags: ACC_PUBLIC
- Code:
- stack=2, locals=1, args_size=1
- 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
- 3: ldc #3 // String "Hello"
- 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 8: return
- LineNumberTable:
- line 6: 0
- line 7: 8
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 9 0 this Lorg/jvminternals/SimpleClass;
- }
此类文件有三个主要部分:常量池、构造函数和sayHello方法。
- 常量池, 它提供了与符号表相同的信息,这个将在后面进行详述。
- 方法, 每个方法包含了四个部分:
- 签名及访问标记
- 字节码
- 行号表, 它主要用于为调试器提供信息,行号表指示了每个字节码指令对应的行数,如Java代码中的第六行对应了sayHello方法中的字节码0,第七行对应了字节码8.
- 局部变量表, 它列出了帧中提供的所有局部变量,在本例子中,只有this这一个局部变量。
此类文件中使用到的字节码操作数有:
aload_0
|
此操作码是格式为aload_<n>的操作码组的成员之一,它们都是用于加载一个对象引用到操作
对象栈中. <n>指示了对象在局部变量数组中的位置,可选的数字只能是0,1,2,3之一。还有其它类似的操作
码用于加载数值,而非对象引用。如iload_<n>,lload_<n>, fload_<n>和dload_<n>,其中i是指int类型
l是long,f是float,d是double类型。对于索引值超过3的局部变量,可以使用iload,lload,fload,dload进行
加载。这些操作码都只有一个操作数,用于指定要加载的局部变量的索引。
|
ldc
|
这个指令(操作码)用于将一个运行时常量池中的常量压入操作对象栈中
|
getstatic
|
此指令用于将运行时常量池的静态字段列表中的一个静态值压入到操作对象栈中。
|
invokespecial,
invokevirtual
|
这两个指令属于调用方法的指令组成员之一,调用方法的指令有:invokedynamic,invokestatic,
invokevirtual。在这个类文件中,invokevirtual用于基于对象的类进行方法调用,而invokespecial指令则用
于调用当前类的实例初始化方法,私有方法及父类方法。
|
return
|
此指令时返回型指令组成员之一,其它的还有ireturn,lreturn,freturn,dreturn,areturn及
return。每个指令都是一类返回不同类型数值的返回声明,i是int类型,l是long,f是float,d是
double,a则是对象引用。没有前置类型字符的return则是返回void类型。
|
在任何典型的字节码中,局部变量、操作对象栈和运行时常量池间操作数交互的主要过程如下:
构造函数中有两条指令,首先是aload_0将this压入到操作对象栈中,之后调用父类的构造函数,并使用this对象,因此this从操作对象栈中出栈。
sayHello()方法比构造函数复杂,因为它必须要使用运行时常量池将符号引用解析为直接引用,可参考上一篇翻译文章。第一个指令getstatic用于压入一个到System类的静态字段out的引用到操作对象栈中,下一个指令ldc把字符串“Hello”压入到操作对象栈中,最后的指令invokevirtual调用System.out的println方法,将“Hello”出栈并作为println方法的一个参数,为当前线程创建一个新的帧。整个过程如下:
(7) 类加载器
JVM通过使用bootstrap类加载器加载一个初始类进行启动。初始类在调用public static void main(String [])方法之前被链接和初始化。main方法的执行最终驱使了其它需要的附加类和接口的加载、链接和初始化。
加载是根据其特定的名字找到表示类或接口类型的类文件,并将其读取到byte数组中的过程。下一步将会解析byte数组,以确保它们表示的是一个类文件并具有正确的主及副版本号。此类的任何直接父类也会被加载。一旦加载过程完成,一个类或接口对象将被依据其二进制表示创建。
链接是进行类或接口的验证,准备类型及其直接父类和实现的接口的过程。链接包含了三个步骤:验证、准备和可选的解析。
1. 验证,是确定类或接口的表示是结构正确的,且遵守Java编程语言及JVM的语义要求,比如说它应该具有合适的符号表。
2. 准备,它涉及了静态存储空间的分配和JVM使用到的任何数据结构,如方法表的分配。静态字段被创建并初始化为它们的默认值,然而,此时没有初始化块或代码被执行,因此这些是在初始化阶段进行。
3. 解析,它是一个可选的阶段,包含有通过加载引用类或接口来检查符号引用,确认引用是正确的。如果没有在这里进行符号引用的解析,则可以将其推迟到字节码指令使用此符号引用之前进行。
类或接口的初始化包含执行类或接口的初始化方法<clinit>.
在JVM中有多个类加载器,它们扮演了不同的角色。每个类的加载被委托给它的父类加载器,除了bootstrap类加载器,因此它是最上层的类加载器。 Bootstrap类加载器的责任是加载基本的Java API,如rt.jar, 它只加载在具有更高的信任级别的根类路径中发现的类,也正因如此,它省去了许多对于普通的类需要进行的验证工作。JVM还包含了一个扩展类加载器(extension class loader),它用于加载标准Java 扩展API中的类,如安全扩展功能。而系统类加载器(system class loader)是默认的应用类加载器,它从classpath中加载应用类。
用户自定义类加载器也可以作为应用类加载器。使用用户自定义类加载器的原因有许多,如运行时重加载类,分开不同组的加载类,这个功能一般web服务器都需要用到,如Tomcat。
(8) 加速类的加载
从5.0版本开始,Hotspot JVM中引入了一个称为类数据共享(Class Data Sharing,CDS)的特色功能。在JVM的安装过程中,安装程序加载一系列的关键JVM类到一个内存映射的共享档案中(memory-mapped shared archive),如rt.jar。CDS可以减少用于加载这些类的时间,从而提高JVM的启动速度,并允许不同的JVM实例共享这些类,减少内存的使用。
(9) 方法区位于何处?
Java虚拟机规范中清楚的说明了:“尽管方法区逻辑上是属于堆的一部分,简单实现也许会选择既不对其进行垃圾回收,也不进行压缩。”与jconsole中显示Oracle JVM中方法区属于非堆内存相反,OpenJDK的代码显示CodeCache是VM的ObjectHeap的一个独立字段。
(10) 类加载器的引用
所有被加载的类都包含了一个到加载它们的类加载器的引用。反过来,类加载器同样包含了一个到它加载的所有类的引用。
(11) 运行时常量池
JVM为每种类型都维护了一个对应的常量池,常量池是一个运行时数据结构,有点类似于符号表,不过他包含了更多的数据。Java中的字节码需要数据,通常这些数据太大而无法直接存储在字节码中,因此将它们存储在常量池中,而字节码包含一个到常量池的引用。
有几种类型的数据会被存储在常量池中,如:
- 数值型字面值
- 字符串字面值
- 类引用
- 字段引用
- 方法引用
如下面的代码:
- Object foo = new Object();
其对应的字节码如下:
- 0: new #2 // Class java/lang/Object
- 1: dup
- 2: invokespecial #3 // Method java/ lang/Object "<init>"( ) V
new操作码后面跟着#2操作数,此操作数是一个常量池的索引,指向常量池中的第二个实体,第二个实体是一个类引用,此实体转而引用常量池中另一个包含类名字的实体,类的名字用一个值为java/lang/Object的UTF8字符串常量表示。之后,此符号链接可以用于查询java.lang.Object类。new操作码创建一个类实例并初始化它的变量,一个指向新创建的类实例的引用被添加到操作对象栈中。dup操作码则在操作对象栈顶创建此引用的两份拷贝。最后,通过invokespecial指令调用实例的初始化方法。这个指令的操作数同样包含了一个到常量池的引用,此初始化方法消耗操作数池顶端的一个引用作为方法的参数,最后,产生了一个指向已经被创建并初始化的新对象的引用。
(12) 异常表
异常表存储了每个异常处理器信息,如:
- 起始点
- 结束点
- 处理器代码的PC偏移值
- 被捕抓的异常类的常量池索引
如果一个方法定义了一个try-catch或try-finally异常处理器,则一个异常表将会被创建。异常表包含了每个异常处理器或者是finally块的信息,如异常处理应用的范围,那种类型的异常会被处理及异常处理代码所在的位置。
当一个异常被抛出时,JVM会在当前方法中查找匹配的异常处理器。