JVM运行时数据区

一、JVM的Run-Time Data Areas(运行时数据区)包含哪些部分?

运行时数据区是在JVM运行的时候操作所分配的内存区。运行时内存区可以划分为5个区域,如图:

rt-data-area.jpg

在上图的区域中,其中3个:PC Register,JVM stack 以及Native Method Statck都是按照线程创建的,另外三个:Heap,Method Area以及Runtime Constant Pool都是被所有线程公用的。

根据Oracle Java SE8 官方文档:

(The Java® SE 8 Edition of The Java Virtual Machine Specification)

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5

运行时内存区包含如下 5个部分:

  1. The pc Register(程序计数器,Program Counter Register)

  2. JVM Stacks(虚拟机栈,包含很多 “栈帧Frame”)

    —— 在JVM Spec v1.0版本中 叫做 “Java Stack”

  3. Heap(堆)

  4. Method Area(方法区)

    方法区包括:Run-Time Constant Pool(运行时常量池)

  5. Native Method Stacks(本地方法栈)


1、Program Counter Register

Oracle的官方文档:

    每个JVM线程都有自己的PC(程序计数器)寄存器。在任何时候,每个JVM线程都在执行一个且仅一个Method的代码,这个Method叫Current Method。如果该Method不是原生的,则PC寄存器包含当前正在执行的JVM指令的地址(Address of the current instruction (or opcode) unless it is native)。如果当前由线程执行的方法是原生的,则JVM的PC寄存器的值是undefined的。JVM的PC寄存器足够大,可以在特定平台上保存返回地址或本地指针。

    All CPUs have a PC, typically the PC is incremented after each instruction and therefore holds the address of the next instruction to be executed. The JVM uses the PC to keep track of where it is executing instructions, the PC will in fact be pointing at a memory address in the Method Area.

简而言之:

    每个线程启动的时候,都会创建一个PC(Program Counter ,程序计数器)寄存器。PC寄存器里保存有当前正在执行的JVM指令的地址。

    程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

    由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

    如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。


2、JVM Stack(又叫Java Stack)

Oracle的官方文档:

    每个线程都有自己的Stack,为在该线程上执行的每个方法保存一个帧(Frame)。Stack是后进先出(LIFO)数据结构,因此当前执行的方法位于Stack的顶部。为每个方法调用创建一个新的Frame并将其添加(推送)到Stack的顶部。当方法正常返回或在方法调用期间引发未捕获的异常时,Frame将被移除(弹出)。除了push和pop帧对象外,Stack不直接被操作,因此帧对象可以在堆中分配,内存不需要是连续的。(英语原文:Stack

    JVM Stack的结构为:{JVM Stack [Frame][Frame][Frame]... }

    JVM Spec规定,Stack可以是动态大小或固定大小。如果线程需要的Stack数量大于允许的Stack,则会引发StackOverflowError。如果一个线程需要一个新的帧,并且没有足够的内存来分配它,那么就会抛出OutOfMemoryError。

简而言之:

    每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈(push)或出栈(pop)。如果出现了异常,堆栈跟踪信息的每一行都代表一个栈帧的信息,这些信息它是通过类似于printStackTrace()这样的方法来展示的。

    与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

    经常有人把Java内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java内存区域的划分实际上远比这复杂。“栈内存”所指的就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。

    局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

    关于栈内存的分配,参考C语言:

1)栈区(stack)—  由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 

2)堆区(heap) — 一般由程序员分配释放。

    其实Java中,堆内存(Heap)和栈内存(Stack)也是类似的,Java需要的栈内存是由编译器自动分配的,它不占用Heap内存空间。

    虚拟机栈的最大总内存大致上等于:

= JVM进程能占用的最大内存(依赖于具体操作系统和配置)
   - 最大堆内存
   - 最大方法区内存
   - 程序计数器内存(可以忽略不计) - JVM进程本身消耗内存

    

    关于 Stack Frame 栈帧 的详细说明,参见后文“JVM Stack的Frame”章节。


3、Native Method Stacks(简称native stack)

    它与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。 

详细说明:

     Java线程调用native method(非Java语言编写的程序)时,通常会创建native stack(类似于C语言的stack)来支持。

     当某个线程调用一个本地方法时,他就进入了一个全新的并且不再受虚拟机限制的世界 ,本地方法可以通过本地方法接口来访问虚拟机运行时的数据区,但不止于此,他还可以做任何他想做的事情。比如,他甚至可以直接使用本地处理器中的寄存器,或者直接从本地内存的堆中分配任意数量的内存等等。总之,他和虚拟机拥有同样的权限(或者说能力)。

    任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当他调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

    并且native方法通常(取决于JVM实现)可以返回JVM并调用Java方法。这样的从native到Java的调用将在JVM堆栈上发生,该线程会保存本地方法栈的状态并进入到JVM并在JVM堆栈上创建新的帧。一个线程可能在整个生命周期中都执行Java方法,操作他的Java栈;或者他可能毫无障碍地在Java栈和本地方法栈之间跳转。如下图所示:

    03231458_sjaE1.jpg

    Native stack的特点和JVM stack是一样的,可以是动态大小或固定大小,都可能导致产生StackOverflowError和OutOfMemoryError。


4、Heap

Oracle官方文档: 

   堆是运行时数据区域,从中为所有类实例和数组分配内存。堆内存被所有JVM线程共享。

    The Heap is used to allocate class instances and arrays at runtime. Arrays and objects can never be stored on the stack because a frame is not designed to change in size after it has been created. 

    The frame only stores references that point to objects or arrays on the heap. Unlike primitive variables and references in the local variable array (in each frame), objects are always stored on the heap so they are not removed when a method ends. Instead objects are only removed by the garbage collector.

    To support garbage collection the heap is divided into three sections:

  • Young Generation(Often split between Eden and Survivor)

  • Old Generation (also called Tenured Generation)

  • Permanent Generation

简而言之:

    Java堆用来保存实例或者对象的空间,而且它是垃圾回收的主要目标。JVM提供者可以决定怎么来配置堆空间,以及不对它进行垃圾回收。几乎所有的对象实例都在这里分配内存。 如果从内存回收的角度看,由于现在收集器(比如Hotspot)基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

详细描述:

    对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换②优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

    Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。如果从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。Java堆只要求逻辑上是连续的,在物理空间上可以不连续。

    堆的分区图如下:

1543311151403041208.gif

    关于Heap和Non-Heap的内存管理,参见我的另一篇文章《JVM内存管理和垃圾回收》。


5、Method Area

Oracle官方文档:

    方法区存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括在类和实例初始化及接口初始化中使用的特殊方法(例如<clinit>)。方法区存在OutOfMemoryError。

    The method area stores per-class information 包括:

  • Classloader Reference

  • Run Time Constant Pool

     (参见后面的专题说明)

  • Field data

   (Per field)

        Name

        Type

        Modifiers

        Attributes

  • Method data

     (Per method)

        Name

        Return Type

        Parameter Types (in order)

        Modifiers

        Attributes

  • Method code

   (Per method)

        Bytecodes

        Operand stack size

        Local variable size

        Local variable table

        Exception table

    方法区中这些类的信息,其实在 Class File Structure 中可以找到,具体参见Class File Structure。    

    All threads share the same method area, so access to the method area data and the process of dynamic linking must be thread safe. If two threads attempt to access a field or method on a class that has not yet been loaded it must only be loaded once.

简而言之:

    方法区是所有线程共享的,它是在JVM启动的时候创建的。JVM的提供者可以通过不同的方式来实现方法区。是否对方法区进行垃圾回是可选的。在Oracle 的HotSpot JVM里,方法区被称为永久代(PermGen),默认最小值为16MB,最大值为是MaxPermSize,默认是64M。(JDK 1.8中永久区被移除了,取而代之的是元数据区)。方法区保存所有被JVM加载的类的描述信息(包括类的名称、方法信息、字段信息)、运行时常量池,以及编译器编译后的字节码。参见:http://denverj.iteye.com/blog/1209506

详细说明:

    方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、JIT编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。 对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的概念的。即使是HotSpot虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory来实现方法区的规划了。 Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun公司的BUG列表中,曾出现过的若干个严重的BUG就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。

补充:

    方法区存放的数据,和Class字节码息息相关。

    在Class文件结构中,最头的4个字节用于存储魔数Magic Number,用于确定一个文件是否能被JVM接受,再接着4个字节用于存储版本号,再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(constant_pool_count)存储常量池容量计数值。


6、关于 运行时常量池(Run Time Constant Pool)

为了解释运行时常量池,还得先知道Class文件结构,常量池表(constant_pool table) 和 字符串常量池(String Pool)。参见后面的专题讲解。

简而言之:

    运行时数据区是包含在方法区里的,不过,对于JVM的操作而言,它是一个核心的角色,因此在JVM规范里特别提到了它的重要性。它存放字面量和符号引用量,包含了类或接口所有的静态信息。池中的数据通过索引访问,当一个类、方法或者变量被引用时,JVM通过运行时常量区来查找方法或者变量在内存里的实际地址。

    每个运行时常量池都是从JVM的方法区域分配的,它也存在OutOfMemoryError。

    有关运行时常量池的构造信息,可参见 类的加载、链接和初始化


7、Non-heap

非堆区的定义为:Objects that are logically considered as part of the JVM mechanics are not created on the Heap.

那么除了heap之外的,栈内存(包括JVM Stack和Native Method Stack)和Method Area应该是属于Non-heap,除此之外,还有如下:

  • Permanent Generation that contains

        the method area

        interned strings

  • Code Cache used for compilation and storage of methods that have been compiled to native code by the JIT compiler

JVM生成的native code存放的内存空间称之为Code Cache;JIT编译、JNI等都会编译代码到native code,其中JIT生成的native code占用了Code Cache的绝大部分空间。可以通过-XX:ReservedCodeCacheSize等参数设置。


二、JVM Stack的Frame(帧)包含哪些部分?

根据Oracle Java SE8 官方文档:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6

Frame包含如下内容:

  1. Local Variables Array (局部变量,数组)

  2. Operand Stacks(操作数栈,LIFO stack)

  3. Dynamic Linking(动态链接)

    a reference to the run-time constant pool of the class of the current method.

  4. Return Address

    Normal Method Invocation Completion(方法返回地址——正常完成出口)

    Abrupt Method Invocation Completion(方法返回地址——异常完成出口)

  5. Extended information (扩展信息,取决于具体实现,例如debug信息)

什么是动态链接?

1)部分符号引用在类加载阶段(解析)的时候就转化为直接引用,这种转化为静态链接

2)部分符号引用在运行期间转化为直接引用,这种转化为动态链接

其中,一般把(动态链接、方法返回地址、附加信息)统称 “帧栈信息(frame data)”。所以,又可以说Frame由:“局部变量区,操作数栈和帧数据区”,三个部分组成。

下图反映了 frame的一些信息:

1555062886967028223.png

(基于The Java Virtual Machine Specification, Java SE 7 Edition)

Frame的基本结构为:

{Frame [ReturnValue] [LocalVariables[][][]...] [OperandStack [][][]...] [ConstPoolRef] }

详细说明如下:

1. Local Variables

    Each frame contains an array of variables known as its local variables. The length of the local variable array of a frame is determined at compile-time.

    A single local variable can hold a value of type boolean, byte, char, short, int, float, reference, or returnAddress. A pair of local variables can hold a value of type long or double(占两个连续位置,For example, a value of type double stored in the local variable array at index n actually occupies the local variables with indices n and n+1). 注意,这并不要求数组长度为偶数,long和double不需要64位对齐,实现方式是自由的)。

    The JVM uses local variables to pass parameters on method invocation.(JVM使用local variables在方法调用时传递参数)。

    在实例方法被调用时,local variables数组下标0的元素,通常用来存放 this引用,其它参数从位置1开始,在类方法被调用时,则没有this。参见:http://denverj.iteye.com/blog/1218298

    在后面的 Operand Stacks章节中,将展示Local variables的一个实际使用场景。

2. Operand Stacks

    Each frame contains a last-in-first-out (LIFO) stack known as its operand stack. The maximum depth of the operand stack of a frame is determined at compile-time.

    The operand stack is empty when the frame that contains it is created(当frame创建的时候stack是空的). The Java Virtual Machine supplies instructions to load constants or values from local variables or fields onto the operand stack(然后JVM指令加载本地变量或者类属性到 操作数栈中). Other Java Virtual Machine instructions take operands from the operand stack, operate on them, and push the result back onto the operand stack(其它JVM指令从 操作数栈中取数据,然后把结果存入 操作数栈中). The operand stack is also used to prepare parameters to be passed to methods and to receive method results(操作数栈也被用来 存放将要传入方法的参数 和 从方法返回的结果).

简而言之:

    操作数栈(Operand stack) 是 方法实际运行的工作空间。每个方法都在操作数栈和局部变量数组之间交换数据,并且压入或者弹出其他方法返回的结果。操作数栈所需的最大空间是在编译期确定的。因此,操作数栈的大小也可以在编译期间确定。

    例如,iadd指令将两个int值相加。它要求要添加的int值是操作数堆栈的前两个值,由前面的指令推送到这里。两个int值都从操作数堆栈中弹出。相加之后,它们的和被推回到操作数堆栈中。子计算可以嵌套在操作数堆栈上,从而子计算产生的值可以被使用。

    Each entry on the operand stack can hold a value of any Java Virtual Machine type, including a value of type long or type double.(操作数栈 上的条目 可以是任意JVM类型,包括long和double)

    At any point in time, an operand stack has an associated depth(在任何一个确定的时间,可以知道操作数栈的深度), where a value of type long or double contributes two units to the depth and a value of any other type contributes one unit.(其中long和double栈两个单元,其他类型占一个单元)

    补充:The sizes of the local variable array and the operand stack are determined at compile-time, Thus the memory for these structures can be allocated simultaneously on method invocation(在方法被调用时分配内存).

    举个例子,Java代码如下:

int a=1;
int b=2;
int c=a+b;

    它的执行过程如下:

0: iconst_1 // Push 1 to the operation of the stack. A value of int greater than 5 will use the bipush <i> instruction. 

1: istore_0 // Pop the top element, stored in the index=0 local variable. 

2: iconst_2 // Push 2 to the operation of the stack

3: istore_1 // Pop the top element on the stack, stored in the index=1 local variable. 

首先,获得 常量1和2,分别存入local variable中的第1和第2的位置(假设local variable之前是空的);

4: iload_0  // The local variable load index=0 to the top of the stack

5: iload_1  // The local variable load index=1 to the top of the stack

6: iadd     // The top two numbers pop out together, and stores the result to the top of the stack

然后从local variable中取出第1和第2位置的数据,进行add操作;

7: istore_2 // Store the results to the index=2 local variable

最后,把上一步的add结果存入 local variable的第3位置。


3. Dynamic Linking

Each frame contains a reference to the run-time constant pool for the type of the current method to support dynamic linking of the method code(每个帧都包含对运行时常量池的引用,以支持method code的动态链接). The class file code for a method refers to methods to be invoked and variables to be accessed via symbolic references(method code代表的是 将要被调用的方法和通过 符号引用被访问的变量). Dynamic linking translates these symbolic method references into concrete method references, loading classes as necessary to resolve as-yet-undefined symbols(动态链接将这些符号方法引用转换为具体的方法引用,根据需要加载类以解析尚未定义的符号), and translates variable accesses into appropriate offsets in storage structures associated with the run-time location of these variables(并将变量访问转换为与这些变量的运行时位置相关联的存储结构中的适当偏移量).


三、对 运行时常量池 的进一步理解

为了解释运行时常量池,还得先知道Class文件结构、常量池表(constant_pool table) 和 字符串常量池(String Pool)。


1、Class File Structure 以及 常量池表(constant_pool table)

一个编译后的Class结构如下:

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

specifies information about the version of the class and the version of the JDK this class was compiled for.
constant_poolsimilar to a symbol table although it contains more data this is described in more detail below.
access_flagsprovides the list of modifiers for this class.
this_classindex into the constant_pool providing the fully qualified name of this class i.e. org/jamesdbloom/foo/Bar
super_classindex into the constant_pool providing a symbolic reference to the super class i.e. java/lang/Object
interfacesarray of indexes into the constant_pool providing a symbolic references to all interfaces that have been implemented.
fieldsarray of indexes into the constant_pool giving a complete description of each field.
methodsarray of indexes into the constant_pool giving a complete description of each method signature, if the method is not abstract or native then the bytecode is also present.
attributesarray of different value that provide additional information about the class including any annotations with RetentionPolicy.CLASS or RetentionPolicy.RUNTIME

其中最重要的莫过于 constant_pool了。

可以用 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;
}

    主要由 三部分构成:the constant pool, the constructor and the sayHello method.

    注意看 constant pool 里面 #3 为 String,值指向的是 #20 为 Hello。另外,我还做了一个实验,如果把方法名称也改成 Hello,那么constant pool里面只会有一个 Hello。


2、运行时常量池(Run Time Constant Pool)

    它是constant_pool table in a class file的运行时表现形式。The constant_pool table in the binary representation of a class/interface is used to construct the run-time constant pool upon class/interface creation。(当一个类或接口被JVM创建时,constant_pool信息就会被用来构造运行时常量池)【注:具体是在resolving阶段执行,class加载过程:加载(Loading)、链接(包括:Verifying + Preparing + Resolving)和初始化(Initializing)】。

附:constant_pool table 和 Runtime Constant Pool的区别

  • 常量池表(constant_pool table)

        Class文件中存储所有常量(包括字符串)的table。

        它是Class 字节码文件中的一类结构化数据,还不是运行时的内容。

  • 运行时常量池(Runtime Constant Pool)

        JVM 运行时内存中方法区的一部分,这是运行时的内容。

        这部分数据绝大部分是随着JVM 运行,从常量池表转化而来,每个Class 都对应一个运行时常量池

        上面说绝大部分是因为:除了Class 中常量池内容,还可能包括动态生成并加入这里的内容。

    

    运行时常量池的功能类似于传统编程语言的符号表,尽管它包含的数据范围比典型符号表更广。

    常量池主要用于存放两大类常量:字面量(Literal) 和 符号引用量(Symbolic References)。主要类型如下(都是封装成结构体):

    1. The CONSTANT_Class_info Structure

    2. The CONSTANT_Fieldref_info, CONSTANT_Methodref_info, and CONSTANT_InterfaceMethodref_info Structures

    3. The CONSTANT_String_info Structure

    4. The CONSTANT_Integer_info and CONSTANT_Float_info Structures

    5. The CONSTANT_Long_info and CONSTANT_Double_info Structures

    6. The CONSTANT_NameAndType_info Structure

    7. The CONSTANT_Utf8_info Structure

    8. The CONSTANT_MethodHandle_info Structure

    9. The CONSTANT_MethodType_info Structure

    10. The CONSTANT_InvokeDynamic_info

具体说明参见JVM官方文档The Run-Time Constant PoolThe Constant Pool

字面量包括两种大类:

  • numeric literals(对应上面的第4、5项)

  • string literals(对应上面的第3项)

剩下的全都是 符号引用。

    数字字面量(numeric literals) 包括IEEE 754标准的:int, long, float, 和double。Java语言中的各种基本类型都属于numeric literals。

    另外,Java中八种基本类型的包装类(比如Integer、Long等)的大部分都实现了常量池技术,它们是Byte、Short、Integer、Long、Character、Boolean,另外两种浮点数类型的包装类(Float、Double)则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值在-128到127时才可使用对象池。参见:http://chenzehe.iteye.com/blog/1727062

    字符串字面量(string literals) 是String类的引用。例如:

    private String str = "hello"; 

    编译成字节码文件后,这个"hello"会存在于 constant_pool表中,最终 转换成 运行时常量池时,是这样做的:

  • If the method String.intern has previously been called on an instance of class String containing a sequence of Unicode code points identical to that given by the CONSTANT_String_info structure, then the result of string literal derivation is a reference to that same instance of class String.(翻译一下就是,如果 包含该字符串的 Unicode序列的String类的intern方法已经被调用过了,那就直接用这个String的实例)

  • Otherwise, a new instance of class String is created containing the sequence of Unicode code points given by the CONSTANT_String_info structure; a reference to that class instance is the result of string literal derivation. Finally, the intern method of the new String instance is invoked.(否则,新new一个String类的实例来存放该 Unicode字符串序列,最后调用一次 该String实例的intern方法一次)

    但是,这里还没有说清楚,这个unicode字符序列,到底是存放在哪里的。后面会讲String Table,然后继续讨论这个问题。


3、字符串常量池(String Pool,或者String Table)

    JVM规范要求进入这里的String 实例叫“被驻留的字符串 - interned string”,各个JVM 可以有不同的实现,HotSpot 是设置了一个哈希表 - StringTable 来引用堆中的字符串实例,被引用就是被驻留(注意是引用,而不是存放,存放是在堆中Java1.7+)。

    HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet<String>。这是个纯运行时的结构,而且是惰性(lazy)维护的。注意它只存储对java.lang.String实例的引用,而不存储String对象的内容。 注意,它只存了引用,根据这个引用可以得到具体的String对象。一般我们说一个字符串进入了全局的字符串常量池其实是说在这个StringTable中保存了对它的引用,反之,如果说没有在其中就是说StringTable中没有对它的引用。

    字符串常量池与运行时常量池不是一个概念:

  •     String Pool 是JVM 实例全局共享的全局只有一个,而Runtime Constant Pool 每个类都有一个。

  •     String Pool 只记录字符串对象,而Runtime Constant Pool 记录各种对象。

    字符串池在JDK 1.7 之后存在于Heap 堆中,旧版存在于方法区中。

    其实字符串常量池这个问题涉及到一个设计模式,叫“享元模式”,顾名思义 - - - > 共享元素模式。

    也就是说:一个系统中如果有多处用到了相同的一个元素,那么我们应该只存储一份此元素,而让所有地方都引用这一个元素。

    Java中String部分就是根据享元模式设计的,而那个存储元素的地方就叫做“字符串常量池 - String Pool”。

    >> 追踪OpenJDK 关于String pool实现方式,可略知一二:

String的native String intern()方法源码 

openjdk\jdk\src\share\native\java\lang\String.c

#include "jvm.h"
#include "java_lang_String.h"
JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
 return JVM_InternString(env, this);
}

在jvm头文件中 

openjdk\jdk\src\share\javavm\export\jvm.h

/*
* java.lang.String
*/
JNIEXPORT jstring JNICALL
JVM_InternString(JNIEnv *env, jstring str);

在jvm.cpp文件中 

openjdk\hotspot\src\share\vm\prims\jvm.cpp

// String support ///////////////////////////////////////////////////////////////////////////
JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
JVMWrapper("JVM_InternString");
JvmtiVMObjectAllocEventCollector oam;
if (str == NULL) return NULL;
oop string = JNIHandles::resolve_non_null(str);
oop result = StringTable::intern(string, CHECK_NULL);
return (jstring) JNIHandles::make_local(env, result);
JVM_END

可见,里面有个 StringTable是核心。

延伸阅读:字面量进入字符串常量池的时机(关于lazy resolve)

    上面所说,在resolve阶段,字符串常量会被创建出来,并在字符串常量池中驻留其引用。

    这里说的比较笼统,没错,是resolve阶段,但是并不是大家想的那样,立即就创建对象并且在字符串常量池中驻留了引用。 JVM规范里明确指定resolve阶段可以是lazy的。

    JVM规范里Class文件的常量池项的类型,有两种东西:

  • CONSTANT_Utf8

  • CONSTANT_String

    后者是String常量的类型,但它并不直接持有String常量的内容,而是只持有一个index,这个index所指定的另一个常量池项必须是一个CONSTANT_Utf8类型的常量,这里才真正持有字符串的内容。

     在HotSpot VM中,运行时常量池里,

  • CONSTANT_Utf8 -> Symbol*(一个指针,指向一个Symbol类型的C++对象,内容是跟Class文件同样格式的UTF-8编码的字符串)

  • CONSTANT_String -> java.lang.String(一个实际的Java对象的引用,C++类型是oop) 

    CONSTANT_Utf8会在类加载的过程中就全部创建出来,而CONSTANT_String则是lazy resolve的,例如说在第一次引用该项的ldc指令被第一次执行到的时候才会resolve。那么在尚未resolve的时候,HotSpot VM把它的类型叫做JVM_CONSTANT_UnresolvedString,内容跟Class文件里一样只是一个index;等到resolve过后这个项的常量类型就会变成最终的JVM_CONSTANT_String,而内容则变成实际的那个oop。

    看到这里想必也就明白了, 就HotSpot VM的实现来说,加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池(即在StringTable中并没有相应的引用,在堆中也没有对应的对象产生)。

    Idc指令是什么?

    简单地说,它用于将int、float或String型常量值从常量池中推送至栈顶

    执行ldc指令就是触发这个lazy resolution动作的条件 ldc字节码在这里的执行语义是:到当前类的运行时常量池(runtime constant pool,HotSpot VM里是ConstantPool + ConstantPoolCache)去查找该index对应的项,如果该项尚未resolve则resolve之,并返回resolve后的内容。

    在遇到String类型常量时,resolve的过程如果发现StringTable已经有了内容匹配的java.lang.String的引用,则直接返回这个引用,反之,如果StringTable里尚未有内容匹配的String实例的引用,则会在Java堆里创建一个对应内容的String对象,然后在StringTable记录下这个引用,并返回这个引用出去。


4、下面以一个例子来理清 运行时常量池 和 字符串常量池 的关系

在*.java文件中有如下代码:

int a = 17;
String b = "hello";

    首先,17和"hello"会在经过javac(或者其他编译器)编译过后变为Class文件中constant_pool table的内容。

    当我们的程序运行时,也就是说JVM运行时,每个Class字节码 constant_pool table中的内容会被加载到JVM 内存中的方法区中各自Class对象的Runtime Constant Pool中。

    一个没有被String Pool包含的Runtime Constant Pool中的字符串(这里是"hello")会被加入到String Pool中(HosSpot 使用hashtable 引用方式),步骤如下:

  • 在Java Heap (Java 1.7+) 中根据"hello"字面量创建一个字符串对象。

  • 将字面量"hello"与字符串对象的引用在 hashtable 中关联起来,键 - 值 形式是:"hello" = 对象的引用地址。

另外来说,当一个新的字符串出现在Runtime Constant Pool中时怎么判断需不需要在Java Heap中创建新对象呢?

策略是这样:

    会先去根据equals来比较Runtime Constant Pool中的这个字符串是否和String Pool中某一个是相等的(也就是找是否已经存在),如果有那么就不创建,直接使用其引用。

如此,就实现了享元模式,提高的内存利用效率。


    再举个例子深入理解,下面的代码执行结果是什么?

public static void main(String[] args) {
    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);
 
    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);
}

调换一下行序,如下,输出结果又是什么?

public static void main2(String[] args) {
    // s为new出来的,同时常量池中放入 "1"
    String s = new String("1");
    // 将s放入常量池,但是由于常量池中已经存在"1",故这个方法没实际效果
    s.intern();
    // s2从常量池中取出
    String s2 = "1";
    System.out.println(s == s2);
    
    // s3为new出来的
    String s3 = new String("1") + new String("1");
    // 将s3放入常量池
    s3.intern();
    // s4从常量池中取出
    String s4 = "11";
    System.out.println(s3 == s4);
}

备注:针对Java 1.7和1.8

分析:

    首先说两点知识:

    1. new String("1")中的"1"是字面量,在class加载后【注:具体是在resolve阶段执行】就已经进入了运行时常量池;

    2. new String("1") + new String("1") 实际上调用的是StringBuilder的append添加到一个char[]中,最后toString是调用new String(char value[], int offset, int count)返回的。

    见第二个的注释:

    1)s == s2输出始终为 false,因为 s2="1"始终是取的 new String("1")里面的那个"1",而s始终指向的不是那个"1"。

    2)第一个 s3==s4为false,因为s3是new出来的,s4是从常量池中取的,两者不同。第二个 s3 == s4为true,因为常量池中本没有"11",是调用s3.intern() 后,把"11"放入了常量池,此时常量池中的"11"和s3是同一个,再取s4="11",实际上取的是常量池里面的"11",就等于s3。

    参考资料:https://www.zhihu.com/question/55994121


© 2009-2020 Zollty.com 版权所有。渝ICP备20008982号