JVM内部原理
原文链接 原文作者:James D Bloom 翻译:梅小西(904516706) 校对:吴京润
这篇文章详细描述了Java虚拟机的内在结构。下面这张图来自《The Java Virtual Machine Specification Java SE 7 Edition》,它展示了一个典型的JVM的主要的内部结构。
接下来的2个部分,将详细介绍这幅图中所有组成结构。 第一部分涵盖了每个线程都会生成的结构, 第二部分 涵盖了单独的每个线程生成的结构。
- 线程
- JVM 系统线程
- 每个线程
- 程序计数器 (PC)
- 栈
- 本地栈
- 栈的限制
- 栈帧
- 局部变量表
- 操作数栈
- 动态连接
- 线程间共享
- 堆
- 内存管理
- 堆外内存
- 即时(JIT)编译
- 方法区
- Class 文件结构
- 类加载器
- 快速类加载
- 方法区在哪里
- 类加载器的引用
- 运行时常量池
- 异常表
- 符号表
- 内部字符串 (String Table)
线程
线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。在一个Java线程准备好了所有的状态后,比如线程本地存储,缓存分配,同步的对象,栈以及程序计数器,这时一个操作系统中的本地线程也同时创建。当Java线程终止后,本地线程也会回收。操作系统因此负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法。当run()方法返回,发生了未捕获异常,Java线程终止,本地线程就会决定是否JVM也应该被终止(是否是最后一个非守护线程) 。当线程终止后,本地线程和Java线程持有的资源都会被释放。
JVM 系统线程
如果你使用jconsole或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main(String[])的main线程以及所有这个main线程自己创建的线程。这些主要的后台系统线程在Hotspot JVM里主要是以下几个:
虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括”stop-the-world”的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。
GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
编译线程:这种线程在运行时会将字节码编译成到本地代码。
信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。
每个线程
每个执行线程都包含以下的部分:
程序计数器(PC)
当前非native指令(或者字节码)的地址。如果当前方法是native的,那么这个程序计数器便是无用的。所有CPU都有程序计数器,通常来说,程序计数器在每次指令执行后自增,它会维护下一个将要执行的指令的地址。JVM通过程序计数器来追踪指令执行的位置,在方法区中,程序计数器实际上是指向了一个内存地址。
栈
每个线程都有自己的栈,它维护了在这个线程上正在执行的每个方法的栈帧。这个栈是一个后进先出的数据结构,所以当前正在执行的方法在栈的顶端,每当一个方法被调用时,一个新的栈帧就会被创建然后放在了栈的顶端。当方法正常返回或者发生了未捕获的异常,栈帧就会从栈里移除。栈是不能被直接操作的,尤其是栈帧对象的入栈和出栈,因此,栈帧对象有可能在堆里分配并且内存不需要连续。
本地栈
并不是所有的JVM都支持本地方法。不过那些支持的通常会创建出每个线程的本地方法栈。如果一个JVM已经实现了使用C-linkage 模型来支持Java本地调用,那么这个本地方法栈将会是一个C 栈。在这种情况下,参数的顺序以及返回值和传统的c程序在本地栈下几乎是一样的。一个native方法通常(取决于不同的JVM实现)会回调JVM,并且调用一个Java方法。这种native到Java的调用会发生在栈里(通常指Java栈)。这个线程会离开这个本地栈并且在栈上创建一个新的栈帧。
栈的限制
一个栈可以是动态的大小,也可以是指定的大小。如果一个线程需要一个大一点的栈,可能会导致StackOverflowError异常。如果一个线程需要一个新的栈帧而又没有足够的内存来分配,就会发生OutOfMemoryError异常。
栈帧
JVM为每个方法调用创建一个新的栈帧并推到每个方法调用的栈顶。当方法正常返回或者遇到了未捕获的异常,这个栈帧将被移除。想要了解更多的关于异常处理的可以看下面的“异常表”部分。
每个栈帧包含了:
- 局部变量表
- 返回值
- 操作数栈
- 当前方法所在的类的运行时常量池引用
局部变量表
局部变量表包含了这个方法执行期间所有用到的变量,包括this引用,所有方法参数以及其他的局部声明变量。对于类方法(比如静态方法)来说,所有方法参数下标都是从0开始,然而,对于实例方法来说这个0是留给this的。
一个局部变量可以是:
- boolean
- byte
- char
- long
- short
- int
- float
- double
- reference
- returnAddress
在局部变量表里,除了long和double,所有类型都是占了一个槽位,它们占了2个连续槽位,因为他们是64位宽度。
操作数栈
操作数栈用于字节码指令执行期间,就像通用寄存器在CPU里使用一样。大部分JVM的字节码各自操作出栈,入栈,复制,交换,或者执行操作,使其生产和消费各种数据。因此,在字节码里,指令把值在局部变量表和操作数栈之间频繁移动。比如,一个简单的变量初始化导致两个字节码在操作数栈里交互影响。
int i;
编译后得到下面字节码:
0: iconst_0 // 将 0 入栈到操作数栈的顶端。
1: istore_1 // 从操作数栈顶端弹出并保存到局部变量
想要了解更多关于局部变量表和操作数栈,运行时常量池之间的交互,请看下面的“class文件结构”。
动态链接
每个栈帧都包含了运行时常量池的引用。这个引用指向了这个栈帧正在执行的方法所在的类的常量池,它对动态链接提供了支持。
C/C++ 代码通常编译成一个对象文件,然后多个文件被链接起来生成一个可用的文件比如一个可执行文件或者动态链接库。在链接阶段,符号引用在每个对象文件里被替换成一个和最终执行相关的实际的内存地址。在Java里,这个链接过程在运行时是自动发生的。
当Java文件被编译时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。一个符号引用是一个逻辑引用并不是一个实际的指向一个物理内存地址的引用。不同的JVM实现能选择什么时候去解决符号引用,它通常发生在class文件加载后的验证,加载完成,立即调用或者静态解析等阶段,另外一种发生的时候是当符号引用第一次被使用,也叫做延迟或者延期解析。无论如何当每个引用第一次使用的时候,JVM必须保证解析发生,并抛出任何解析错误。绑定是一个字段,方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次,因为符号引用是完全替换的。如果符号引用关联到某个类,而这个类却还没准备好,就会引发类加载。每个直接引用被保存为偏移地址而不是和变量或者方法在运行时的位置相关的存储结构。
线程间共享
堆
堆用于在运行时分配类实例和数组。数组和对象可能永远不会存储在栈上,因为一个栈帧并不是设计为在创建后会随时改变大小。栈帧仅仅保存引用,这个引用指向对象或者数组在堆中的位置。与局部变量表(每个栈帧里)中的基本数据类型和引用不同,对象总是被存储在堆里,所以他们在方法结束后不会被移除,仅仅在垃圾收集的时候才会被移除。
为了支持垃圾收集,堆被分为三个部分:
- 年轻代
- 常常又被划分为Eden区和Survivor区
- 老年代(也被叫做年老代)
- 持久代
内存管理
对象和数组不会被明确的释放,只有垃圾收集器会自动释放他们。
通常他们的工作流程如下:
- 新对象和数组被分配在年轻代。
- 年轻代会发生Minor GC。 对象如果仍然存活,将会从eden区移到survivor区。
- Major GC 通常会导致应用线程暂停,它会在2个区中移动对象,如果对象依然存活,将会从年轻代移到老年代。
- 当每次老年代进行垃圾收集的时候,会触发持久代带也进行一次收集。同样,在发生full gc的时候他们2个也会被收集一次。
堆外内存
堆外内存的对象在逻辑上是JVM的一部分,但是它却不是在堆里创建的。
堆外内存包括:
- 持久代包含
- 方法区
- 内部字符串
- 代码缓存 用于编译和保存已经被JIT编译器编译成的本地代码的方法。
即时 (JIT)编译
在JVM里,Java字节码被解释运行,但是它没有直接运行本地代码快。为了提高性能,Oracle Hotspot VM会寻找字节码的”热点”区域,它指频繁被执行的代码,然后编译成本地代码。这些本地代码会被保存在堆外内存的代码缓存区。Hotspot用这种方式,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。
方法区
方法区存储的是每个class的信息,例如:
- 类加载器引用
- 运行时常量池
- 所有常量
- 字段引用
- 方法引用
- 属性
- 字段数据
- 每个方法
- 名字
- 类型
- 修饰符
- 属性
- 方法数据
- 每个方法
- 名字
- 返回类型
- 参数类型(按顺序)
- 修饰符
- 属性
- 方法代码
- 每个方法
- 字节码
- 操作数栈大小
- 局部变量大小
- 局部变量表
- 异常表
- 每个异常处理
- 开始位置
- 结束位置
- 代码处理在程序计数器中的偏移地址
- 被捕获的异常类的常量池索引
- 每个异常处理
- 每个方法
- 每个方法
- 每个方法
所有线程都共享同样的方法区,所以访问方法区的数据和动态链接的过程都是线程安全的。如果两个线程尝试访问一个类的字段或者方法而这个类还没有加载,这个类就一定会首先被加载而且仅仅加载一次,这2个线程也一定要等到加载完后才会继续执行。
类文件结构
一个编译好的类文件包含如下的结构:
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_flags:
提供了类的修饰符清单
this_class:
指向常量池的索引,它提供了类的全限定名,如org/jamesdbloom/foo/Bar
super_class:
指向常量池的索引,它提供了一个到父类符号引用,如java/lang/Object
interfaces:
指向常量池索引集合,它提供了一个符号引用到所有已实现的接口
fields:
指向常量池索引集合,它完整描述了每个字段
methods:
指向常量池索引集合,它完整描述了每个方法的签名,如果这个方法不是抽象的或者不是native的,那么字节码中会体现出来
attributes:
不同值的集合,它提供了额外的关于这个类的信息,包括任何带有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIME的注解
通过使用javap这个命令,我们可以在已编译的class文件中看到字节码信息。
如果你编译下面这段代码
package org.jvminternals;
public class SimpleClass {
public void sayHello() {
System.out.println("Hello");
}
}
这时运行如下命令便可以看到接下来的输出
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;
}
这个类文件说明在常量池有3个主要的部分,构造函数和sayHello方法。
- 常量池 – 它提供了和符号表一样的信息,详细描述可以看后面的章节。
- 方法– 每个方法包含4个区域:
- 签名和访问标识
- 字节码
- 行号表 – 它为调试器提供了指向字节码关联的代码行信息,例如,sayHello方法中,字节码0代表的是第6行Java代码,字节码8代表的是第7行Java代码。
- 局部变量表 – 栈帧里所有局部变量的集合,在所有的例子里局部变量都是指这个。
接下来介绍这个类文件中用到的字节码操作符。
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 和aload加载。这些操作符都只加载单一的并且是明确的局部变量索引的操作数。
ldc
这种操作符用于将一个常量从运行时常量池推入到操作数栈。
getstatic
这种操作符用于将一个在运行时常量池里的静态值从静态字段列表推入到操作数栈。
invokespecial, invokevirtual
这种操作符是一系列方法调用操作符中的一种,比如 invokedynamic, invokeinterface,invokespecial, invokestatic, invokevirtual。在这个类文件里invokespecial 和 invokevirutal用于不同用途,invokevirutal用于调用一个基于对象的类方法,而invokespecial指令用于调用实例初始化方法,以及private方法,父类的方法。
return
这种操作符是一组操作符中的一种,比如ireturn, lreturn,freturn, dreturn, areturn 和return。每个操作符被指定了返回声明,他们返回不同的值,i用于int,l用于long,f用于float,d用于double,而a是对象的引用。没有return符号的将只返回void。
与局部变量,操作数栈以及运行时常量池交互的大部分操作数中,任何一个典型的字节码都如下所示。
.
构造函数有2个指令,第一个this被推入到操作数栈,接下来父类的构造函数被调用,它使用了this,并从操作数栈里弹出。
这个sayHello() 方法会更复杂,它必须通过运行时常量池将符号引用转成实际的引用,就像之前介绍的那样。第一个操作符getstatic将一个引用从System类里移出并推入到操作数栈的静态字段。接下来的操作符ldc将字符串”Hello”推入操作数栈。最后一个操作符invokevirtual调用System.out的println方法,把从操作数栈弹出字符串”Hello”作为参数并且创建一个当前线程的新的栈帧。
类加载器
JVM启动的时候通过引导类加载器加载一个初始类。这个类在调用public static void main(String[])方法之前被链接和初始化。这个方法的执行将依次导致所需的类的加载,链接和初始化。
加载 是一个通过指定的名字来查找当前类和接口并把它读取到一个字节数组的过程。下一步这个字节数组将被解析成一个确定的并带有major version和minor version的类对象。任何被直接父类指定了名字的类或者接口都会被加载。一旦这个过程完成,一个类或者接口对象便通过一个二进制表示的数据来创建完成。
链接 是一个类或接口验证以及类型、直接父类和父接口准备的过程。链接包含了3步,验证,准备以及部分解析。
验证 是一个确定类或者接口是否是正确结构以及是否遵从Java语言和JVM规定的语法的过程。比如下面:
- 符号表中一致的,正确的格式
- final 方法 / 类没有被重写
- 方法遵从访问控制关键字
- 方法有正确的参数个数和类型
- 字节码没有不正确的操作栈结构
- 变量在使用前已经初始化
- 变量有正确的类型值
验证阶段的这些检查意味着它们不需要在运行的时候才进行。链接阶段的验证虽然拖慢了加载速度,但是它避免了在字节码运行时还需要进行各种检查。
准备 涉及到静态存储的内存分配以及JVM会用到的任何的数据结构比如方法表。静态字段被创建和初始化为默认值,然而,在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。
解析 是一个可选阶段,它通过加载引用的类或者接口来检查符号引用,以及检查引用的正确性。如果这时没有发生符号引用的解析,它会被延期到字节码指令使用之前进行。
初始化 一个类或者接口的初始化包含了执行类或者接口的初始化方法<clinit>
在JVM里有许多不同角色的类加载器。每个类加载器委托给父类加载器去加载,除了最顶层的引导类加载器(bootstrap classloader)。
引导类加载器(Bootstrap Classloader )通常被本地代码实现,因为它在JVM里是最早被实例化的。这个引导类加载器(bootstrap classloader)负责加载最基本的Java APIs,包括rt.jar。它仅仅加载启动的classpath里找到的拥有最高信任的类,这也就导致它会跳过很多给普通类进行的验证工作。
扩展类加载器(Extension Classloader )它加载Java标准扩展APIs类,比如security扩展类。
系统类加载器(System Classloader)系统类加载器是默认的应用加载器,它加载classpath下的应用类。
用户定义类加载器(User Defined Classloaders) 是一个可替换的用于加载应用类的类加载器。一个用户自定义的类加载器一般用于多种特殊原因包括运行时重加载或者通常在web 服务器中将所需要的已加载的类分成不同组,比如Tomcat。
快速类加载
在HotSpot JVM 5.0版本中介绍了一种叫做类数据共享(CDS)的特性。在JVM安装过程中,它会加载一些关键的JVM类到内存映射共享存档里,比如rt.jar。CDS减少了加载这些类的时间,提高了JVM启动的速度,并且允许这些类在不同的JVM实例之间共享,降低了内存占用。
方法区在哪里
The Java Virtual Machine Specification Java SE 7 Edition 明确说明: “尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。” 相反,在Oracle JVM 的jconsole里会发现这个方法区(以及代码缓存)并不是堆的一部分。在OpenJDK代码里可以看到这个CodeCache在虚拟机里和ObjectHeap是不同的字段。
类加载器的引用
所有被加载的类都包含了一个指向加载他们自己的类加载器的引用。同样这个类加载器也包含了他自己加载的所有类的引用。
运行时常量池
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 操作符。这个操作符是一个指向常量池的索引,因此它指向的是常量池的第2个入口。第2个入口是一个类引用,这个入口接下来引用的是另一个常量池入口,它包含类的名字,是一个UTF8常量字符串,内容为// Class java/lang/Object ,这个符号连接可以用于查找java.lang.Object这个类。new操作符创建了一个类实例并且实例化了它的值。一个指向新的类实例的引用会被加入到操作数栈。dup操作符这时会创建一个操作数栈最顶层元素的额外的拷贝,并且把它再次加入到操作数栈的顶部。最后在第2行通过invokespecial调用一个实例初始化方法。这个操作数同样包含一个指向常量池的引用。这个初始化方法从操作数池的顶端弹出一个元素并把它作为参数传给方法。最后便生成了一个指向这个新创建并被初始化的对象的引用。
如果你编译下面这个简单类:
package org.jvminternals;
public class SimpleClass {
public void sayHello() {
System.out.println(&quot;Hello&quot;);
}
}
这个已生成的类文件中的常量池像如下这样:
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
这个常量池包含如下类型:
Integer
一个4字节的int类型常量
Long
一个8字节的long类型常量
Float
一个4字节的float类型常量
Double
一个8字节的double类型常量
String
一个字符串常量,指向另一个UTF8入口,在这个常量池里包含了实际的字节数据。
UTF8
一个代表UTF8编码字符序列的字节流。
Class
一个指向另一个UTF8入口的类常量 , 在这个常量池内部包含了JVM内部格式化的类名字的完全限定符(在动态链接过程里用到)。
NameAndType
冒号分隔的一对值,每个值指向另一个常量池的入口。第1个值(冒号前面)指向一个UTF8字符串入口,这个字符串是一个方法名或者字段名。第2个值指向一个UTF8入口,它代表一种类型,当前面是字段的时候,它就是字段类的全限定名,如果是方法,它就是每个参数类型的全限定名集合。
Fieldref, Methodref, InterfaceMethodref
逗号分隔的一对值,每个值指向另一个常量池的入口。第1个值(逗号前面)指向一个类入口。第2个值指向一个NameAndType入口。
异常表
异常表保存了每个异常处理信息比如:
- 起始位置
- 结束位置
- 程序计数器记录的代码处理的偏移地址
- 被捕获的异常类在常量池中的索引
如果一个方法定义了一个try-catch 或者try-finally的异常处理,就会创建一个异常表。它包含了每个异常处理或者finally块的信息,这些信息包括异常的处理范围,被处理的异常类型以及处理代码的位置。当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。
.
不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标(校对者注:此处原文似乎不正确,至少从JDK6开始,finally块会在每个方法返回前的分支复制一份,而不会发生跳转)。
符号表
在持久代里,除了有各种类型的运行时常量池外,JVM还维护了一个符号表。这个符号表是一个哈希表,它从符号指针映射到符号(比如Hashtable<Symbol*, Symbol>),并且还包含了一个指向所有符号的指针,包括每个类的运行时常量池中维护的符号。
引用计数器用于控制当一个符号从符号表移除的时候。比如当一个类被卸载时,所有在运行时常量池中维护的符号的引用计数将减少。当符号表里的一个符号引用计数器变成0,这个符号表就知道这个符号将不再被引用,并且这个符号会从符号表里卸载。对符号表和字符串表来说,为了提高效率和保证每个入口只出现一次,所有的入口被维护在一个标准化的格式里。
内部字符串 (String Table)
Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列,并且必须是同样的字符串实例。另外,如果String.intern() 被一个String 实例调用,它应该返回一个相同的引用。如果一个字符串是一个固定的字面量,那么下面会是返回true。
(“j” + “v” + “m”).intern() == “jvm”
在Hotspot JVM里,字符串表维护了内部的字符串,它是一个哈希表结构,从对象指针映射到符号(例如Hashtable<oop, Symbol>),并且是维护在持久代里。对符号表和字符串表来说,为了提高效率和保证每个入口只出现一次,所有的入口被维护在一个标准化的格式里。
类被加载的时候,字符串的字面量由编译器自动的内部化,并且加入到符号表里。此外,通过调用String.intern()方法,String类的实例能够明确的被内部化。调用String.intern()方法时,如果这个符号表里已经包含了这个字符串,那么将返回指向它的引用,如果不包含,那这个字符串就会被加入到字符串表并且返回它的引用。