JVM中三个常量池(两种常量池)的解析及其随jdk版本的变化

未分类 一条评论

目录

        常量池

        静态常量池

        运行时常量池

        字符串常量池

        三个常量池的关系

        其随jdk版本的变化

 

常量池

请注意常量池是线程共享数据区,常量池的内容:

 

常量池的好处:

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
  (1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
  (2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。

 

静态常量池

静态常量池也就是Class文件中的常量池,下面用一张图来看看静态常量池在Class文件中的位置:

 

从上图可以看出,Class文件中包括:

魔数:它的唯一作用是确定这个文件是否可以被JVM接受。很多文件储存标准中都使用魔数来进行身份识别的,其占用这个文件的前四个字节。

版本号:第5和第6个字节是副版本号,第7个和第8 个是主版本号。

常量池计数器:也就是常量池的入口,代表常量池的容量计数器。

常量池:常量池中主要存放两类常量:字面量和符号引用。字面量比较接近Java语言层面的常量概念。就是我们什么提到的常量。而符号引用则属于编译原理的方面的概念。包括以下三类常量:

(1)类和接口的全限定名

(2)字段的名称和描述符

(3)方法的名称和描述符

静态常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。其中符号引用其实引用的就是常量池里面的字符串,但符号引用不是直接存储字符串,而是存储字符串在常量池里的索引。

Class文件被加载完成后,java虚拟机会将静态常量池里的内容转移到运行时常量池里,在静态常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。

 

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池还有个更重要的的特征:动态性。Java要求,编译期的常量池的内容可以进入运行时常量池,运行时产生的常量也可以放入池中。常用的是String类的intern()方法。

既然运行时常量池是方法区的一部分自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

 

字符串常量池(string pool

字符串常量池存在运行时常量池之中(在JDK7之前存在运行时常量池之中,在JDK7已经将其转移到堆中)。

字符串常量池的存在使JVM提高了性能和减少了内存开销。

使用字符串常量池,每当我们使用字面量(String s=1;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈中)。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)

 

使用字符串常量池,每当我们使用关键字new(String s=new String(1);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中创建该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中创建该对象的副本,然后将堆中对象的地址赋值给引用s

下图是API说明:

 

翻译为:初始化一个新创建的字符串对象,以便它表示与参数相同的字符序列;换句话说,新创建的字符串是参数字符串的副本。除非需要显式的原始副本,否则使用此构造函数是不必要的,因为字符串是不可变的。

由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串

鉴于String.intern()在API上的说明和new String(a)创建字符串(创建了两个对象,如果字符串常量池存在则是一个对象)在官方API上的说明,我个人认为字符串常量池存的是字符串对象,当然在JKD7之后,常量池中存储的可能是堆对象的引用,我的另一篇关于String类的文章会讲到。(可用javap -c反编译即可得到JVM执行的字节码内容,javap -verbose 反编译查看常量池内容)

在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是key-value键值对,上面也说了,常量池是线程共享数据区,这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享(享元模式)

 

 

三个常量池的关系

通过上面的描述,我们可以总结出三个常量池的关系如下:

JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。

静态常量池用于存放编译期生成的各种字面量和符号引用,而当类加载到内存中后,jvm就会将静态常量池中的内容存放到运行时常量池中。而字符串常量池存的是引用值,其存在于运行时常量池之中。

 

其随jdk版本的变化

在JDK6及之前的版本:

静态常量池在Class文件中。

运行时常量池在Perm Gen区(也就是方法区)中。(所谓的方法区是在Java堆的一个逻辑部分,为了与Java堆区别开来,也称其为非堆(Non-Heap),那么Perm Gen(永久代)也被视为方法区的一种实现。)

字符串常量池在运行时常量池中。

 

在JDK7版本:

静态常量池在Class文件中。

运行时常量池依然在Perm Gen区(也就是方法区)中。在JDK7版本中,永久代的转移工作就已经开始了,将譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。但是运行时常量池依然还存在,只是很多内容被转移,其只存着这些被转移的引用。网上流传的一些测试运行时常量池转移的方式或者代码,其实是对字符串常量池转移的测试。

字符串常量池被分配到了Java堆的主要部分(known as the young and old generations)。也就是字符串常量池从运行时常量池分离出来了,下面是官方给出的改动摘要:

 

上文翻译:

在JDK 7中,实例化的字符串不再分配在Java堆的永久生成中,而是分配在Java堆的主要部分(称为年轻和老一代),以及另一个应用程序创建的对象。此更改将导致更多数据驻留在主Java堆中,并且永久生成中的数据更少,因此可能需要调整堆大小。由于此更改,大多数应用程序将只看到堆使用中相对较小的差异,但是加载许多类或大量使用该String.intern()方法的较大应用程序将看到更显着的差异。

 

在JDK8版本:

静态常量池在Class文件中。

JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。同时永久代被移除,以元空间代替。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。其主要用于存放一些元数据。

字符串常量池存在于Java堆中。

 

 

1条评论

啦啦啦 says: 回复

你好,看了你的文章收获很多,有两个问题想请教你一下

一、java8的运行时常量池在堆中吗,有什么证据吗,我看网上大多数文章都说是在元空间中,但是并没有说是为什么

二、符号引用 我的理解是用符号去代替真正的对象的地址,那么符号就是符号引用吗,还是说指向符号的引用叫做符号引用

发表评论

电子邮件地址不会被公开。 必填项已用*标注

昵称 *