JVM剖析
这篇文章详细解释了Java虚拟机的内部架构。以下这幅图展示了Java虚拟机里面的关键组件(是依据Java SE 7版本的Java虚拟机)。
这些组件将在下面的两个章节一一展开。第一章节涵盖了创建每一个线程(Thread)的组件;第二章节涵盖了独立于线程外的组件。
- 线程(Threads)
1 JVM系统线程
2 单线程
3 程序计数器(PC)
4 栈
5 局部栈(Native Stack)
6 栈约束
7 Frame
8 局部变量数组
9 操作符栈
10 动态链接
- 线程之间共享(Shared Between Threads)
1 堆
2 内存管理
3 非堆内存
4 实时编译(Just In Time Compilation)
5 方法域
6 类文件结构
7 类加载器
8 加快类的加载
9 方法域在哪里
10 引用类加载器
11 运行时常量池(Run Time Constant Pool)
12 异常表
13 符号表
14 内部字符串(字符串表)
线程
一个线程是程序的一次执行。JVM允许一个应用程序并行运行多个线程。在Hotspot JVM中存在着在Java线程和本地操作系统线程之间的映射。在准备完一个Java线程的所有状态,像thread-local存储、缓冲分配、对象同步、栈和程序计数器之后,本地线程就被创造出来了。本地线程将在Java线程终止后被回收。操作系统因此负责调度所有的线程和分配给它们可用的CPU。一旦本地线程初始化后,它将调用Java 线程里面的run()方法。当run()方法返回时,未捕捉的异常被处理,随后本地线程确定JVM是否需要因为线程的终止而被终止(比如说,它是最后的非守护线程)。当线程终止后,所有本地线程的和JVM线程的资源将被释放。
JVM系统线程
如果你是用jconsole或者其他任何调试器是有可能看到后台有无数的线程在运行。这些后台线程运行是来补充主线程的,其部分是由于调用public static void main(String[])而构造出来的。在Hotspot JVM系统中主要的后台系统线程如下:
1.VM 线程:这个线程等待那些需要JVM到达一个安全点(Safe-point)的操作。之所以这些操作需要发生在一个独立的线程是因为它们都需要JVM在一个安全点(safe-point)、这个点对堆的修改不会出现。线程执行的操作是stop-the-world型的垃圾回收器、线程栈dumps、thread suspension、和biased locking revocation。
2.Periodic task 线程: 这些线程是负责哪些用于调度阶段性的操作执行的时间事件(比如中断)
3.GC 线程: 这些线程支持不同类型的垃圾回收活动
4.Compiler 线程: 这些线程在运行时将byte code编译成本地码
5.Signal dispatcher thread 这个线程接受发送给JVM进程的信号并且通过调用合适的JVM方法处理它们。
单线程
每一个线程有以下这些组件:
程序计数器
除非它是本地的,那PC表示当前指令的地址。如果当前的方法是本地的那么PC是未被定义的。所有的CPUs 都拥有一个PC,典型的情况是在一条指令执行之后PC会增加,因此保存要执行的下一条指令的地址。JVM使用PC去追踪当前执行的指令,事实上PC将会指向方法域(Method Area)的一个内存地址
栈
每一个线程都会拥有一个装有当前执行的每一个方法的栈。栈是一个LIFO数据结构,所以当前执行的方法将会在栈顶。每当一个新的方法被调用,一个新的框架会被创建并且push到栈顶。当方法正常返回或者一个未捕捉的异常抛出时框架将会被移除(Pop)。
局部栈
不是所有的JVMs支持局部栈,然而,那些支持的将会为每一个线程创建一个局部方法栈。如果JVM已经使用C-linkage模型为Java Native Invocation(JNI)实现,那么局部栈将会是C栈。在这种情况下参数的顺序和返回值将会和C语言的局部栈完全一样。一个局部栈可以调用至JVM并且调用一个java方法;像native to Java 调用将会发生在Java stack中。
stack restrictions
一个栈可以是动态的或者固定的大小。如果一个线程需要一个大的栈那么一个StackOverflowError异常被抛出。如果一个线程需要一个行的框架并且没有足够的存储分配那么一个OutOfMemoryError异常被抛出。
Frame
每当一个方法被调用,一个新的框架被创建并且push进栈顶。
每个框架包括如下:
1.local variable array
2.Return value
3.Operand stack
4.Reference to runtime constant pool for class of the current method
Local Variables Arrays
local variables arrays包括方法执行期间的变量:包括一个this引用,所有的方法参数和其他局部定义变量。对于类方法(比如.静态方法),方法参数从零开始;然而对于实例方法,零槽位保留给this。
一个局部变量可以是:
.boolean
.byte
.char
.long
.short
.int
.float
.double
.reference
.returnAddress
Operand stack
有点类似CPU中一般目的的寄存器,the operand stack在执行byte code指令时使用。大多数JVM byte code 将时间花费在操纵operand stack上:比如push,pop,duplicate,swap,或者执行产生或者消费变量的操作上。因此,在byte code中,那些在局部变量数组和operand stack之间的移动数据是非常频繁的。例如,一个简单的变量初始化会产生两个与operand stack打交道的byte codes。
int i;
会得到编译后的byte code:
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
Dynamic linking
每个框架包含一个指向运行时常量池的引用。该引用指向对应类执行的方法对应的框架(Frame)的常量池。这个引用有助于支持动态链接。
C/C++代码通常是编译成一个对象文件(.o文件)然后多个对象文件被链接并生成一个可用的产品比如说exe或者dll文件。在链接阶段,每个对象的符号引用被替换成一个实际的内存地址。在java中这个链接阶段发生在运行时。
当一个Java类被编译时,所有的指向变量和方法的引用被作为一个符号引用保存在该类常量池中。符号引用是一个逻辑引用而不是一个指向实际地址的引用。JVM实现版本可是选择何时解决这些标记引用,这过程可以放生在类文件被识别时、加载之后,这叫做贪心或者静态解析;或者发生在当此标记引用被使用时,这叫做懒惰或者后期解析。绑定是以标记引用标识的区域(field)、方法、或者类被直接引用代替的过程。所有的直接引用都会以运行位置相关的存储结构偏移量类存储的。
线程共享
Heap
堆是用于在运行时分配类实例和数组的。因为一个框架(Frame)在创建后不是被设计为可以改变的,所以数组和对象永远不会存储在栈中。框架只会存储指向堆中的对象或者数组的引用。不像原始变量和引用存储在局部变量数组(在框架)中,对象总是存储在堆中,所以它们在一个方法结束后不会被移除。相反对象只会被垃圾收集器移除。
为了支持垃圾手机机制,堆被分成三部分:
.Young Generation(年轻代):经常被分为Eden和Survivor
.Old Generation(老一代):也被成为终生(Tenured)代
.Permanent Generation(持久代)
Memory Management
对象和数组从来不会显示的被回收除了垃圾收集器自动的回收它们。
大多数情况下这个工作如下:
1.新的对象和数组被创建在年轻代。
2.较小的垃圾收集器在新的一代中操作。那些依然活着的对象,将会从死(Eden)的空间移动到活(survivor)的空间。
3.较大的垃圾收集器,使应用线程暂停,将会在世代(generations)间移动对象。那些活着的对象,将会从新一代移动到老的一代。
4.当老一代每次收集时,永久一代(permanent generation)也会收集一次。
Non-Heap Memory
那些被逻辑上认为是JVM机制的一部分对象不是被创建在堆中。
非堆内存包括:
1.永久一代(permanent generation), 包括:
方法域(method area)
内部字符串
2.缓存代码
Just In Time(JIT) Compilation
Java byte code 是被解释的但是这个并没有直接在JVM的宿主机CPU上执行本地代码快。为了改善性能,Oracle Hotspot VM寻找那些频繁执行的byte code代码并将它们编译成本地码。本地代码然后存放在缓存代码中(code cache)。用这种方法,Hotspot VM试着选择最合适的方法去在编译代码和执行解释的代码之间取得平衡。
Method Area
方法域存放着每个类的信息,就像:
1.类加载器引用
2.运行时常量池
.数值常量
.区域引用
.方法引用
.属性
3.域(Field)数据
.每个区域:
.名称
.类型
.修改器
.属性值
4.方法数据
.每个方法
.名称
.返回值
.参数类型(按顺序)
.修改器
.属性值
5.方法代码
.每个方法:
.Bytecodes
.操作符栈大小
.局部变量大小
.局部变量表
.异常表:
. 每个异常句柄:
.开始指针
.结束指针
.句柄代码的PC偏移量
.常量池索引
所有的线程共享相同的方法域,所以对于方法域的读取和动态连接的过程必须是线程安全的。如果两个线程试图读取一个尚未加载的类的域(Field)或者方法(Method),它必须加载一次而且停止等待。
类文件结构
一个编译后的类文件结构如下:
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];}
magic、minor_version、major_version:指明类的版本和用来编译的JDK的版本
constant_pool: 类似符号表,但包含更多的信息。
access_flag: 提供这个类的修改器列表
this_class:通过提供这个类的完整的index找到常数池,比如:org/jamesdbloom/foo/bar
super_class: 通过提供父类的完整路径的索引指向常数池,比如:java/lang/Object
interface: 通过提供所有指向已经实现的接口的索引。。
fields: 通过提供完整的每个域的描述指向常数池的数组索引
methods: 通过提供每个方法的完整的签名。。。
attributes: 略。
比如编译一下简单的类:
package org.jvminternals;public class SimpleClass { public void sayHello() { System.out.println("Hello"); }}
那么通过下面指令:
javap -v -p -s -sysinfo -constantsclasses/org/javinternals/SimpleClass.class
将会得到,如下:
public class org.jvminternals.SimpleClass SourceFile: "SimpleClass.java" minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #6.#17 // java/lang/Object."":()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 #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 // " ":()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." ":()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方法
常量池:这个提供了一个标识表通常提供的信息。
方法(s):每一个包含了四个部分:
.签名和access flag
.字节码
.LineNumberTable:这个为debugger提供了那一行代码对应的字节码指令,比如说:Java代码第6行对应着sayHello方法中byte code 0 以及Java打啊代码中第7行对应 byte code 7
.局部变量表: 这个展示了框架中所有的局部变量。
以下是本代码中的byte code指令:
aload_0:
ldc:将常量池中的一个常量push进operand stack
getstatic:将一个静态值从运行时常量池中的静态域push进operand stack
invokespecial,invokevirtual: 这是一组执行方法的指令,像invokedynamic, invokeinterface,invokespecial, invokestatic, invokevirtual. invokevirtual是调用对象类中的方法,invokespecial是调用实例初始化方法向私用方法和父类的方法。
return: 这是一组指令,向lreturn, freturn, dreturn, areturn, return。每一种return前面都代表着返回值的类型。
大多数的bytecode都和局部变量,operand stack,和运行时常量池。
构造器有两条指令,第一条this被push进operand stack, 第二条对应父类的构造器被调用,它消耗了this,所以this被pop出operand stack。
sayHello方法更加复杂因为它必须利用运行时常量池解决标识引用到实际引用的问题。第一条getstatic指令是用来从常量池中push一个引用到operand stack中。第二条ldc指令push了一个“Hello”字符串到operand stack中。第三条invokevirtual指令调用println方法,它将会将“Hello”字符串从operand stack中作为一个参数pop出来并且为当前线程创建一个新的框架。
类加载器
JVM通过使用bootstrap类加载器来加载一个初始化类的方法来启动。这个类然后在调用public static void main(String[])方法前链接并且初始化。
Loading是找到那个表示类或者接口类型的类文件的过程。第二步就是被解析确认它们表示了一个正确的类对象已经拥有正确的major 和minor versions。所有的以一个直接父类的类或者接口也会被加载进来。一旦这个过程完成,一个类或者接口就会以二进制数据的形式表现出来。
Linking是认证类型和它的父类或者父接口的过程。Linking包括三个步骤:认证(verifying)、准备和可选的解析。
Verifying就是确定类或接口结构正确或者符合JVM的语法规范。比如:
1.连续且正确格式的符号表
2.Final方法或者类没有被覆写(overridden)
3.method have correct number and type of parameters
4.方法有正确的参数个数和类型
5.bytecode没有错误操作栈
6.变量读前初始化
7.变量的格式正确
在认证阶段做这些检查意味着在运行时不需要做这些检查。在Linking时执行认证操作会减缓Loading的速度,但是它避免了在执行bytecode是做这些检查。
Preparing为静态存储分配内存并且为JVM所必需的数据(像方法表)分配内存。静态域被创建并初始化为缺省值。
Resolving是一个可选项,它主要包含通过加载引用的类或者接口来检查符号引用。
Initialization
类或者接口的初始化主要由执行类或者接口的初始化方法<clinit>构成。
在JVM中有多种类加载器担任着不同的角色。每一个类加载器都委托给它的父加载器,除了bootstrap classloader。
Bootstrap类加载器通常是由局部码实现的,因为它在JVM被装载时就被实例化了。Bootstrap类加载器负责加载基础的Java API,像rt.jar。它只加载在启动路径上的类。
Extension 类加载器加载标准的Java扩展API,像安全扩展功能。
System类加载器是缺省的应用程序类加载器,它从路径中加载应用程序类。
User Defined类加载器是用户自定义类加载器。
Faster Class Loading
在Hotspot JVM中一种叫做Class Data Sharing的特性被引入。在JVM安装的过程中一系列关键的JVM类被加载进一个内存映射的共享模块。CDS降低了加载这些类的时间,改善了JVM启动的速度并且允许这些类的共享。
方法域在哪里:存在非堆(No-Heap)
类加载器引用
被加载的所有类都包含一个指向加载它们的类加载器的引用。反过来,类加载器同样包含一个指向所有它加载的类的引用。
运行时常量池
Java的bytecode需要数据,但是常常这些数据都太大以至于不能直接存储在bytecode中,所以它存储在常量池中并且bytecode保存着指向常量池的引用。运行时常量池使用的是动态链接。
一些存储在常量池中的数据有如下类型:
1.numeric literaal:数值常量
2.String literals:字面串常量String a = “123”
3.Class references:类引用
4.Field reference:域引用
5.Method reference:方法引用
比如说:
Object foo = new Object()
它的bytecode代码如下:
0: new #2 // Class java/lang/Object 1: dup 2: invokespecial #3 // Method java/ lang/Object ""( ) V
new指令后接#2,这个操作数是一个指向常量池的引用,索引值为2,第二个索引是一个类引用,这个所以接着指向常量池中另外一个索引: UTF8 Class java/lang/Object。这个符号连接可以被用来找到java.lang.Object的类。new操作符创造了一个类实例并且初始化它的变量。一个指向类实例的引用然后被加入到operand stack中。Dup指令然后复制了operand stack栈顶的引用并再次加入到栈顶。最后,一个实例初始化方法被调用invokespecial。这个指令同样包含了一个指向常量池的引用。操作符将栈顶的引用当着参数消耗掉。在最后只用一个指向新对象的引用存在。
如果编译一下的简单代码:
package org.jvminternals;public class SimpleClass { public void sayHello() { System.out.println("Hello"); }}
常量池将会如下:
Constant pool: #1 = Methodref #6.#17 // java/lang/Object."":()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 #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 // " ":()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
常量池主要包括一下类型:
Interger: 一个4字节的常量
Long: 一个8字节的常量
Float: 一个4字节的浮点数常量
Double: 一个8字节的浮点数常量
String: 一个字符串常量指向一个UTF8的入口(他包含真正的字符串常量)。
UTF8: 一个以UTF8编码的字节流。
Class: 一个指向另外一个UTF8入口的类常量(此入口包含真正的类名)
NameAndType:一个以“.”分割的值,每一个指向常量池的另一个入口。第一个值指向一个UTF8入口(它是一个方法或者域名称),第二个指向另一个UTF8入口(它表示类型)。如果第一个是域名称,第二个则是一个完整的类名。如果第一个是方法名,第二个这是参数列表。
Fieldref,Methodref,InterfaceMethodref: 一个以“.”分割的值,每一个值指向常量池的另一个入口。第一个入口指向一个类入口,第二个指向一个NameAndType入口。
异常表
异常表存储了每个异常句柄的信息,像:
1.起位置
2.终位置
3.PC偏移量
4.正捕捉异常的常量池索引
如果一个方法定义了一个try-catch或者try-final的异常句柄,那么一个异常表就会被创建。这包括了每个异常句柄的信息或者final块信息。
当一个异常被抛出时,JVM就会在当前方法中搜素匹配的句柄;如果没有找到匹配的,方法将会终止、从当前栈中弹出并且异常在新的当前框架中被重新抛出。如果在所有框架没有完全弹出栈之前没有找到匹配的异常句柄,这个线程就会被终止。如果这个线程是最后的非守护线程,那么JVM将会自终止,例如这个线程是主线程。
符号表
在Hotspot JVM中,除了每一种类型的运行时常量池,还有永恒一代中的符号表。符号表是一个哈希映射指针指向对应的符号(例如:Hashtable<Symbol*, Symbol>)并且包含一个指向所有符号的指针。
引用计数器是用于当一个符号从符号表中移除时。例如:当一个类被unloaded时,所有它的运行时常量池的符号引用计数器都将会减少。当一个符号的符号引用计数器都减少到0时,符号表知道这个符号不再被引用而且会被移除出符号表。对于符号表和字符串表(将在下面讲),所有的入口都以固定的格式存储以保证效率。
内部字符串(字符串表)
Java语言的特性是对于相同的字符串常量引用,必须指向同一个字符串实例。另外在字符串实例化时如果调用了String.intern(),对于如果字符串是常量,相同的字符串引用必须相同。例如下面:
("j" + "v" + "m").intern() == "jvm"
将返回true
在Hotspot JVM中内部字符串存储在字符串表中,它是一个可哈希搜索的对象-符号的字符串表(例如:Hashtable<oop, Symbol>)。并且存储在永恒一代中。
当类加载时,字符串常量是自动通过编译器被内部化并加入到字符串表中的。另外字符串类实例也可以显示的通过调用String.intern()被内部化的。当String.intern()被调用时,如果字符串表已经保存了这个字符串那么字符串引用将被返回;如果没有,则加入到字符串表而且它的引用返回。
参考文献:
1.
2.http://blog.itpub.net/91710/viewspace-841798/