这篇文章解释了Java 虚拟机(JVM)的内部架构。下图显示了遵照Java SE 7 规范的典型的 JVM 核心内部组件。html
上图显示的组件分两个章节解释。第一章讨论针对每一个线程建立的组件,第二章节讨论了线程无关组件。java
这里所说的线程指程序执行过程当中的一个线程实体。JVM 容许一个应用并发执行多个线程。Hotspot JVM 中的 Java 线程与原生操做系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好之后,就会建立一个操做系统原生线程。Java 线程结束,原生线程随之被回收。操做系统负责调度全部线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。run() 返回时,被处理未捕获异常,原生线程将确认因为它的结束是否要终止 JVM 进程(好比这个线程是最后一个非守护线程)。当线程结束时,会释放原生线程和 Java 线程的全部资源。web
若是使用 jconsole 或者其它调试器,你会看到不少线程在后台运行。这些后台线程与触发 public static void main(String[]) 函数的主线程以及主线程建立的其余线程一块儿运行。Hotspot JVM 后台运行的系统线程主要有下面几个:bootstrap
虚拟机线程(VM thread) | 这个线程等待 JVM 到达安全点操做出现。这些操做必需要在独立的线程里执行,由于当堆修改没法进行时,线程都须要 JVM 位于安全点。这些操做的类型有:stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。 |
周期性任务线程 | 这线程负责定时器事件(也就是中断),用来调度周期性操做的执行。 |
GC 线程 | 这些线程支持 JVM 中不一样的垃圾回收活动。 |
编译器线程 | 这些线程在运行时将字节码动态编译成本地平台相关的机器码。 |
信号分发线程 | 这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。 |
每一个运行的线程都包含下面这些组件:数组
PC 指当前指令(或操做码)的地址,本地指令除外。若是当前方法是 native 方法,那么PC 的值为 undefined。全部的 CPU 都有一个 PC,典型状态下,每执行一条指令 PC 都会自增,所以 PC 存储了指向下一条要被执行的指令地址。JVM 用 PC 来跟踪指令执行的位置,PC 将其实是指向方法区(Method Area)的一个内存地址。缓存
每一个线程拥有本身的栈,栈包含每一个方法执行的栈帧。栈是一个后进先出(LIFO)的数据结构,所以当前执行的方法在栈的顶部。每次方法调用时,一个新的栈帧建立并压栈到栈顶。当方法正常返回或抛出未捕获的异常时,栈帧就会出栈。除了栈帧的压栈和出栈,栈不能被直接操做。因此能够在堆上分配栈帧,而且不须要连续内存。安全
并不是全部的 JVM 实现都支持本地(native)方法,那些提供支持的 JVM 通常都会为每一个线程建立本地方法栈。若是 JVM 用 C-linkage 模型实现 JNI(Java Native Invocation),那么本地栈就是一个 C 的栈。在这种状况下,本地方法栈的参数顺序、返回值和典型的 C 程序相同。本地方法通常来讲能够(依赖 JVM 的实现)反过来调用 JVM 中的 Java 方法。这种 native 方法调用 Java 会发生在栈(通常是 Java 栈)上;线程将离开本地方法栈,并在 Java 栈上开辟一个新的栈帧。服务器
栈能够是动态分配也能够固定大小。若是线程请求一个超过容许范围的空间,就会抛出一个StackOverflowError。若是线程须要一个新的栈帧,可是没有足够的内存能够分配,就会抛出一个 OutOfMemoryError。数据结构
每次方法调用都会新建一个新的栈帧并把它压栈到栈顶。当方法正常返回或者调用过程当中抛出未捕获的异常时,栈帧将出栈。更多关于异常处理的细节,能够参考下面的异常信息表章节。多线程
每一个栈帧包含:
局部变量数组包含了方法执行过程当中的全部变量,包括 this 引用、全部方法参数、其余局部变量。对于类方法(也就是静态方法),方法参数从下标 0 开始,对于对象方法,位置0保留为 this。
有下面这些局部变量:
除了 long 和 double 类型之外,全部的变量类型都占用局部变量数组的一个位置。long 和 double 须要占用局部变量数组两个连续的位置,由于它们是 64 位双精度,其它类型都是 32 位单精度。
操做数栈在执行字节码指令过程当中被用到,这种方式相似于原生 CPU 寄存器。大部分 JVM 字节码把时间花费在操做数栈的操做上:入栈、出栈、复制、交换、产生消费变量的操做。所以,局部变量数组和操做数栈之间的交换变量指令操做经过字节码频繁执行。好比,一个简单的变量初始化语句将产生两条跟操做数栈交互的字节码。
1 int i;
被编译成下面的字节码:
1 0: iconst_0 // Push 0 to top of the operand stack 2 1: istore_1 // Pop value from top of operand stack and store as local variable 1
更多关于局部变量数组、操做数栈和运行时常量池之间交互的详细信息,能够在类文件结构部分找到。
每一个栈帧都有一个运行时常量池的引用。这个引用指向栈帧当前运行方法所在类的常量池。经过这个引用支持动态连接(dynamic linking)。
C/C++ 代码通常被编译成对象文件,而后多个对象文件被连接到一块儿产生可执行文件或者 dll。在连接阶段,每一个对象文件的符号引用被替换成了最终执行文件的相对偏移内存地址。在 Java中,连接阶段是运行时动态完成的。
当 Java 类文件编译时,全部变量和方法的引用都被当作符号引用存储在这个类的常量池中。符号引用是一个逻辑引用,实际上并不指向物理内存地址。JVM 能够选择符号引用解析的时机,一种是当类文件加载并校验经过后,这种解析方式被称为饥饿方式。另一种是符号引用在第一次使用的时候被解析,这种解析方式称为惰性方式。不管如何 ,JVM 必需要在第一次使用符号引用时完成解析并抛出可能发生的解析错误。绑定是将对象域、方法、类的符号引用替换为直接引用的过程。绑定只会发生一次。一旦绑定,符号引用会被彻底替换。若是一个类的符号引用尚未被解析,那么就会载入这个类。每一个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联的)偏移量。
堆被用来在运行时分配类实例、数组。不能在栈上存储数组和对象。由于栈帧被设计为建立之后没法调整大小。栈帧只存储指向堆中对象或数组的引用。与局部变量数组(每一个栈帧中的)中的原始类型和引用类型不一样,对象老是存储在堆上以便在方法结束时不会被移除。对象只能由垃圾回收器移除。
为了支持垃圾回收机制,堆被分为了下面三个区域:
对象和数组永远不会显式回收,而是由垃圾回收器自动回收。一般,过程是这样的:
非堆内存指的是那些逻辑上属于 JVM 一部分对象,但实际上不在堆上建立。
非堆内存包括:
Java 字节码是解释执行的,可是没有直接在 JVM 宿主执行原生代码快。为了提升性能,Oracle Hotspot 虚拟机会找到执行最频繁的字节码片断并把它们编译成原生机器码。编译出的原生机器码被存储在非堆内存的代码缓存中。经过这种方法,Hotspot 虚拟机将权衡下面两种时间消耗:将字节码编译成本地代码须要的额外时间和解释执行字节码消耗更多的时间。
方法区存储了每一个类的信息,好比:
全部线程共享同一个方法区,所以访问方法区数据的和动态连接的进程必须线程安全。若是两个线程试图访问一个还未加载的类的字段或方法,必须只加载一次,并且两个线程必须等它加载完毕才能继续执行。
一个编译后的类文件包含下面的结构:
1 ClassFile { 2 u4 magic; 3 u2 minor_version; 4 u2 major_version; 5 u2 constant_pool_count; 6 cp_info contant_pool[constant_pool_count – 1]; 7 u2 access_flags; 8 u2 this_class; 9 u2 super_class; 10 u2 interfaces_count; 11 u2 interfaces[interfaces_count]; 12 u2 fields_count; 13 field_info fields[fields_count]; 14 u2 methods_count; 15 method_info methods[methods_count]; 16 u2 attributes_count; 17 attribute_info attributes[attributes_count]; 18 }
magic, minor_version, major_version | 类文件的版本信息和用于编译这个类的 JDK 版本。 |
constant_pool | 相似于符号表,尽管它包含更多数据。下面有更多的详细描述。 |
access_flags | 提供这个类的描述符列表。 |
this_class | 提供这个类全名的常量池(constant_pool)索引,好比org/jamesdbloom/foo/Bar。 |
super_class | 提供这个类的父类符号引用的常量池索引。 |
interfaces | 指向常量池的索引数组,提供那些被实现的接口的符号引用。 |
fields | 提供每一个字段完整描述的常量池索引数组。 |
methods | 指向constant_pool的索引数组,用于表示每一个方法签名的完整描述。若是这个方法不是抽象方法也不是 native 方法,那么就会显示这个函数的字节码。 |
attributes | 不一样值的数组,表示这个类的附加信息,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 注解。 |
能够用 javap 查看编译后的 java class 文件字节码。
若是你编译下面这个简单的类:
1 package org.jvminternals; 2 public class SimpleClass { 3 public void sayHello() { 4 System.out.println("Hello"); 5 } 6 }
运行下面的命令,就能够获得下面的结果输出: javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class。
1 public class org.jvminternals.SimpleClass 2 SourceFile: "SimpleClass.java" 3 minor version: 0 4 major version: 51 5 flags: ACC_PUBLIC, ACC_SUPER 6 Constant pool: 7 #1 = Methodref #6.#17 // java/lang/Object."<init>":()V 8 #2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream; 9 #3 = String #20 // "Hello" 10 #4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V 11 #5 = Class #23 // org/jvminternals/SimpleClass 12 #6 = Class #24 // java/lang/Object 13 #7 = Utf8 <init> 14 #8 = Utf8 ()V 15 #9 = Utf8 Code 16 #10 = Utf8 LineNumberTable 17 #11 = Utf8 LocalVariableTable 18 #12 = Utf8 this 19 #13 = Utf8 Lorg/jvminternals/SimpleClass; 20 #14 = Utf8 sayHello 21 #15 = Utf8 SourceFile 22 #16 = Utf8 SimpleClass.java 23 #17 = NameAndType #7:#8 // "<init>":()V 24 #18 = Class #25 // java/lang/System 25 #19 = NameAndType #26:#27 // out:Ljava/io/PrintStream; 26 #20 = Utf8 Hello 27 #21 = Class #28 // java/io/PrintStream 28 #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V 29 #23 = Utf8 org/jvminternals/SimpleClass 30 #24 = Utf8 java/lang/Object 31 #25 = Utf8 java/lang/System 32 #26 = Utf8 out 33 #27 = Utf8 Ljava/io/PrintStream; 34 #28 = Utf8 java/io/PrintStream 35 #29 = Utf8 println 36 #30 = Utf8 (Ljava/lang/String;)V 37 { 38 public org.jvminternals.SimpleClass(); 39 Signature: ()V 40 flags: ACC_PUBLIC 41 Code: 42 stack=1, locals=1, args_size=1 43 0: aload_0 44 1: invokespecial #1 // Method java/lang/Object."<init>":()V 45 4: return 46 LineNumberTable: 47 line 3: 0 48 LocalVariableTable: 49 Start Length Slot Name Signature 50 0 5 0 this Lorg/jvminternals/SimpleClass; 51 52 public void sayHello(); 53 Signature: ()V 54 flags: ACC_PUBLIC 55 Code: 56 stack=2, locals=1, args_size=1 57 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 58 3: ldc #3 // String "Hello" 59 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 60 8: return 61 LineNumberTable: 62 line 6: 0 63 line 7: 8 64 LocalVariableTable: 65 Start Length Slot Name Signature 66 0 9 0 this Lorg/jvminternals/SimpleClass; 67 }
这个 class 文件展现了三个主要部分:常量池、构造器方法和 sayHello 方法。
这个 class 文件用到下面这些字节码操做符:
aload0 | 这个操做码是aload格式操做码中的一个。它们用来把对象引用加载到操做码栈。 表示正在被访问的局部变量数组的位置,但只能是0、一、二、3 中的一个。还有一些其它相似的操做码用来载入非对象引用的数据,如iload, lload, float 和 dload。其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double。局部变量数组位置大于 3 的局部变量能够用 iload, lload, float, dload 和 aload 载入。这些操做码都只须要一个操做数,即数组中的位置 |
ldc | 这个操做码用来将常量从运行时常量池压栈到操做数栈 |
getstatic | 这个操做码用来把一个静态变量从运行时常量池的静态变量列表中压栈到操做数栈 |
invokespecial, invokevirtual | 这些操做码属于一组函数调用的操做码,包括:invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual。在这个 class 文件中,invokespecial 和 invokevirutal 两个指令都用到了,二者的区别是,invokevirutal 指令调用一个对象的实例方法,invokespecial 指令调用实例初始化方法、私有方法、父类方法。 |
return | 这个操做码属于ireturn、lreturn、freturn、dreturn、areturn 和 return 操做码组。每一个操做码返回一种类型的返回值,其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double,a 表示 对象引用。没有前缀类型字母的 return 表示返回 void |
跟任何典型的字节码同样,操做数与局部变量、操做数栈、运行时常量池的主要交互以下所示。
构造器函数包含两个指令。首先,this 变量被压栈到操做数栈,而后父类的构造器函数被调用,而这个构造器会消费 this,以后 this 被弹出操做数栈。
sayHello() 方法更加复杂,正如以前解释的那样,由于它须要用运行时常量池中的指向符号引用的真实引用。第一个操做码 getstatic 从System类中将out静态变量压到操做数栈。下一个操做码 ldc 把字符串 “Hello” 压栈到操做数栈。最后 invokevirtual 操做符会调用 System.out 变量的 println 方法,从操做数栈做弹出”Hello” 变量做为 println 的一个参数,并在当前线程开辟一个新栈帧。
JVM 启动时会用 bootstrap 类加载器加载一个初始化类,而后这个类会在public static void main(String[])调用以前完成连接和初始化。执行这个方法会执行加载、连接、初始化须要的额外类和接口。
加载(Loading)是这样一个过程,找到表明这个类的 class 文件或根据特定的名字找到接口类型,而后读取到一个字节数组中。接着,这些字节会被解析检验它们是否表明一个 Class 对象并包含正确的 major、minor 版本信息。直接父类的类和接口也会被加载进来。这些操做一旦完成,类或者接口对象就从二进制表示中建立出来了。
连接(Linking)是校验类或接口并准备类型和父类父接口的过程。连接过程包含三步:校验(verifying)、准备(preparing)、部分解析(optionally resolving)。
校验会确认类或者接口表示是否结构正确,以及是否遵循 Java 语言和 JVM 的语义要求,好比会进行下面的检查:
在验证阶段作这些检查意味着不须要在运行阶段作这些检查。连接阶段的检查减慢了类加载的速度,可是它避免了执行这些字节码时的屡次检查。
准备过程包括为静态存储和 JVM 使用的数据结构(好比方法表)分配内存空间。静态变量建立并初始化为默认值,可是初始化代码不在这个阶段执行,由于这是初始化过程的一部分。
解析是可选的阶段。它包括经过加载引用的类和接口来检查这些符号引用是否正确。若是不是发生在这个阶段,符号引用的解析要等到字节码指令使用这个引用的时候才会进行。
类或者接口初始化由类或接口初始化方法<clinit>
的执行组成。
JVM 中有多个类加载器,分饰不一样的角色。每一个类加载器由它的父加载器加载。bootstrap 加载器除外,它是全部最顶层的类加载器。
共享类数据(CDS)是Hotspot JVM 5.0 的时候引入的新特性。在 JVM 安装过程当中,安装进程会加载一系列核心 JVM 类(好比 rt.jar)到一个共享的内存映射区域。CDS 减小了加载这些类须要的时间,提升了 JVM 启动的速度,容许这些类被不一样的 JVM 实例共享,同时也减小了内存消耗。
The Java Virtual Machine Specification Java SE 7 Edition 中写得很清楚:“尽管方法区逻辑上属于堆的一部分,简单的实现能够选择不对它进行回收和压缩。”。Oracle JVM 的 jconsle 显示方法区和 code cache 区被当作为非堆内存,而 OpenJDK 则显示 CodeCache 被当作 VM 中对象堆(ObjectHeap)的一个独立的域。
全部的类加载以后都包含一个加载自身的加载器的引用,反过来每一个类加载器都包含它们加载的全部类的引用。
JVM 维护了一个按类型区分的常量池,一个相似于符号表的运行时数据结构。尽管它包含更多数据。Java 字节码须要数据。这个数据常常由于太大不能直接存储在字节码中,取而代之的是存储在常量池中,字节码包含这个常量池的引用。运行时常量池被用来上面介绍过的动态连接。
常量池中能够存储多种类型的数据:
示例代码以下:
1 Object foo = new Object();
写成字节码将是下面这样:
1 0: new #2 // Class java/lang/Object 2 1: dup 3 2: invokespecial #3 // Method java/ lang/Object "<init>"( ) V
new 操做码的后面紧跟着操做数 #2 。这个操做数是常量池的一个索引,表示它指向常量池的第二个实体。第二个实体是一个类的引用,这个实体反过来引用了另外一个在常量池中包含 UTF8 编码的字符串类名的实体(// Class java/lang/Object)。而后,这个符号引用被用来寻找 java.lang.Object 类。new 操做码建立一个类实例并初始化变量。新类实例的引用则被添加到操做数栈。dup 操做码建立一个操做数栈顶元素引用的额外拷贝。最后用 invokespecial 来调用第 2 行的实例初始化方法。操做码也包含一个指向常量池的引用。初始化方法把操做数栈出栈的顶部引用当作此方法的一个参数。最后这个新对象只有一个引用,这个对象已经完成了建立及初始化。
若是你编译下面的类:
1 package org.jvminternals; 2 public class SimpleClass { 3 4 public void sayHello() { 5 System.out.println("Hello"); 6 } 7 8 }
生成的类文件常量池将是这个样子:
1 Constant pool: 2 #1 = Methodref #6.#17 // java/lang/Object."<init>":()V 3 #2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream; 4 #3 = String #20 // "Hello" 5 #4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V 6 #5 = Class #23 // org/jvminternals/SimpleClass 7 #6 = Class #24 // java/lang/Object 8 #7 = Utf8 <init> 9 #8 = Utf8 ()V 10 #9 = Utf8 Code 11 #10 = Utf8 LineNumberTable 12 #11 = Utf8 LocalVariableTable 13 #12 = Utf8 this 14 #13 = Utf8 Lorg/jvminternals/SimpleClass; 15 #14 = Utf8 sayHello 16 #15 = Utf8 SourceFile 17 #16 = Utf8 SimpleClass.java 18 #17 = NameAndType #7:#8 // "<init>":()V 19 #18 = Class #25 // java/lang/System 20 #19 = NameAndType #26:#27 // out:Ljava/io/PrintStream; 21 #20 = Utf8 Hello 22 #21 = Class #28 // java/io/PrintStream 23 #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V 24 #23 = Utf8 org/jvminternals/SimpleClass 25 #24 = Utf8 java/lang/Object 26 #25 = Utf8 java/lang/System 27 #26 = Utf8 out 28 #27 = Utf8 Ljava/io/PrintStream; 29 #28 = Utf8 java/io/PrintStream 30 #29 = Utf8 println 31 #30 = Utf8 (Ljava/lang/String;)V
这个常量池包含了下面的类型:
Integer | 4 字节常量 |
Long | 8 字节常量 |
Float | 4 字节常量 |
Double | 8 字节常量 |
String | 字符串常量指向常量池的另一个包含真正字节 Utf8 编码的实体 |
Utf8 | Utf8 编码的字符序列字节流 |
Class | 一个 Class 常量,指向常量池的另外一个 Utf8 实体,这个实体包含了符合 JVM 内部格式的类的全名(动态连接过程须要用到) |
NameAndType | 冒号(:)分隔的一组值,这些值都指向常量池中的其它实体。第一个值(“:”以前的)指向一个 Utf8 字符串实体,它是一个方法名或者字段名。第二个值指向表示类型的 Utf8 实体。对于字段类型,这个值是类的全名,对于方法类型,这个值是每一个参数类型类的类全名的列表。 |
Fieldref, Methodref, InterfaceMethodref | 点号(.)分隔的一组值,每一个值都指向常量池中的其它的实体。第一个值(“.”号以前的)指向类实体,第二个值指向 NameAndType 实体。 |
异常表像这样存储每一个异常处理信息:
若是一个方法有定义 try-catch 或者 try-finally 异常处理器,那么就会建立一个异常表。它为每一个异常处理器和 finally 代码块存储必要的信息,包括处理器覆盖的代码块区域和处理异常的类型。
当方法抛出异常时,JVM 会寻找匹配的异常处理器。若是没有找到,那么方法会当即结束并弹出当前栈帧,这个异常会被从新抛到调用这个方法的方法中(在新的栈帧中)。若是全部的栈帧都被弹出尚未找到匹配的异常处理器,那么这个线程就会终止。若是这个异常在最后一个非守护进程抛出(好比这个线程是主线程),那么也有会致使 JVM 进程终止。
Finally 异常处理器匹配全部的异常类型,且无论什么异常抛出 finally 代码块都会执行。在这种状况下,当没有异常抛出时,finally 代码块仍是会在方法最后执行。这种靠在代码 return 以前跳转到 finally 代码块来实现。
除了按类型来分的运行时常量池,Hotspot JVM 在永久代还包含一个符号表。这个符号表是一个哈希表,保存了符号指针到符号的映射关系(也就是 Hashtable<Symbol*, Symbol>),它拥有指向全部符号(包括在每一个类运行时常量池中的符号)的指针。
引用计数被用来控制一个符号从符号表从移除的过程。好比当一个类被卸载时,它拥有的在常量池中全部符号的引用计数将减小。当符号表中的符号引用计数为 0 时,符号表会认为这个符号再也不被引用,将从符号表中卸载。符号表和后面介绍的字符串表都被保存在一个规范化的结构中,以便提升效率并保证每一个实例只出现一次。
Java 语言规范要求相同的(即包含相同序列的 Unicode 指针序列)字符串字面量必须指向相同的 String 实例。除此以外,在一个字符串实例上调用 String.intern() 方法的返回引用必须与字符串是字面量时的同样。所以,下面的代码返回 true:
1 ("j" + "v" + "m").intern() == "jvm"
Hotspot JVM 中 interned 字符串保存在字符串表中。字符串表是一个哈希表,保存着对象指针到符号的映射关系(也就是Hashtable<oop, Symbol>),它被保存到永久代中。符号表和字符串表的实体都以规范的格式保存,保证每一个实体都只出现一次。
当类加载时,字符串字面量被编译器自动 intern 并加入到符号表。除此以外,String 类的实例能够调用 String.intern() 显式地 intern。当调用 String.intern() 方法时,若是符号表已经包含了这个字符串,那么就会返回符号表里的这个引用,若是不是,那么这个字符串就被加入到字符串表中同时返回这个引用。
原文连接:jamesdbloom 翻译: ImportNew.com - 挖坑的张师傅
译文连接: http://www.importnew.com/17770.html
[ 转载请保留原文出处、译者和译文连接。]