JVM常量池的深入讲解

提示:这里咱们要说的常量池,常量池就是咱们面试中所说的常量池,谈谈你对常量池的认识?面试官一问咱们就懵逼了,你要记得你脑子中有一张图!!! 剩下的就好办了

提示:请各位大佬批评指正!!

前言

提示:学习的时候会有点头疼哦

一、Class常量池与运行时常量池

Class常量池可以理解为是Class文件中的资源仓库。 Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是 常量池(constant pool table) ,用于存放编译期生成的各种 字面量(Literal)和符号引用(Symbolic References)

还是回到前面说的class文件的16进制大体结构如下图:

对应的含义如下,细节可以查下oracle官方文档

当然我们一般不会去人工解析这种16进制的字节码文件,我们一般可以通过javap命令生成更可读的JVM字节码指令文件:

javap -v Test.class

public class com.qjc.construction.Test
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#27         // java/lang/Object."<init>":()V
   #2 = Class              #28            // com/qjc/construction/Test
   #3 = Methodref          #2.#27         // com/qjc/construction/Test."<init>":()V
   #4 = Methodref          #2.#29         // com/qjc/construction/Test.test:()I
   #5 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = Methodref          #32.#33        // java/io/PrintStream.println:(Ljava/lang/Object;)V
   #7 = Class              #34            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/qjc/construction/Test;
  #15 = Utf8               test
  #16 = Utf8               ()I
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               c
  #21 = Utf8               main
  #22 = Utf8               ([Ljava/lang/String;)V
  #23 = Utf8               args
  #24 = Utf8               [Ljava/lang/String;
  #25 = Utf8               SourceFile
  #26 = Utf8               Test.java
  #27 = NameAndType        #8:#9          // "<init>":()V
  #28 = Utf8               com/qjc/construction/Test
  #29 = NameAndType        #15:#16        // test:()I
  #30 = Class              #35            // java/lang/System
  #31 = NameAndType        #36:#37        // out:Ljava/io/PrintStream;
  #32 = Class              #38            // java/io/PrintStream
  #33 = NameAndType        #39:#40        // println:(Ljava/lang/Object;)V
  #34 = Utf8               java/lang/Object
  #35 = Utf8               java/lang/System
  #36 = Utf8               out
  #37 = Utf8               Ljava/io/PrintStream;
  #38 = Utf8               java/io/PrintStream
  #39 = Utf8               println
  #40 = Utf8               (Ljava/lang/Object;)V

Constant pool: 就是class常量池信息,常量池中主要存放两大类常量:字面量和符号引用。

字面量

字面量就是指由字母、数字等构成的字符串或者数值常量

字面量只可以右值出现,所谓右值是指等号右边的值,如:int a=1 这里的a为左值,1为右值。在这个例子中1就是字面量。

int a = 1;
int b = 2;
int c = "abcdefg";
int d = "abcdefg";

符号引用

符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

上面的a,b就是字段名称,就是一种符号引用,还有Test类常量池里的 Lcom/qjc/construction/Test; 是类的全限定名,main和上面的a,b就是字段名称,就是一种符号引用,还有Test类常量池里的 Lcom/qjc/construction/Test; 是类的全限定名,main和test是方法名称,()是一种UTF8格式的描述符,这些都是符号引用。

这些常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用在程序加载或运行时会被转变为被加载到内存区域的代码的直接引用,也就是我们说的动态链接了。例如,test()这个符号引用在运行时就会被转变为test()方法具体代码在内存中的地址,主要通过对象头里的类型指针去转换直接引用。是方法名称,()是一种UTF8格式的描述符,这些都是符号引用。

这些常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用在程序加载或运行时会被转变为被加载到内存区域的代码的直接引用,也就是我们说的动态链接了。例如,test()这个符号引用在运行时就会被转变为test()方法具体代码在内存中的地址,主要通过对象头里的类型指针去转换直接引用。

二、字符串常量池

字符串常量池的设计思想

1.字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能

2.JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化

  • 为字符串开辟一个字符串常量池,类似于缓存区
  • 创建字符串常量时,首先查询字符串常量池是否存在该字符串
  • 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
  • 三种字符串操作(Jdk1.7 及以上版本)
  • 直接赋值字符串

三种字符串操作(Jdk1.7 及以上版本)直接赋值字符串

String s = "qjc";  // s指向常量池中的引用

这种方式创建的字符串对象,只会在常量池中。

因为有"qjc"这个字面量,创建对象s的时候,JVM会先去常量池中通过 equals(key) 方法,判断是否有相同的对象。如果有,则直接返回该对象在常量池中的引用;如果没有,则会在常量池中创建一个新对象,再返回引用。

new String();

String s1 = new String("qjc");  // s1指向内存中的对象引用

这种方式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。

步骤大致如下:

因为有"qjc"这个字面量,所以会先检查字符串常量池中是否存在字符串"qjc"

不存在,先在字符串常量池里创建一个字符串对象;再去内存中创建一个字符串对象"qjc";

存在的话,就直接去堆内存中创建一个字符串对象"qjc";

最后,将内存中的引用返回。

intern方法

String s1 = new String("qjc");
String s2 = s1.intern();

System.out.println(s1 == s2);  //false

String中的intern方法是一个 native 的方法,当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将intern返回的引用指向当前字符串 s1(jdk1.6版本需要将 s1 复制到字符串常量池里)。

字符串常量池位置

Jdk1.6及之前: 有永久代, 运行时常量池在永久代,运行时常量池包含字符串常量池

Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里

Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里

用一个程序证明下字符串常量池在哪里:

/**
 * @author qijianchun
 * @title: TestPool
 * @projectName
 * @description: TODO JDK8:-Xms6M -Xmx6M -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
 * @description: TODO JDK6:-Xms6M -Xmx6M -XX:PermSize=6M -XX:MaxPermSize=6M
 * @date 2021/4/1313:02
 */
public class TestPool {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000000; i++) {
            String str = String.valueOf(i).intern();
            list.add(str);
        }
    }
}

结果:

运行结果:
jdk7及以上:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
jdk6:Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

字符串常量池设计原理

字符串常量池底层是hotspot的C++实现的,底层类似一个 HashTable, 保存的本质上是字符串对象的引用。

看一道比较常见的面试题,下面的代码创建了多少个 String 对象?

String s1 = new String("he") + new String("llo");
String s2 = s1.intern();

System.out.println(s1 == s2);
// 在 JDK 1.6 下输出是 false,创建了 6 个对象
// 在 JDK 1.7 及以上的版本输出是 true,创建了 5 个对象
// 当然我们这里没有考虑GC,但这些对象确实存在或存在过

为什么输出会有这些变化呢?主要还是字符串池从永久代中脱离、移入堆区的原因, intern() 方法也相应发生了变化:

1、在 JDK 1.6 中,调用 intern() 首先会在字符串池中寻找 equal() 相等的字符串,假如字符串存在就返回该字符串在字符串池中的引用;假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将 StringTable 的一个表项指向这个新创建的实例。

2、在 JDK 1.7 (及以上版本)中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。字符串存在时和 JDK 1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。

由上看出也不难理解为什么 JDK 1.6 字符串池溢出会抛出 OutOfMemoryError: PermGen space ,而在 JDK 1.7 及以上版本抛出 OutOfMemoryError: Java heap space 。

String常量池问题的几个例子

示例1:

String s0="qjc";
String s1="qjc";
String s2="qj" + "c";
System.out.println( s0==s1 ); //true
System.out.println( s0==s2 ); //true

分析:因为例子中的 s0和s1中的”qjc”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”qj”和”c”也都是字符串常量,当一个字 符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被优化为一个字符串常量"qjc",所以s2也是常量池中” qjc”的一个引用。所以我们得出s0s1s2

示例2:

String s0="qjc";
String s1=new String("qjc");
String s2="qj" + new String("c");
System.out.println( s0==s1 );// false
System.out.println( s0==s2 );// false
System.out.println( s1==s2 );// false

分析:用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。

s0还是常量池 中"qjc”的引用,s1因为无法在编译期确定,所以是运行时创建的新对象”qjc”的引用,s2因为有后半部分 new String(”c”)所以也无法在编译期确定,所以也是一个新创建对象”qjc”的引用;明白了这些也就知道为何得出此结果了。

示例3:

String a = "a1";
String b = "a" + 1;
System.out.println(a == b); // true

String a1 = "atrue";
String b1 = "a" + "true";
System.out.println(a1 == b1); // true

String a2 = "a3.4";
String b2 = "a" + 3.4;
System.out.println(a2 == b2); // true

分析:JVM对于字符串常量的"+“号连接,将在程序编译期,JVM就将常量字符串的”+“连接优化为连接后的值,拿"a” + 1来说,经编译器优化后在class中就已经是a1。在编译期其字符串常量的值就确定下来,故上面程序最终的结果都为true。在编译时就确定了,然后放入常量池

示例4:

String a = "ab";
String bb = "b";
String b = "a" + bb;
System.out.println(a == b); // false

分析:JVM对于字符串引用,由于在字符串的"+“连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a” + bb无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b。所以上面程序的结果也就为false。

示例5:

String a = "ab";
final String bb = "b";
String b = "a" + bb;

System.out.println(a == b); // true

分析:和示例4中唯一不同的是bb字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的"a" + bb和"a" + "b"效果是一样的。故上面程序的结果为true。

示例6:

String a = "ab";
final String bb = getBB();
String b = "a" + bb;

System.out.println(a == b); // false

private static String getBB()
{
    return "b";
 }

分析:JVM对于字符串引用bb,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和"a"来动态连接并分配地址为b,故上面 程序的结果为false。

关于String是不可变的

通过上面例子可以得出得知:

String  s  =  "a" + "b" + "c";  //就等价于String s = "abc";
String  a  =  "a";
String  b  =  "b";
String  c  =  "c";
String  s1  =   a  +  b  +  c;

s1 这个就不一样了,可以通过观察其JVM指令码发现s1的"+"操作会变成如下操作:

StringBuilder temp = new StringBuilder();
temp.append(a).append(b).append(c);
String s = temp.toString();

因为调用toString方法就会 newString

这可就不一样了 new String

再看一个例子:

//字符串常量池:"计算机"和"技术"     堆内存:str1引用的对象"计算机技术"
//堆内存中还有个StringBuilder的对象,但是会被gc回收,StringBuilder的toString方法会new String(),这个String才是真正返回的对象引用
String str2 = new StringBuilder("计算机").append("技术").toString();   //没有出现"计算机技术"字面量,所以不会在常量池里生成"计算机技术"对象
System.out.println(str2 == str2.intern());  //true
//"计算机技术" 在池中没有,但是在heap中存在,则intern时,会直接返回该heap中的引用
//字符串常量池:"ja"和"va"     堆内存:str1引用的对象"java"
//堆内存中还有个StringBuilder的对象,但是会被gc回收,StringBuilder的toString方法会new String(),这个String才是真正返回的对象引用
String str1 = new StringBuilder("ja").append("va").toString();    //没有出现"java"字面量,所以不会在常量池里生成"java"对象
System.out.println(str1 == str1.intern());  //false
//java是关键字,在JVM初始化的相关类里肯定早就放进字符串常量池了
String s1=new String("test");
System.out.println(s1==s1.intern());   //false
//"test"作为字面量,放入了池中,而new时s1指向的是heap中新生成的string对象,s1.intern()指向的是"test"字面量之前在池中生成的字符串对象

String s2=new StringBuilder("abc").toString();
System.out.println(s2==s2.intern());  //false
//同上

八种基本类型的包装类和对象池

java中基本类型的包装类的大部分都实现了常量池技术(严格来说应该叫对象池,在堆上),这些类是**Byte,Short,Integer,Long,Character,Boolean,**另外两种浮点数类型的包装类则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。因为一般这种比较小的数用到的概率相对较大。

        //5种整形的包装类Byte,Short,Integer,Long,Character的对象,
        //在值小于127时可以使用对象池
        Integer i1 = 127;  //这种调用底层实际是执行的Integer.valueOf(127),里面用到了IntegerCache对象池
        Integer i2 = 127;
        System.out.println(i1 == i2);//输出true

       //值大于127时,不会从对象池中取对象
        Integer i3 = 128;
        Integer i4 = 128;
        System.out.println(i3 == i4);//输出false
 	   //用new关键词新生成对象不会使用对象池
        Integer i5 = new Integer(127);
        Integer i6 = new Integer(127);
        System.out.println(i5 == i6);//输出false


Boolean

        //Boolean类也实现了对象池技术
        Boolean bool1 = true;
        Boolean bool2 = true;
        System.out.println(bool1 == bool2);//输出true

Double

       //浮点类型的包装类没有实现对象池技术
        Double d1 = 1.0;
        Double d2 = 1.0;
        System.out.println(d1 == d2);//输出false

总结

到此这篇关于JVM常量池的文章就介绍到这了,更多相关JVM常量池内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入解析JVM之内存结构及字符串常量池(推荐)

    前言 Java作为一种平台无关性的语言,其主要依靠于Java虚拟机--JVM,我们写好的代码会被编译成class文件,再由JVM进行加载.解析.执行,而JVM有统一的规范,所以我们不需要像C++那样需要程序员自己关注平台,大大方便了我们的开发.另外,能够运行在JVM上的并只有Java,只要能够编译生成合乎规范的class文件的语言都是可以跑在JVM上的.而作为一名Java开发,JVM是我们必须要学习了解的基础,也是通向高级及更高层次的必修课:但JVM的体系非常庞大,且术语非常多,所以初学者对此非

  • JVM常量池的深入讲解

    提示:这里咱们要说的常量池,常量池就是咱们面试中所说的常量池,谈谈你对常量池的认识?面试官一问咱们就懵逼了,你要记得你脑子中有一张图!!! 剩下的就好办了 提示:请各位大佬批评指正!! 前言 提示:学习的时候会有点头疼哦 一.Class常量池与运行时常量池 Class常量池可以理解为是Class文件中的资源仓库. Class文件中除了包含类的版本.字段.方法.接口等描述信息外,还有一项信息就是 常量池(constant pool table) ,用于存放编译期生成的各种 字面量(Literal)

  • 详解JAVA 常量池

    前言 对常量池的理解之前,需要熟悉的是一些术语: 字面量 在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation). 几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数.浮点数以及字符串:而有很多也对布尔类型和字符类型的值也支持字面量表示: 还有一些甚至对枚举类型的元素以及像数组.记录和对象等复合类型的值也支持字面量表示法.C语言关于复合字面量的介绍可参考: [1]  . 百度也给了一个例子: 这个object-c 的例子,容易理解. #incl

  • Java 常量池详解之字符串常量池实现代码

    目录 1.字符串常量池(String Constant Pool) 1.1:字符串常量池在Java内存区域的哪个位置? 1.2:字符串常量池是什么? 1.3 字符串常量池生成的时机? 如何将String对象放入到常量池 String 对象代码案例解析 new string(“abc”)创建了几个对象 解析public native String intern() 方法 Integer 对象代码案例解析 为啥Integer i1 =10 跟Integer.valueOf(10) 是相等的? 为啥I

  • 详解JVM之运行时常量池

    class文件中的常量池 之前我们在讲class文件的结构时,提到了每个class文件都有一个常量池,常量池中存了些什么东西呢? 字符串常量,类和接口名字,字段名,和其他一些在class中引用的常量. 运行时常量池 但是只有class文件中的常量池肯定是不够的,因为我们需要在JVM中运行起来. 这时候就需要一个运行时常量池,为JVM的运行服务. 运行时常量池和class文件的常量池是一一对应的,它就是class文件的常量池来构建的. 运行时常量池中有两种类型,分别是symbolic refere

  • java学习之JVM运行时常量池理解

    运行时常量池 运行时常量池是方法区的一部分.Class文件中除了有类的版本.字段.方法.接口等描述信息外,还有一项信息时常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放. 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是S

  • Java常量池知识点总结

    java常量池是一个经久不衰的话题,也是面试官的最爱,题目花样百出,这次好好总结一下. 理论 先拙劣的表达一下jvm虚拟内存分布: 程序计数器是jvm执行程序的流水线,存放一些跳转指令,这个太高深,不懂. 本地方法栈是jvm调用操作系统方法所使用的栈. 虚拟机栈是jvm执行java代码所使用的栈. 方法区存放了一些常量.静态变量.类信息等,可以理解成class文件在内存中的存放位置. 虚拟机堆是jvm执行java代码所使用的堆. Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池.

  • Java class文件格式之常量池_动力节点Java学院整理

    常量池中各数据项类型详解 常量池中的数据项是通过索引来引用的, 常量池中的各个数据项之间也会相互引用.在这11中常量池数据项类型中, 有两种比较基础, 之所以说它们基础, 是因为这两种类型的数据项会被其他类型的数据项引用. 这两种数据类型就是CONSTANT_Utf8 和 CONSTANT_NameAndType , 其中CONSTANT_NameAndType类型的数据项(CONSTANT_NameAndType_info)也会引用CONSTANT_Utf8类型的数据项(CONSTANT_Ut

  • java String类常量池分析及"equals"和"==”区别详细介绍

    java "equals"和"=="异同 首先简单说一下"equal"和"==" ==操作对于基本数据类型比较的是两个变量的值是否相等, 对于引用型变量表示的是两个变量在堆中存储的地址是否相同, 即栈中的内容是否相同 equals操作表示的两个变量是否是对同一个对象的引用, 即堆中的内容是否相同. 综上,==比较的是2个对象的地址,而equals比较的是2个对象的内容. 再简单介绍一下String类 String类 又称作不可

  • Java 中的字符串常量池详解

    Java中的字符串常量池 Java中字符串对象创建有两种形式,一种为字面量形式,如String str = "droid";,另一种就是使用new这种标准的构造对象的方法,如String str = new String("droid");,这两种方式我们在代码编写时都经常使用,尤其是字面量的方式.然而这两种实现其实存在着一些性能和内存占用的差别.这一切都是源于JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池.

随机推荐