深入浅出JVM

时间:2024-04-21 15:33:22

这篇文章简要解析了JVM的内部结构。下面这幅图展示了一个典型的JVM(符合JVM Specification Java SE 7 Edition)所具备的关键内部组件。

深入浅出JVM

上图展示的所有这些组件都将在下面两个章节中被解析。第一章包含将会在每个线程上创建的组件;第二章包含那些不依赖于线程即可创建的组件(线程间可共享的组件)。

  • 线程内创建
    • JVM系统线程
    • 单个线程
    • 程序计数器(PC)
    • 原生栈
    • 栈的限制
    • Frame
    • 局部变量数组
    • 操作数栈
    • 动态链接
  • 线程之间共享
    • 内存管理
    • 非堆内存区
    • JIT编译
    • 方法区
    • 类文件结构
    • 类加载器
    • 更快的类加载
    • 方法区位置
    • 类加载器引用
    • 运行时常量池
    • 异常表
    • 符号表
    • (内部字符串)字符串表

线程内组件

一个线程是对程序的一次执行。JVM允许一个应用创建多个线程并行地执行。在HotspotJVM中,对Java线程和原生操作系统线程之间有一个直接的映射。在所有用于创建Java线程的状态(诸如:thread-local存储、分配缓冲区、同步对象、栈以及程序计数器)准备好之后,原生线程才会被创建。而一旦Java线程终止,原生线程将会被回收。因此,操作系统将会调度所有线程并分配给它们任何可用的CPU时间片。一旦原生线程被实例化完成,它将调用Java线程上的run()方法。当run()方法返回,未捕获的异常被处理,原生线程会确认JVM是否需要随着线程的终止而终止(比如它是最后一个非deamon线程)。一旦线程被终止,无论是原生线程还是Java线程,它们所占用的资源都会被释放。

JVM系统线程

如果你用jconsole或者任何其他的debug工具查看,可能会看到有许多线程在后台运行。这些运行着的后台线程不包含主线程,主线程是基于执行publicstatic void main(String[]) 的需要而被创建的。而这些后台线程都是被主线程所创建。在HotspotJVM中主要的后台系统线程,见下表:

VM 线程 该线程用于等待执行一系列能够使得JVM到达一个“safe-point”的操作。
而这些操作不得不发生在一个独立的线程上的原因是:它们都要求JVM处于一个——无法修改堆的safepoint。
被这个线程执行的该类操作都是“stop-the-world”型的垃圾回收、线程栈回收、线程搁置以及有偏差的锁定撤销。
周期性的任务线程 该线程用于响应timer事件(例如,中断),这些事件用于调度执行周期性的操作
GC 线程 这些线程支持在JVM中不同类型的垃圾回收
编译器线程 它们用于在运行时将字节码编译为本地机器码
信号分发线程 该线程接收发送给JVM的信号,并通过调用JVM合适的方法进行处理

单个线程

每个线程的一次执行都包含如下的组件

程序计数器(PC)

除非当前指令或者操作码是原生的,否则当前指令或操作码的地址都需要依赖于PC来寻址。如果当前方法是原生的,那么该PC即为undefined。所有的CPU都有一个PC,通常PC在每个指令执行后被增加以指向即将执行的下一条指令的地址。JVM使用PC来跟踪正在执行的指令的位置。事实上,PC被用来指向methodarea的一个内存地址。

每个线程都有属于它自己的栈,用于存储在线程上执行的每个方法的frame。栈是一个后进先出的数据结构,这可以使得当前正在执行的方法位于栈的顶部。对于每个方法的执行,都会有一个新的frame被创建并被入栈到栈的顶部。当方法正常的返回或在方法执行的过程中遇到未捕获的异常时frame会被出栈。栈不会被直接进行操作,除了push/ pop frame 对象。因此可以看出,frame对象可能会被分配在堆上,并且内存也没必要是连续的地址空间(请注意区分frame的指针跟frame对象)。

原生栈

不是所有的JVM都支持原生方法,但那些支持该特性的JVM通常会对每个线程创建一个原生方法栈。如果对JVM的JNI(JavaNative Invocation)采用c链接模型的实现,那么原生栈也将是一个C实现的栈。在这个例子中,原生栈中参数的顺序 、返回值都将跟通常的C程序相同。一个原生方法通常会对JVM产生一个回调(这依赖于JVM的实现)并执行一个Java方法。这样一个原生到Java的调用发生在栈上(通常在Java栈),与此同时线程也将离开原生栈,通常在Java栈上创建一个新的frame。

栈的限制

一个栈可以是动态的或者是有合适大小的。如果一个线程要求更大的栈,那么将抛出*Error异常;如果一个线程要求新创建一个frame,又没有足够的内存空间来分配,将会抛出OutOfMemoryError异常。

Frame

对于每一个方法的执行,一个新frame会被创建并被入栈到栈顶。当方法正常返回或在方法执行的过程中遇到未捕获的异常,frame会被出栈。

每个frame都包含如下部分:

  • 局部变量数组
  • 返回值
  • 操作数栈
  • 对当前方法所属的类的常量池的引用

局部变量数组

局部变量数组包含了在方法执行期间所用到的所有的变量。包含一个对this的引用,所有的方法参数,以及其他局部定义的变量。对于类方法(比如静态方法),方法参数的存储索引从0开始;而对于实例方法,索引为0的槽都为存储this指针而保留。

一个局部变量,可以是如下类型:

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress
除了long以及double(它们都占据两个连续的槽,因为它们有双倍的宽度为64位,而不是32位)其他所有的类型都在局部变量数组中占据一个独立的槽位。

操作数栈

操作数栈在字节码指令被执行的过程中使用。它跟原生CPU使用的通用目的的寄存器类似。大部分的字节码都把时间花费在跟操作数栈打交道上,通过入栈、出栈、复制、交换或者执行那些生产/消费值的操作。对字节码而言,那些在局部变量数组和操作数栈之间移动值的指令是非常频繁的。例如,一个简单的变量初始化,就导致产生两个与操作数栈交互的字节码。
int i;

编译后获得如下的字节码:

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

对更多细节,请阅读后续内容。

动态链接

每个frame都包含一个对运行时常量池的引用。该引用指向将要被执行的方法所属的类的常量池。该引用也用于辅助动态链接。
C/C++ 代码通常被编译为一个对象文件,然后多个对象文件被链接到一起从而产生一个可用的artifact,例如一个可执行文件或dll。在链接阶段,每个对象文件内的符号引用被替代为一个跟最终可执行文件相关的实际内存地址。而对于Java而言,这个链接阶段将会在运行时被动态的完成。
当一个Java类被编译时,所有对存储在类的常量池中的变量以及方法的引用都被当做符号引用。一个符号引用仅仅只是一个逻辑引用而不是最终指向物理内存地址的引用。JVM的实现可以选择解析符号引用的时机,该时机可以发生在当类文件被验证后、被加载后,这称之eager或静态分析;不同的是它也可以发生在当符号引用被首次使用的时候,称之为lazy或延迟分析。但JVM必须保证:解析发生在每个引用被首次使用前,同时在该时间点,如果遇到分析错误能够抛出异常。绑定是一个处理过程,它将被符号引用标识的字段、方法或类替换为一个直接引用。这个处理过程只发生一次,因为符号引用需要被完全替换。如果一个符号引用关联着一个类,而该类还没有被解析,那么该类也会被立即加载。每个直接引用都被以偏移的方式存储,该存储结构关联着变量或方法的运行时位置。

线程之间共享

堆用来在运行时存储类的实例和数组。数组和对象永远不能被分配到栈上,因为frame被设计为在其创建后不可更改大小。Frame只存储用于指向堆中的数组和对象的指针。不像原始类型以及存储在局部变量数组中的引用(以上这些都指存储在每个frame里的),对象总是存储在堆上,所以当一个方法执行结束,它们不会被立即移除(对象只能被垃圾回收器回收)。
为了支持垃圾回收,堆被分隔为三个部分:
  • 青年代
    • 通常又被分割为Eden跟Survivor两个部分
  • 老年代(也称之为终身代)
  • 永久代

内存管理

对象和数组永远都不会被显式释放,因此只能依靠垃圾回收器来自动地回收它们。

通常,以如下的步骤进行:

  1. 新对象和数组被创建在年轻代
  2. 次垃圾回收器将在年轻代上执行。那些仍然存活着的对象,将被从eden区移动到survivor区
  3. 主垃圾回收器将会把对象在代与代之间进行移动,主垃圾回收器通常会导致应用程序的线程暂停。那些仍然存活着的对象将被从年轻代移动到老年代
  4. 永久代会在每次老年代被回收的时候同时进行,它们在两者中其一满了之后都会被回收

非堆式内存

有些对象并不会创建在堆中,这些对象在逻辑上被认为是JVM机制的一部分。
非堆式的内存包括:
  • 永久代中包含:
    • 方法区
    • 内部字符串
  • 代码缓存:用于编译以及存储方法,这些方法已经被JIT编译成本地代码

JIT编译

Java 字节码是被解释过的,但它还是没有在JVM所宿主的CPU上执行原生代码快。为了提高性能,OracleHotspot VM会寻找那些有规律地执行的字节码,并把他们编译为本地原生代码。而原生代码将会被存储在代码缓存的非“堆”内存区。这样,HotspotVM会尝试去选择最合适的方式在它编译代码以及它执行被解释过代码的额外时间之间作出权衡。

方法区

方法区存储了每个类的信息,例如:
  • 类加载器的引用
  • 运行时常量池
    • 数值常量
    • 字段引用
    • 方法引用
    • 属性
  • 字段数据
    • 对每个字段
      • 名称
      • 类型
      • 修改器
      • 属性
  • 方法数据
    • 对每个方法
      • 名称
      • 返回值
      • 参数类型(按顺序)
      • 修改器
      • 属性
  • 方法体
    • 对每个方法
      • 字节码
      • 操作数栈大小
      • 局部变量大小
      • 局部变量表
      • 异常表
        • 对每个异常处理器
          • 起始点
          • 终止点
          • 对处理器代码的PC偏移量
          • 被捕获的异常类在常量池中的索引

所有的线程共享相同的方法区。所以,对于方法区数据的访问以及对动态链接的处理必须是线程安全的。如果两个线程企图访问一个还没有被载入的类(该类必须只能被加载一次)的字段或者方法,直到该类被加载完成,这两个线程才能继续执行。

类的文件结构

一个被编译过的类文件包含如下的结构:
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
interface 常量池中的数组索引,该数组提供对所有被实现的接口的符号引用
fields 常量池中的数组索引,该数组提供对每个字段的完整描述
methods 常量池中的数组索引,该数组提供对每个方法签名的完整描述,如果该方法不是抽象的或者native的,
那么也会包含字节码
attributes 不同值的数组,提供关于类的额外信息,包括注解:RetentionPolicy.CLASS以及RetentionPolicy.RUNTIME
可以使用javap命令查看被编译后的java类的字节码。
如果你编译下面的简单类:
package org.jvminternals;

public class SimpleClass {

    public void sayHello() {
System.out.println("Hello");
} }

如果你运行下面的命令,那么你将看到下面的输出:

javap -v -p -s -sysinfo -constantsclasses/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方法

  • 常量池:它提供了跟通用符号表提供的相同的信息
  • 方法:(每个都包含四个区域)
    • 签名和访问标记
    • 字节码
    • line number表 – 它给调试器提供相关信息,哪一行关联到哪条字节码指令。例如,在sayHello方法中,Java代码的第6行关联着字节码指令行数为0,第7行Java代码关联着字节码指令行数为8
    • 局部变量表 – 列出了在frame中提供的所有局部变量,在这个例子中,唯一的局部变量是:this
下面列出了在该类文件中,使用到的操作码:
aload_0 该操作码是形如aload_<n>格式的一组操作码中其中的一个。
它们都是用来加载一个对象引用到操作数栈。
而“<n>”用于指示要被访问的对象引用在局部变量数组中的位置,但n的值只能是0,1,2或3。
也有其他相似的操作码用来加载非对象引用,如:iload_<n>,lload_<n>,fload_<n>以及dload_<n>
(其中,i表示int,l表示long,f表示float,而d表示double,上面n的取值范围对这些*load_<n>同样适用)。
局部变量的索引如果大于3,可以使用iload,lload,float,dload和aload加载。
这些操作码都携带要被加载的局部变量在数组中的索引。
ldc 该操作码用来从运行时常量池取出一个常量压入操作数栈
getstatic 该操作码用来从运行时常量池的静态字段列表入栈一个静态值到操作数栈
invokespecial
invokevirtual
这些操作码是一组用来执行方法的操作码
(总共有:invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual这几种)。
其中,本例中出现的invokevirtual用来执行类的实例方法;
而invokespecial用于执行实例的初始化方法,同时也用于执行私有方法以及属于超类但被当前类继承的方法
(超类方法动态绑定到子类)。
return 该操作码是一组操作码(ireturn,lreturn,freturn,dreturn,areturn以及return)中的其中一个。
每个操作码,都是类型相关的返回语句。
其中i代表int,l表示long,f表示float,d表示double而a表示一个对象的引用。
没有标识符作为首字母的return语句,仅会返回void
就像在其他通用的字节码中那样,以上这些操作码主要用于跟本地变量、操作数栈以及运行时常量池打交道。
构造器有两个指令,第一个将“this”压入到操作数栈,接下来该构造器的父构造器被执行,这一操作将导致this被“消费”,因此this将从操作数栈出栈。
深入浅出JVM
而对于sayHello()方法,它的执行将更为复杂。因为它不得不通过运行时常量池,解析符号引用到真实的引用。第一个操作数getstatic,用来入栈一个指向System类的静态字段out的引用到操作数栈。接下来的操作数ldc,入栈一个字符串字面量“Hello”到操作数栈。最后,invokevirtual操作数,执行System.out的println方法,这将使得“Hello”作为一个参数从操作数栈出栈,并为当前线程创建一个新的frame。
深入浅出JVM

类加载器

JVM的启动是通过bootstrap类加载器来加载一个用于初始化的类。在publicstatic void main(String[])被执行前,该类会被链接以及实例化。main方法的执行,将顺序经历加载,链接,以及对额外必要的类跟接口的初始化。

加载: 加载是这样一个过程:查找表示该类或接口类型的类文件,并把它读到一个字节数组中。接着,这些字节会被解析以确认它们是否表示一个Class对象以及是否有正确的主、次版本号。任何被当做直接superclass的类或接口也一同被加载。一旦这些工作完成,一个类或接口对象将会从二进制表示中创建。

链接: 链接包含了对该类或接口的验证,准备类型以及该类的直接父类跟父接口。简而言之,链接包含三个步骤:验证、准备以及解析(optional)

验证: 该阶段会确认类以及接口的表示形式在结构上的正确性,同时满足Java编程语言以及JVM语义上的要求。比如,接下来的检查动作将会被执行:

  1. 一致以及正确被格式化的符号表
  2. final方法或类没有被override
  3. 方法必须带有访问控制关键字
  4. 方法有正确的参数值跟类型
  5. 字节码不会对栈进行不正确得篡改
  6. 变量在被读取之前已经进行了初始化
  7. 变量具有正确的类型
在验证阶段执行这些检查意味着在运行时可以免去在链接阶段进行这些动作,虽然拖慢了类的加载速度,然而它避免了在执行字节码的时候执行这些检查。
准备: 包含了对静态存储的内存分配以及JVM所使用的任何数据结构(比如方法表)。静态字段都被创建以及实例化为它们的默认值。然而,没有任何实例化器或代码在这个阶段被执行,因为这些任务将会发生在实例化阶段。
解析: 是一个可选的阶段。该阶段通过加载引用的类或接口来检查符号引用是否正确。如果在这个点这些检查没发生,那么对符号引用的解析会被推迟到直到它们被字节码指令使用之前。

实例化 类或接口,包含执行类或接口的实例化方法:<clinit>

深入浅出JVM

在JVM中存在多个不同职责的类加载器。每一个类加载器都代理其已被加载的父加载器(除了bootstrap类加载器,因为它是根加载器)。

Bootstrap类加载器:通常使用原生代码实现,因为它在JVM启动后很快就会被初始化。Bootstrap类加载器用于加载最基本的JavaAPI,比如rt.jar。它仅加载那些位于boot类路径中的信任级别很高的类。因此,它也跳过了很多验证过程。

Extension 类加载器:从标准的Java扩展API中加载类。例如,安全的扩展功能集。

System 类加载器:这是应用程序默认的类加载器。它从classpath中加载应用程序类。

用户定义的类加载器:可以额外得定义类加载器来加载应用程序类。用户定义的类加载器可用于一些特殊的场景,比如:在运行时重新加载类或将一些特殊的类隔离为多个不同的分组(通常web服务器中都会有这样的需求,比如Tomcat)。

更快的类加载

一个称之为类数据共享(CDS)的特性自HotspotJVM 5.0开始被引进。在安装JVM期间,安装器加载一系列的Java核心类(如rt.jar)到一个经过映射过的内存区进行共享存档。CDS减少了加载这些类的时间从而提升了JVM的启动速度,同时允许这些类在不同的JVM实例之间共享。这大大减少了内存碎片。

方法区的位置

JVM Specification Java SE 7 Edition清楚地声明:尽管方法区是堆的一个逻辑组成部分,但最简单的实现可能是既不对它进行垃圾回收也不压缩它。然而矛盾的是利用jconsole查看Oracle的JVM的方法区(以及CodeCache)是非堆形式的。OpenJDK代码显示CodeCache相对ObjectHeap而言是VM中一个独立的域。

类加载器引用

所有的类都包含一个指向加载它们的类加载器的引用。反过来类加载器也包含它加载的所有类的引用。

运行时常量池

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操作数。该操作数是一个指向常量池的索引,因此它引用常量池中得第二条记录。而第二条记录是一个类的引用,该记录反过来引用另一个位于常量池里的记录(它是一个用UTF8编码的类名://Class java/lang/Object)。该符号链接稍后会用于查找java.lang.Object类。new操作码创建类的一个实例同时实例化它的变量。这个指向类的新实例的引用会被加入到操作数栈。dup操作码接着创建一个额外的对操作数栈的栈顶引用的拷贝。同时将this引用加入栈顶。最终,一个实例的初始化方法会被调用(上图第二行通过调用invokespecial)。this操作数同样也包含一个对常量池的引用。实例化方法消费栈顶引用(把其视为传递给该方法的一个参数)。最终,将会产生一个对新对象的引用(这个引用是既被创建完成也被初始化完成的)。

如果你编译下面的这个简单的类:
package org.jvminternals;

public class SimpleClass {

    public void sayHello() {
System.out.println("Hello");
} }

生成的类文件的常量池,看起来会像下图所示:

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 一个String字面值常量指向常量池中另一个包含最终字节的UTF8记录
Utf8 一个字节流表示一个Utf8编码的字串序列
Class 一个Class字面值常量指向常量池中的另一个Utf8记录,它包含JVM内部格式的完全限定名
(它用于动态链接)
NameAndType 用一个冒号区分一对值,每个值都指向常量池中得其他记录。
冒号前的第一个值指向一个utf8字符串字面量表示方法名或者字段名。
第二个值指向一个utf8字符串字面量表示类型。
举一个字段的例子是完全限定的类名;
举一个方法的例子是: 它是一个列表,该列表中每个参数都是完全限定的类名
Fieldref,
Methodref,
InterfaceMethodref
用点来分隔的一对值,每个值指向常量池中的另一个记录。
点前的第一个值指向一个Class记录。第二个值指向一个NameAndType记录

异常表

异常表存储了每个异常处理器的信息:
  • 起始点
  • 终止点
  • 处理代码的PC偏移量
  • 被捕获的异常类的常量池索引
如果一个方法定义了try-catch或try-finally异常处理器,那么一个异常表将会被创建。它包含了每个异常处理器的信息或者finally块以及正在被处理的异常类型跟处理器代码的位置。
当一个异常被抛出,JVM会为当前方法寻找一个匹配的处理器。如果没有找到,那么该方法最终会唐突地出栈当前stackframe而异常会被重新抛出到调用链(新的frame)。如果在所有的frame都出栈之前还是没有找到异常处理器,那么当前线程将会被终止。当然这也可能会导致JVM被终止,如果异常被抛出到最后一个非后台线程的话,比如该线程就是主线程。
最终异常处理器会匹配所有的异常类型并且无论什么时候该类型的异常被抛出总是会得到执行。在没有异常抛出的例子中,finally块仍然会在方法的最后被执行。一旦return语句被执行就会立即跳转到finally代码块继续执行。

符号表

除了每个类型的运行时常量池,HotspotJVM对每个类型都有一个符号表存储在“永久代”中。符号表是一个hashtable将符号指针映射到符号(比如:Hashtable<Symbol*,Symbol>),另外符号表还包含一个指针指向所有符号(这囊括了每个类的运行时常量池)。
“引用计数”被用来作为控制某个符号要从符号表里删除的机制。例如,当某个类被卸载后,所有它的运行时常量池中的符号的引用计数都会被减一。当符号表中的一个符号的引用计数到达0时,符号表就认为该符号将不会再被引用,而随后也会被从符号表中卸载。无论是符号表还是字符串表(见下面),所有的记录都存储在一个标准化的表单中以此来提升性能同时可以确认每条记录仅仅出现一次。

内部字符串(字符串表)

Java语言规范要求相同的字符串字面量,包含相同的unicode字符序列的字符串字面量必须关联到相同的String实例。另外,如果String.intern()在一个字符串实例上被调用,那么必须返回一个引用,该引用指代的实例必须跟该字符串的字面量相同。下面的代码将返回true。
"j" + "v" + "m").intern() == "jvm"

在JVM中,内部字符串被存储在字符串表中。字符串表是一个hashtable映射对象指针到符号(比如:Hashtable<oop,Symbol>),它被存储在永久代里。

当类被加载时,字符串字面量会被编译器自动“内部化”并且被加入到字符表。另外字符串类的实例可以通过调用String.intern()来明确地内部化。当String.intern()被调用,如果符号表里已经包含该字符串,那么指向该字符串的引用将被返回。如果该字符串没有包含在字符表,则会被加入到字符串表同时返回其引用。