潜水JVM「建议收藏」

潜水JVM

大家好,又见面了,我是全栈君。

原文地址:http://blog.jamesdbloom.com/JVMInternals.html(转载请注明出处和本文地址英文原文)

本文简要解析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。

栈的限制

一个栈能够是动态的或者是有合适大小的。假设一个线程要求更大的栈,那么将抛出StackOverflowError异常;假设一个线程要求新创建一个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 stack1:	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_SUPERConstant 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. 方法必须带有訪问控制keyword
  4. 方法有正确的參数值跟类型
  5. 字节码不会对栈进行不对得篡改
  6. 变量在被读取之前已经进行了初始化
  7. 变量具有正确的类型
在验证阶段运行这些检查意味着在运行时能够免去在链接阶段进行这些动作。虽然拖慢了类的载入速度,然而它避免了在运行字节码的时候运行这些检查。

准备: 包括了对静态存储的内存分配以及JVM所使用的不论什么数据结构(比方方法表)。静态字段都被创建以及实例化为它们的默认值。然而,没有不论什么实例化器或代码在这个阶段被运行,由于这些任务将会发生在实例化阶段。

解析: 是一个可选的阶段。

该阶段通过载入引用的类或接口来检查符号引用是否正确。假设在这个点这些检查没发生。那么对符号引用的解析会被推迟到直到它们被字节码指令使用之前。

实例化 类或接口。包括运行类或接口的实例化方法:<clinit>

潜水JVM「建议收藏」

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

Bootstrap类载入器:通常使用原生代码实现。由于它在JVM启动后非常快就会被初始化。

Bootstrap类载入器用于载入最基本的JavaAPI,比方rt.jar。它仅载入那些位于boot类路径中的信任级别非常高的类。因此,它也跳过了非常多验证过程。

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

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

用户定义的类载入器:能够额外得定义类载入器来载入应用程序类。用户定义的类载入器可用于一些特殊的场景。比方:在运行时又一次载入类或将一些特殊的类隔离为多个不同的分组(通常webserver中都会有这种需求,比方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类的例子还可以通过调用来获得String.intern()要明确地内部化。什么时候String.intern()被称为。假设符号表中已包含在字符串中,然后指着引用字符串将被退回。假设串中不包含的字符表,它添加到字符串表它将被返回到它的基准的同时。


版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/116660.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • django debug_vscode django

    django debug_vscode django介绍Django框架的调试工具栏使用django-debug-toolbar库,是一组可配置的面板,显示有关当前请求/响应的各种调试信息,点击时,显示有关面板内容的更多详细信息。应用1.安装

  • Python编写网络爬虫–牛刀小试

    Python编写网络爬虫–牛刀小试本文参考网上的资料,编写简单的Python编写网络爬虫,做了网页内容的抓取,分析出链接的url并抓取。

  • [Linux]F5负载均衡器「建议收藏」

    [Linux]F5负载均衡器「建议收藏」F5负载均衡器是硬件的负载均衡设备F5配置最简单负载均衡,需要配置的参数有Node(节点)、Pool(资源池)、和VirtualServer(虚拟服务器),它们的关系是,先配置Node,然后配置V

  • 多层感知机实现(单层感知器和多层感知器)

    前面利用了softmax来对图像进行分类,也可以使用多层感知机的方法对图像进行分类。多层感知机从零开始实现方法多层感知机(multilayerperceptron,MLP),在单层神经网络的基础上引入了一到多个隐藏层(hiddenlayer)。对于图中的感知机来说,它含有一个隐藏层,该层中有5个隐藏单元。输入和输出个数分别为4和3,中间的隐藏层中包含了5个隐藏单元。…

  • 执行Cmd命令[通俗易懂]

    执行Cmd命令[通俗易懂](1)直接在执行.cmd脚本:(2)直接仿制在CMD敲命令的方式:EventManager.WriteOutput("正在前端构建…");vardir=Path.C

  • ExtJS入门教程03,form中怎能没有validation[通俗易懂]

    ExtJS入门教程03,form中怎能没有validation[通俗易懂]接上篇内容,我们在学会extjsform的基本用法之后,今天我们来看看extjsform的validation功能。必填项,就是不能为空(allowBlank)效果:代码:{xtype:”textfield”,name:”UserName”,fieldLabel:”用户名”,allowBlank:false,…

    2022年10月31日

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号