JVM内幕:Java虚拟机详解



摘自百度百科   JVM  转载地址:https://baike.baidu.com/item/JVM/2902369?fr=aladdin

JVM是Java Virtual Machine(Java 虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的 目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言 编译程序只需生成在Java虚拟机上运行的目标代码( 字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的 机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。




支持类型

编辑

基本数据类型

byte://1 字节有符号整数的补码
JVM
short://2字节有符号整数的补码
int://4字节有符号整数的补码
long://8字节有符号整数的补码
float://4字节IEEE754 单精度浮点数
double://8字节IEEE754 双精度浮点数
char://2 字节无符号Unicode 字符
几乎所有的Java类型检查都是在编译时完成的。上面列出的原始数据类型的数据在Java执行时不需要用硬件标记。操作这些原始数据类型数据的 字节码(指令)本身就已经指出了 操作数的数据类型,例如iadd、ladd、fadd和dadd指令都是把两个数相加,其操作数类型别是int、long、float和double。 虚拟机没有给boolean(布尔)类型设置单独的指令。boolean型的数据是由integer指令,包括integer返回来处理的。boolean型的 数组则是用byte数组来处理的。虚拟机使用IEEE754格式的浮点数。不支持IEEE格式的较旧的计算机,在运行Java数值计算程序时,可能会非常慢。

其它数据类型

object//对一个Javaobject(对象)的4字节引用
returnAddress//4字节,用于jsr/ret/jsr-w/ret-w指令
注:Java 数组被当做object处理。
JVM
虚拟机的规范对于object内部的结构没有任何特殊的要求。在Sun公司的实现中,对object的引用是一个句柄,其中包含一对 指针:一个指针指向该object的方法表,另一个指向该object的数据。用Java虚拟机的 字节码表示的程序应该遵守类型规定。Java虚拟机的实现应拒绝执行违反了类型规定的字节码程序。Java虚拟机由于字节码定义的限制似乎只能运行于32位 地址空间的机器上。但是可以创建一个Java 虚拟机,它自动地把字节码转换成 64位的形式。从Java虚拟机支持的数据类型可以看出,Java对数据类型的内部格式进行了严格规定,这样使得各种Java虚拟机的实现对数据的解释是相同的,从而保证了Java的与平台无关性和可移植性。

规格

编辑
JVM的设计目标是提供一个基于抽象规格描述的计算机模型,为解释程序开发人员提供很好的灵活性,同时也确保Java代码可在符合该规范的任何系统上运行。JVM对其实现的某些方面给出了具体的定义,特别是对Java 可执行代码,即 字节码(Bytecode)的格式给出了明确的规格。这一规格包括 操作码和 操作数的语法和数值、 标识符的数值表示方式、以及Java类文件中的Java对象、 常量缓冲池在JVM的存储映象。这些定义为JVM 解释器开发人员提供了所需的信息和开发环境。Java的设计者希望给开发人员以随心所欲使用Java的自由。
JVM定义了控制Java代码 解释执行和具体实现的五种规格,它们是:
  • JVM 指令系统
  • JVM 寄存器
  • JVM 栈结构
  • JVM 碎片回收堆
  • JVM 存储区

原理

编辑
JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种基于下层的操作系统和硬件平台并利用软件方法来实现的抽象的计算机,可以在上面执行java的字节码程序。
JVM运行原理 JVM运行原理
java编译器只需面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译器,编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。
JVM执行程序的过程 :
I.加载.class文件
II.管理并分配内存
III.执行垃圾收集
JRE(java运行时环境)包含JVM的java程序的运行环境 [1]  
JVM是Java程序运行的容器,但是他同时也是操作系统的一个进程,因此他也有他自己的运行的生命周期,也有自己的代码和数据空间。
JVM在整个jdk中处于最底层,负责与操作系统的交互,用来屏蔽操作系统环境,提供一个完整的Java运行环境,因此也叫虚拟计算机.操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境。
1.创建JVM装载环境和配置
2.装载JVM.dll
3.初始化JVM.dll并挂接到JNIENV(JNI调用接口)实例
4.调用JNIEnv实例装载并处理 class类。 [2]  

指令系统

编辑
JVM指令系统同其他计算机的指令系统极其相似。Java指令也是由 操作码和操作数两部分组
JVM
成。操作码为8位二进制数, 操作数紧随在操作码的后面,其长度根据需要而不同。操作码用于指定一条指令操作的性质(在这里我们采用汇编符号的形式进行说明),如iload表示从 存储器中装入一个整数,anewarray表示为一个新 数组分配空间,iand表示两个整数的"与",ret用于 流程控制,表示从对某一方法的调用中返回。当长度大于8位时,操作数被分为两个以上 字节存放。JVM采用了"big endian [3]  "的编码方式来处理这种情况,即高位bits存放在低字节中。这同 Motorola及其他的RISC CPU采用的 编码方式是一致的,而与Intel采用的"little endian "的编码方式即低位bits存放在低位字节的方法不同。Java指令系统是以Java语言的实现为目的设计的,其中包含了用于调用方法和监视多线程系统的指令。Java的8 位操作码的长度使得JVM最多有256种指令,已使用了160多种操作码。

寄存器

编辑
所有的CPU均包含用于保存系统状态和处理器所需信息的 寄存器组。如果 虚拟机定义较多的寄存器,便可以从中得到更多的信息而不必对栈或内存进行访问,这有利于提高运行速度。然而,如果虚拟机中的寄存器比实际CPU的寄存器多,在实现虚拟机时就会占用处理器大量的时间来用常规 存储器模拟寄存器,这反而会降低虚拟机的效率。针对这种情况,JVM只设置了4个最为常用的寄存器。它们是:
pc程序计数器
optop操作数栈顶指针
frame当前执行环境指针
vars指向当前执行环境中第一个 局部变量的 指针
所有寄存器均为 32位。pc用于记录程序的执行。optop,frame和vars用于记录指向Java栈区的指针。

栈结构

编辑
作为基于栈结构的计算机,Java栈是JVM存储信息的主要方法。当JVM得到一个Java 字节码应用程序后,便为该代码中一个类的每一个方法创建一个栈框架,以保存该方法的状态信息。每个栈框架包括以下三类信息:
局部变量
执行环境
操作数栈
局部变量用于存储一个类的方法中所用到的局部变量。vars寄存器指向该变量表中的第一个局部变量。
执行环境用于保存 解释器对Java字节码进行解释过程中所需的信息。它们是:上次调用的方法、局部变量 指针和操作数栈的栈顶和栈底指针。执行环境是一个执行一个方法的控制中心。例如:如果解释器要执行iadd(整数加法),首先要从frame寄存器中找到当前执行环境,而后便从执行环境中找到操作数栈,从栈顶弹出两个整数进行加法运算,最后将结果压入栈顶。
操作数栈用于存储运算所需操作数及运算的结果。

碎片回收

编辑
Java类的实例所需的存储空间是在堆上分配的。 解释器具体承担为类实例分配空间的工作。解释器在为一个实例分配完存储空间后,便开始记录对该实例所占用的内存区域的使用。一旦对象使用完毕,便将其回收到堆中。在Java语言中,除了new语句外没有其他方法为一对象申请和释放内存。对内存进行释放和回收的工作是由Java运行系统承担的。这允许Java运行系统的设计者自己决定碎片回收的方法。在SUN公司开发的Java解释器和Hot Java环境中,碎片回收用后台线程的方式来执行。这不但为运行系统提供了良好的性能,而且使程序设计人员摆脱了自己控制内存使用的风险。

存储区

编辑
JVM有两类存储区: 常量缓冲池和方法区。常量缓冲池用于存储类名称、方法和字段名称以及串常量。方法区则用于存储Java方法的 字节码。对于这两种 存储区域具体实现方式在JVM规格中没有明确规定。这使得Java应用程序的存储布局必须在运行过程中确定,依赖于具体平台的实现方式。JVM是为Java字节码定义的一种独立于具体平台的规格描述,是 Java平台独立性的基础。JVM还存在一些限制和不足,有待于进一步的完善,但无论如何,JVM的思想是成功的。
对比分析:如果把Java原程序想象成我们的C++原程序,Java原程序编译后生成的字节码就相当于C++原程序编译后的80x86的 机器码(二进制 程序文件),JVM 虚拟机相当于80x86计算机系统,Java 解释器相当于80x86CPU。在80x86CPU上运行的是机器码,在Java解释器上运行的是Java字节码。Java解释器相当于运行Java 字节码的“CPU”,但该“CPU”不是通过硬件实现的,而是用软件实现的。Java解释器实际上就是特定的平台下的一个应用程序。只要实现了特定平台下的解释器程序,Java字节码就能通过解释器程序在该平台下运行,这是Java跨平台的根本。当前,并不是在所有的平台下都有相应Java解释器程序,这也是Java并不能在所有的平台下都能运行的原因,它只能在已实现了Java解释器程序的平台下运行。

运行数据

编辑
JVM定义了若干个程序执行期间使用的数据区域。这个区域里的一些数据在JVM启动的时候创建,在JVM退出的时候销毁。而其他的数据依赖于每一个线程,在线程创建时创建,在线程退出时销毁。分别有 程序计数器,堆,栈,方法区,运行时常量池。[3] 

体系结构

编辑
JVM可以由不同的厂商来实现。由于厂商的不同必然导致JVM在实现上的一些不同,然而JVM还是可以实现跨平台的特性,这就要归功于设计JVM时的体系结构了。我们知道,一个JVM实例的行为不光是它自己的事,还涉及到它的子系统、 存储区域、数据类型和指令这些部分,它们描述了JVM的一个抽象的内部体系结构,其目的不光规定实现JVM时它内部的体系结构,更重要的是提供了一种方式,用于严格定义实现时的外部行为。每个JVM都有两种机制,一个是装载具有合适名称的类(类或是接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。每个JVM又包括方法区、堆、Java栈、 程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一起组成的体系结构图为:
JVM体系结构
JVM的每个实例都有一个它自己的方法域和一个堆,运行于JVM内的所有的线程都共享这些区域;当 虚拟机装载类文件的时候,它解析其中的二进制数据所包含的类信息,并把它们放到方法域中;当程序运行的时候,JVM把程序初始化的所有对象置于堆上;而每个线程创建的时候,都会拥有自己的 程序计数器和Java栈,其中程序计数器中的值指向下一条即将被执行的指令,线程的Java栈则存储为该线程调用Java方法的状态;本地方法调用的状态被存储在本地方法栈,该方法栈依赖于具体的实现。
下面分别对这几个部分进行说明。
执行引擎处于JVM的核心位置,在Java 虚拟机规范中,它的行为是由 指令集所决定的。尽管对于每条指令,规范很详细地说明了当JVM执行 字节码遇到指令时,它的实现应该做什么,但对于怎么做却言之甚少。Java虚拟机支持大约248个字节码。每个字节码执行一种基本的CPU运算,例如,把一个整数加到寄存器, 子程序转移等。Java指令集相当于Java程序的汇编语言。Java指令集中的指令包含一个单字节的操作符,用于指定要执行的操作,还有0个或多个 操作数,提供操作所需的参数或数据。许多指令没有操作数,仅由一个单字节的操作符构成。
虚拟机的内层循环的执行过程如下:
do{
取一个操作符字节;
根据操作符的值执行一个动作;
}while(程序未结束)
由于 指令系统的简单性,使得 虚拟机执行的过程十分简单,从而有利于提高执行的效率。指令中操作数的数量和大小是由操作符决定的。如果操作数比一个 字节大,那么它存储的顺序是高位字节优先。例如,一个16位的参数存放时占用两个字节,其值为:
第一个字节*256+第二个字节 字节码。
指令流一般只是 字节对齐的。指令tableswitch和lookup是例外,在这两条指令内部要求强制的4字节 边界对齐。对于本地方法接口,实现JVM并不要求一定要有它的支持,甚至可以完全没有。Sun公司实现Java本地接口(JNI [3]  )是出于可移植性的考虑,当然我们也可以设计出其它的本地接口来代替Sun公司的JNI [4]  。但是这些设计与实现是比较复杂的事情,需要确保垃圾回收器不会将那些正在被本地方法调用的对象释放掉。
Java的堆是一个运行时数据区,类的实例(对象)从中分配空间,它的管理是由垃圾回收来负责的:不给程序员显式释放对象的能力。Java不规定具体使用的垃圾回收算法,可以根据系统的需求使用各种各样的算法。
Java方法区与传统语言中的编译后代码或是Unix进程中的正文段类似。它保存方法代码(编译后的java代码)和 符号表。在当前的Java实现中,方法代码不包括在垃圾回收堆中,但计划在将来的版本中实现。每个类文件包含了一个Java类或一个Java界面的编译后的代码。可以说类文件是Java语言的执行代码文件。为了保证类文件的平台无关性,Java 虚拟机规范中对类文件的格式也作了详细的说明。其具体细节请参考Sun公司的Java虚拟机规范。
Java虚拟机的寄存器用于保存机器的运行状态,与微处理器中的某些专用寄存器类似。Java虚拟机的寄存器有四种:
pc: Java 程序计数器;
optop: 指向 操作数栈顶端的 指针;
frame: 指向当前执行方法的执行环境的指针;。
vars: 指向当前执行方法的 局部变量区第一个变量的指针。
在上述 体系结构图中,我们所说的是第一种,即程序计数器,每个线程一旦被创建就拥有了自己的程序计数器。当线程执行Java方法的时候,它包含该线程正在被执行的指令的地址。但是若线程执行的是一个本地的方法,那么 程序计数器的值就不会被定义。
Java 虚拟机的栈有三个区域:局部变量区、运行环境区、 操作数区。
局部变量区
每个Java方法使用一个固定大小的局部变量集。它们按照与vars 寄存器的字 偏移量来寻址。 局部变量都是32位的。长整数和 双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址。(例如,一个具有索引n的局部变量,如果是一个双精度浮点数,那么它实际占据了 索引n和n+1所代表的存储空间)虚拟机规范并不要求在局部变量中的 64位的值是64位对齐的。虚拟机提供了把局部变量中的值装载到 操作数栈的指令,也提供了把操作数栈中的值写入局部变量的指令。
JRE和JVM的区别
JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。所有的Java程序都要在JRE下才能运行。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。 [3]  
运行环境区
在运行环境中包含的信息用于 动态链接,正常的方法返回以及异常捕捉。
动态链接
运行环境包括对指向当前类和当前方法的 解释器 符号表的 指针,用于支持方法代码的动态链接。方法的 class文件代码在引用要调用的方法和要访问的变量时使用符号。动态链接把符号形式的方法调用翻译成实际方法调用,装载必要的类以解释还没有定义的符号,并把变量访问翻译成与这些变量运行时的 存储结构相应的 偏移地址。动态链接方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码。
正常的方法返回
如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值。执行环境在正常返回的情况下用于恢复调用者的 寄存器,并把调用者的 程序计数器增加一个恰当的数值,以跳过已执行过的方法调用指令,然后在调用者的执行环境中继续执行下去。
异常捕捉
异常情况在Java中被称作Error(错误)或Exception(异常),是Throwable类的子类,在程序中的原因是:① 动态链接错,如无法找到所需的 class文件。②运行时错,如对一个空指针的引用。程序使用了throw语句。
当异常发生时,Java 虚拟机采取如下措施:
§ 检查与当前方法相联系的catch子句表。每个catch子句包含其有效指令范围,能够处理的异常类型,以及处理异常的代码块地址。
§ 与异常相匹配的catch子句应该符合下面的条件:造成异常的指令在其指令范围之内,发生的异常类型是其能处理的异常类型的子类型。如果找到了匹配的catch子句,那么系统转移到指定的 异常处理块处执行;如果没有找到异常处理块,重复寻找匹配的catch子句的过程,直到当前方法的所有嵌套的catch子句都被检查过。
§ 由于 虚拟机从第一个匹配的catch子句处继续执行,所以catch子句表中的顺序是很重要的。因为Java代码是结构化的,因此总可以把某个方法的所有的异常处理器都按序排列到一个表中,对任意可能的 程序计数器的值,都可以用线性的顺序找到合适的异常处理块,以处理在该程序计数器值下发生的异常情况。
§ 如果找不到匹配的catch子句,那么当前方法得到一个"未截获异常"的结果并返回到当前方法的调用者,好像异常刚刚在其调用者中发生一样。如果在调用者中仍然没有找到相应的 异常处理块,那么这种错误将被传播下去。如果错误被传播到最顶层,那么系统将调用一个缺省的异常处理块。
操作数栈区
机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在只有少量寄存器或非 通用寄存器的机器(如Intel486)上,也能够高效地模拟 虚拟机的行为。操作数栈是32位的。它用于给方法传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果。例如,iadd指令将两个整数相加。相加的两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进 堆栈的。这两个整数将从堆栈弹出、相加,并把结果压回到操作数栈中。
每个原始数据类型都有专门的指令对它们进行必须的操作。每个 操作数在栈中需要一个存储位置,除了long和double型,它们需要两个位置。操作数只能被适用于其类型的操作符所操作。例如,压入两个int类型的数,如果把它们当作是一个long类型的数则是非法的。在Sun的 虚拟机实现中,这个限制由 字节码验证器强制实行。但是,有少数操作(操作符dupe和swap),用于对运行时数据区进行操作时是不考虑类型的。
本地方法栈,当一个线程调用本地方法时,它就不再受到虚拟机关于结构和安全限制方面的约束,它既可以访问虚拟机的运行期数据区,也可以使用本地处理器以及任何类型的栈。例如,本地栈是一个C语言的栈,那么当C程序调用C函数时,函数的参数以某种顺序被压入栈,结果则返回给调用函数。在实现Java 虚拟机时,本地方法接口使用的是C语言的模型栈,那么它的本地方法栈的调度与使用则完全与C语言的栈相同。

运行过程

编辑
上面对 虚拟机的各个部分进行了比较详细的说明,下面通过一个具体的例子来分析它的运行过程。
虚拟机通过调用某个指定类的方法main启动,传递给main一个字符串 数组参数,使指定的类被装载,同时链接该类所使用的其它的类型,并且初始化它们。新建一java源文件并取名HelloApp.java,内容如下:
class HelloApp {
public static void main(String[] args) {
System.out.println("Hello World!");
for (int i = 0; i < args.length; i++ ) {
System.out.println(args);
}
}
}
在命令模式下输入:javac HelloApp.java 进行编译,这时同目录下会产生一个编译后的文件:HelloApp.class
然后在命令行模式下键入:java HelloApp run virtual machine
将通过调用HelloApp的方法main来启动 java虚拟机,传递给main一个包含三个字符串"run"、"virtual"、"machine"的数组。我们略述虚拟机在执行HelloApp时可能采取的步骤。
JVM虚拟机运行过程
开始试图执行类HelloApp的main方法,发现该类并没有被装载,也就是说 虚拟机当前不包含该类的二进制代表,于是虚拟机使用 ClassLoader试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。类被装载后同时在main方法被调用之前,必须对类HelloApp与其它类型进行链接然后初始化。链接包含三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的静态初始化函数和静态域的初始化 构造方法的执行。一个类在初始化之前它的父类必须被初始化。









原文链接: jamesdbloom 翻译: ImportNew.com - 挖坑的张师傅
译文链接: http://www.importnew.com/17770.html
[ 转载请保留原文出处、译者和译文链接。]


这篇文章解释了Java 虚拟机(JVM)的内部架构。下图显示了遵守 Java SE 7 规范的典型的 JVM 核心内部组件。

 

上图显示的组件分两个章节解释。第一章讨论针对每个线程创建的组件,第二章节讨论了线程无关组件。

  • 线程
    • JVM 系统线程
    • 每个线程相关的
    • 程序计数器
    • 本地栈
    • 栈限制
    • 栈帧
    • 局部变量数组
    • 操作数栈
    • 动态链接
  • 线程共享
    • 内存管理
    • 非堆内存
    • 即时编译
    • 方法区
    • 类文件结构
    • 类加载器
    • 更快的类加载
    • 方法区在哪里
    • 类加载器参考
    • 运行时常量池
    • 异常表
    • 符号表
    • Interned 字符串

线程

这里所说的线程指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。run() 返回时,被处理未捕获异常,原生线程将确认由于它的结束是否要终止 JVM 进程(比如这个线程是最后一个非守护线程)。当线程结束时,会释放原生线程和 Java 线程的所有资源。

JVM 系统线程

如果使用 jconsole 或者其它调试器,你会看到很多线程在后台运行。这些后台线程与触发 public static void main(String[]) 函数的主线程以及主线程创建的其他线程一起运行。Hotspot JVM 后台运行的系统线程主要有下面几个:

虚拟机线程(VM thread) 这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。
周期性任务线程 这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。
GC 线程 这些线程支持 JVM 中不同的垃圾回收活动。
编译器线程 这些线程在运行时将字节码动态编译成本地平台相关的机器码。
信号分发线程 这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。

线程相关组件

每个运行的线程都包含下面这些组件:

程序计数器(PC)

PC 指当前指令(或操作码)的地址,本地指令除外。如果当前方法是 native 方法,那么PC 的值为 undefined。所有的 CPU 都有一个 PC,典型状态下,每执行一条指令 PC 都会自增,因此 PC 存储了指向下一条要被执行的指令地址。JVM 用 PC 来跟踪指令执行的位置,PC 将实际上是指向方法区(Method Area)的一个内存地址。

栈(Stack)

每个线程拥有自己的栈,栈包含每个方法执行的栈帧。栈是一个后进先出(LIFO)的数据结构,因此当前执行的方法在栈的顶部。每次方法调用时,一个新的栈帧创建并压栈到栈顶。当方法正常返回或抛出未捕获的异常时,栈帧就会出栈。除了栈帧的压栈和出栈,栈不能被直接操作。所以可以在堆上分配栈帧,并且不需要连续内存。

Native栈

并非所有的 JVM 实现都支持本地(native)方法,那些提供支持的 JVM 一般都会为每个线程创建本地方法栈。如果 JVM 用 C-linkage 模型实现 JNI(Java Native Invocation),那么本地栈就是一个 C 的栈。在这种情况下,本地方法栈的参数顺序、返回值和典型的 C 程序相同。本地方法一般来说可以(依赖 JVM 的实现)反过来调用 JVM 中的 Java 方法。这种 native 方法调用 Java 会发生在栈(一般是 Java 栈)上;线程将离开本地方法栈,并在 Java 栈上开辟一个新的栈帧。

栈的限制

栈可以是动态分配也可以固定大小。如果线程请求一个超过允许范围的空间,就会抛出一个StackOverflowError。如果线程需要一个新的栈帧,但是没有足够的内存可以分配,就会抛出一个 OutOfMemoryError。

栈帧(Frame)

每次方法调用都会新建一个新的栈帧并把它压栈到栈顶。当方法正常返回或者调用过程中抛出未捕获的异常时,栈帧将出栈。更多关于异常处理的细节,可以参考下面的异常信息表章节。

每个栈帧包含:

  • 局部变量数组
  • 返回值
  • 操作数栈
  • 类当前方法的运行时常量池引用

局部变量数组

局部变量数组包含了方法执行过程中的所有变量,包括 this 引用、所有方法参数、其他局部变量。对于类方法(也就是静态方法),方法参数从下标 0 开始,对于对象方法,位置0保留为 this。

有下面这些局部变量:

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

除了 long 和 double 类型以外,所有的变量类型都占用局部变量数组的一个位置。long 和 double 需要占用局部变量数组两个连续的位置,因为它们是 64 位双精度,其它类型都是 32 位单精度。

操作数栈

操作数栈在执行字节码指令过程中被用到,这种方式类似于原生 CPU 寄存器。大部分 JVM 字节码把时间花费在操作数栈的操作上:入栈、出栈、复制、交换、产生消费变量的操作。因此,局部变量数组和操作数栈之间的交换变量指令操作通过字节码频繁执行。比如,一个简单的变量初始化语句将产生两条跟操作数栈交互的字节码。

1
int i;

被编译成下面的字节码:

1
2
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)。

C/C++ 代码一般被编译成对象文件,然后多个对象文件被链接到一起产生可执行文件或者 dll。在链接阶段,每个对象文件的符号引用被替换成了最终执行文件的相对偏移内存地址。在 Java中,链接阶段是运行时动态完成的。

当 Java 类文件编译时,所有变量和方法的引用都被当做符号引用存储在这个类的常量池中。符号引用是一个逻辑引用,实际上并不指向物理内存地址。JVM 可以选择符号引用解析的时机,一种是当类文件加载并校验通过后,这种解析方式被称为饥饿方式。另外一种是符号引用在第一次使用的时候被解析,这种解析方式称为惰性方式。无论如何 ,JVM 必须要在第一次使用符号引用时完成解析并抛出可能发生的解析错误。绑定是将对象域、方法、类的符号引用替换为直接引用的过程。绑定只会发生一次。一旦绑定,符号引用会被完全替换。如果一个类的符号引用还没有被解析,那么就会载入这个类。每个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联的)偏移量。

线程间共享

堆被用来在运行时分配类实例、数组。不能在栈上存储数组和对象。因为栈帧被设计为创建以后无法调整大小。栈帧只存储指向堆中对象或数组的引用。与局部变量数组(每个栈帧中的)中的原始类型和引用类型不同,对象总是存储在堆上以便在方法结束时不会被移除。对象只能由垃圾回收器移除。

为了支持垃圾回收机制,堆被分为了下面三个区域:

  • 新生代
    • 经常被分为 Eden 和 Survivor
  • 老年代
  • 永久代

内存管理

对象和数组永远不会显式回收,而是由垃圾回收器自动回收。通常,过程是这样的:

  1. 新的对象和数组被创建并放入老年代。
  2. Minor垃圾回收将发生在新生代。依旧存活的对象将从 eden 区移到 survivor 区。
  3. Major垃圾回收一般会导致应用进程暂停,它将在三个区内移动对象。仍然存活的对象将被从新生代移动到老年代。
  4. 每次进行老年代回收时也会进行永久代回收。它们之中任何一个变满时,都会进行回收。

非堆内存

非堆内存指的是那些逻辑上属于 JVM 一部分对象,但实际上不在堆上创建。

非堆内存包括:

  • 永久代,包括:
    • 方法区
    • 驻留字符串(interned strings)
  • 代码缓存(Code Cache):用于编译和存储那些被 JIT 编译器编译成原生代码的方法。

即时编译(JIT)

Java 字节码是解释执行的,但是没有直接在 JVM 宿主执行原生代码快。为了提高性能,Oracle Hotspot 虚拟机会找到执行最频繁的字节码片段并把它们编译成原生机器码。编译出的原生机器码被存储在非堆内存的代码缓存中。通过这种方法,Hotspot 虚拟机将权衡下面两种时间消耗:将字节码编译成本地代码需要的额外时间和解释执行字节码消耗更多的时间。

方法区

方法区存储了每个类的信息,比如:

  • Classloader 引用
  • 运行时常量池
    • 数值型常量
    • 字段引用
    • 方法引用
    • 属性
  • 字段数据
    • 针对每个字段的信息
      • 字段名
      • 类型
      • 修饰符
      • 属性(Attribute)
  • 方法数据
    • 每个方法
      • 方法名
      • 返回值类型
      • 参数类型(按顺序)
      • 修饰符
      • 属性
  • 方法代码
    • 每个方法
      • 字节码
      • 操作数栈大小
      • 局部变量大小
      • 局部变量表
      • 异常表
      • 每个异常处理器
      • 开始点
      • 结束点
      • 异常处理代码的程序计数器(PC)偏移量
      • 被捕获的异常类对应的常量池下标

所有线程共享同一个方法区,因此访问方法区数据的和动态链接的进程必须线程安全。如果两个线程试图访问一个还未加载的类的字段或方法,必须只加载一次,而且两个线程必须等它加载完毕才能继续执行。

类文件结构

一个编译后的类文件包含下面的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 提供这个类全名的常量池(constant_pool)索引,比如org/jamesdbloom/foo/Bar。
super_class 提供这个类的父类符号引用的常量池索引。
interfaces 指向常量池的索引数组,提供那些被实现的接口的符号引用。
fields 提供每个字段完整描述的常量池索引数组。
methods 指向constant_pool的索引数组,用于表示每个方法签名的完整描述。如果这个方法不是抽象方法也不是 native 方法,那么就会显示这个函数的字节码。
attributes 不同值的数组,表示这个类的附加信息,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 注解。

可以用 javap 查看编译后的 java class 文件字节码。

如果你编译下面这个简单的类:

1
2
3
4
5
6
package org.jvminternals;
public class SimpleClass {
     public void sayHello() {
         System.out.println( "Hello" );
     }
}

运行下面的命令,就可以得到下面的结果输出: javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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;
}

这个 class 文件展示了三个主要部分:常量池、构造器方法和 sayHello 方法。

  • 常量池:提供了通常由符号表提供的相同信息,详细描述见下文。
  • 方法:每一个方法包含四个区域,
    • 签名和访问标签
    • 字节码
    • LineNumberTable:为调试器提供源码中的每一行对应的字节码信息。上面的例子中,Java 源码里的第 6 行与 sayHello 函数字节码序号 0 相关,第 7 行与字节码序号 8 相关。
    • LocalVariableTable:列出了所有栈帧中的局部变量。上面两个例子中,唯一的局部变量就是 this。

这个 class 文件用到下面这些字节码操作符:

aload0 这个操作码是aload格式操作码中的一个。它们用来把对象引用加载到操作码栈。 表示正在被访问的局部变量数组的位置,但只能是0、1、2、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 的语义要求,比如会进行下面的检查:

  1. 格式一致且格式化正确的符号表
  2. final 方法和类没有被重载
  3. 方法遵循访问控制关键词
  4. 方法参数的数量、类型正确
  5. 字节码没有不当的操作栈数据
  6. 变量在读取之前被初始化过
  7. 变量值的类型正确

在验证阶段做这些检查意味着不需要在运行阶段做这些检查。链接阶段的检查减慢了类加载的速度,但是它避免了执行这些字节码时的多次检查。

准备过程包括为静态存储和 JVM 使用的数据结构(比如方法表)分配内存空间。静态变量创建并初始化为默认值,但是初始化代码不在这个阶段执行,因为这是初始化过程的一部分。

解析是可选的阶段。它包括通过加载引用的类和接口来检查这些符号引用是否正确。如果不是发生在这个阶段,符号引用的解析要等到字节码指令使用这个引用的时候才会进行。

类或者接口初始化由类或接口初始化方法的执行组成。

 

JVM 中有多个类加载器,分饰不同的角色。每个类加载器由它的父加载器加载。bootstrap 加载器除外,它是所有最顶层的类加载器。

  • Bootstrap 加载器一般由本地代码实现,因为它在 JVM 加载以后的早期阶段就被初始化了。bootstrap 加载器负责载入基础的 Java API,比如包含 rt.jar。它只加载拥有较高信任级别的启动路径下找到的类,因此跳过了很多普通类需要做的校验工作。
  • Extension 加载器加载了标准 Java 扩展 API 中的类,比如 security 的扩展函数。
  • System 加载器是应用的默认类加载器,比如从 classpath 中加载应用类。
  • 用户自定义类加载器也可以用来加载应用类。使用自定义的类加载器有很多特殊的原因:运行时重新加载类或者把加载的类分隔为不同的组,典型的用法比如 web 服务器 Tomcat。

 

加速类加载

共享类数据(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)的一个独立的域。

Classloader 引用

所有的类加载之后都包含一个加载自身的加载器的引用,反过来每个类加载器都包含它们加载的所有类的引用。

运行时常量池

JVM 维护了一个按类型区分的常量池,一个类似于符号表的运行时数据结构。尽管它包含更多数据。Java 字节码需要数据。这个数据经常因为太大不能直接存储在字节码中,取而代之的是存储在常量池中,字节码包含这个常量池的引用。运行时常量池被用来上面介绍过的动态链接。

常量池中可以存储多种类型的数据:

  • 数字型
  • 字符串型
  • 类引用型
  • 域引用型
  • 方法引用

示例代码如下:

1
Object foo = new Object();

写成字节码将是下面这样:

1
2
3
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 操作码创建一个操作数栈顶元素引用的额外拷贝。最后用 invokespecial 来调用第 2 行的实例初始化方法。操作码也包含一个指向常量池的引用。初始化方法把操作数栈出栈的顶部引用当做此方法的一个参数。最后这个新对象只有一个引用,这个对象已经完成了创建及初始化。

如果你编译下面的类:

1
2
3
4
5
6
7
8
package org.jvminternals;
public class SimpleClass {
 
     public void sayHello() {
         System.out.println( "Hello" );
     }
 
}

生成的类文件常量池将是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 字节常量
Long 8 字节常量
Float 4 字节常量
Double 8 字节常量
String 字符串常量指向常量池的另外一个包含真正字节 Utf8 编码的实体
Utf8 Utf8 编码的字符序列字节流
Class 一个 Class 常量,指向常量池的另一个 Utf8 实体,这个实体包含了符合 JVM 内部格式的类的全名(动态链接过程需要用到)
NameAndType 冒号(:)分隔的一组值,这些值都指向常量池中的其它实体。第一个值(“:”之前的)指向一个 Utf8 字符串实体,它是一个方法名或者字段名。第二个值指向表示类型的 Utf8 实体。对于字段类型,这个值是类的全名,对于方法类型,这个值是每个参数类型类的类全名的列表。
Fieldref, Methodref, InterfaceMethodref 点号(.)分隔的一组值,每个值都指向常量池中的其它的实体。第一个值(“.”号之前的)指向类实体,第二个值指向 NameAndType 实体。

异常表

异常表像这样存储每个异常处理信息:

  • 起始点(Start point)
  • 结束点(End point)
  • 异常处理代码的 PC 偏移量
  • 被捕获异常的常量池索引

如果一个方法有定义 try-catch 或者 try-finally 异常处理器,那么就会创建一个异常表。它为每个异常处理器和 finally 代码块存储必要的信息,包括处理器覆盖的代码块区域和处理异常的类型。

当方法抛出异常时,JVM 会寻找匹配的异常处理器。如果没有找到,那么方法会立即结束并弹出当前栈帧,这个异常会被重新抛到调用这个方法的方法中(在新的栈帧中)。如果所有的栈帧都被弹出还没有找到匹配的异常处理器,那么这个线程就会终止。如果这个异常在最后一个非守护进程抛出(比如这个线程是主线程),那么也有会导致 JVM 进程终止。

Finally 异常处理器匹配所有的异常类型,且不管什么异常抛出 finally 代码块都会执行。在这种情况下,当没有异常抛出时,finally 代码块还是会在方法最后执行。这种靠在代码 return 之前跳转到 finally 代码块来实现。

符号表

除了按类型来分的运行时常量池,Hotspot JVM 在永久代还包含一个符号表。这个符号表是一个哈希表,保存了符号指针到符号的映射关系(也就是 Hashtable),它拥有指向所有符号(包括在每个类运行时常量池中的符号)的指针。

引用计数被用来控制一个符号从符号表从移除的过程。比如当一个类被卸载时,它拥有的在常量池中所有符号的引用计数将减少。当符号表中的符号引用计数为 0 时,符号表会认为这个符号不再被引用,将从符号表中卸载。符号表和后面介绍的字符串表都被保存在一个规范化的结构中,以便提高效率并保证每个实例只出现一次。

字符串表

Java 语言规范要求相同的(即包含相同序列的 Unicode 指针序列)字符串字面量必须指向相同的 String 实例。除此之外,在一个字符串实例上调用 String.intern() 方法的返回引用必须与字符串是字面量时的一样。因此,下面的代码返回 true:

1
( "j" + "v" + "m" ).intern() == "jvm"

Hotspot JVM 中 interned 字符串保存在字符串表中。字符串表是一个哈希表,保存着对象指针到符号的映射关系(也就是Hashtable),它被保存到永久代中。符号表和字符串表的实体都以规范的格式保存,保证每个实体都只出现一次。

当类加载时,字符串字面量被编译器自动 intern 并加入到符号表。除此之外,String 类的实例可以调用 String.intern() 显式地 intern。当调用 String.intern() 方法时,如果符号表已经包含了这个字符串,那么就会返回符号表里的这个引用,如果不是,那么这个字符串就被加入到字符串表中同时返回这个引用。





你可能感兴趣的:(java)