资讯专栏INFORMATION COLUMN

java 为什么需要常量池

Yuanf / 2833人阅读

摘要:常量池探秘每个文件编译为文件后,都将产生当前类独有的常量池,我们称之为静态常量池。文件中的常量池包含两部分字面值和符号引用。方法的调用成员变量的访问最终都是通过运行时常量池来查找具体地址的。其中,表示将一个常量加载到操作数栈。

java中讲的常量池,通常指的是运行时常量池,它是方法区的一部分,一个jvm实例只有一个运行常量池,各线程间共享该运行常量池。

java内存模型中将内存分为堆和栈,其中堆为线程间共享的内存数据区域,栈为线程间私有的内存区域。堆又包括方法区以及非方法区部分,栈包括本地方法栈、虚拟机栈等,如下图所示:

为什么需要常量池

jvm 在栈帧(frame) 中进行操作数和方法的动态链接(link),为了便于链接,jvm 使用常量池来保存跟踪当前类中引用的其他类及其成员变量和成员方法。

每个栈帧(frame)都包含一个运行常量池的引用,这个引用指向当前栈帧需要执行的方法,jvm使用这个引用来进行动态链接。

在 c/c++ 中,编译器将多个编译期编译的文件链接成一个可执行文件或者dll文件,在链接阶段,符号引用被解析为实际地址。java 中这种链接是在程序运行时动态进行的。

常量池探秘

每个 java 文件编译为 class 文件后,都将产生当前类独有的常量池,我们称之为静态常量池。class 文件中的常量池包含两部分:字面值(literal)和符号引用(Symbolic Reference)。其中字面值可以理解为 java 中定义的字符串常量、final 常量等;符号引用指的是一些字符串,这些字符串表示当前类引用的外部类、方法、变量等的引用地址的抽象表示形式,在类被jvm装载并第一次使用这些符号引用时,这些符号引用将会解析为直接引用。符号常量包含:

类和接口的全限定名

字段的名称和描述符

方法的名称和描述符

jvm在进行类装载时,将class文件中常量池部分的常量加载到方法区中,此时方法区中的保存常量的逻辑区域称之为运行时常量区。

使用javap -verbose 命令可以查看class字节码的详细信息,其中包含了编译期确定的静态常量池。

public class StringTest {
    
    public static void main(String[] args){
        String s = new String("abc");
        String s2 = s.intern();
        System.out.println(s2 == s);

        String s3 = (s + s2);

        System.out.println(s3 == s3.intern());

    }
}

上述代码javap -verbose后得到(只拿出常量池部分):

major version: 52
Constant pool:
   #1 = Methodref          #13.#26        // java/lang/Object."":()V
   #2 = Class              #27            // java/lang/String
   #3 = String             #28            // abc
   #4 = Methodref          #2.#29         // java/lang/String."":(Ljava/lang/String;)V
   #5 = Methodref          #2.#30         // java/lang/String.intern:()Ljava/lang/String;
   #6 = Fieldref           #31.#32        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = Methodref          #33.#34        // java/io/PrintStream.println:(Z)V
   #8 = Class              #35            // java/lang/StringBuilder
   #9 = Methodref          #8.#26         // java/lang/StringBuilder."":()V
  #10 = Methodref          #8.#36         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #11 = Methodref          #8.#37         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #12 = Class              #38            // StringTest
  #13 = Class              #39            // java/lang/Object
  #14 = Utf8               
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               StackMapTable
  #21 = Class              #40            // "[Ljava/lang/String;"
  #22 = Class              #27            // java/lang/String
  #23 = Class              #41            // java/io/PrintStream
  #24 = Utf8               SourceFile
  #25 = Utf8               StringTest.java
  #26 = NameAndType        #14:#15        // "":()V
  #27 = Utf8               java/lang/String
  #28 = Utf8               abc
  #29 = NameAndType        #14:#42        // "":(Ljava/lang/String;)V
  #30 = NameAndType        #43:#44        // intern:()Ljava/lang/String;
  #31 = Class              #45            // java/lang/System
  #32 = NameAndType        #46:#47        // out:Ljava/io/PrintStream;
  #33 = Class              #41            // java/io/PrintStream
  #34 = NameAndType        #48:#49        // println:(Z)V
  #35 = Utf8               java/lang/StringBuilder
  #36 = NameAndType        #50:#51        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #37 = NameAndType        #52:#44        // toString:()Ljava/lang/String;
  #38 = Utf8               StringTest
  #39 = Utf8               java/lang/Object
  #40 = Utf8               [Ljava/lang/String;
  #41 = Utf8               java/io/PrintStream
  #42 = Utf8               (Ljava/lang/String;)V
  #43 = Utf8               intern
  #44 = Utf8               ()Ljava/lang/String;
  #45 = Utf8               java/lang/System
  #46 = Utf8               out
  #47 = Utf8               Ljava/io/PrintStream;
  #48 = Utf8               println
  #49 = Utf8               (Z)V
  #50 = Utf8               append
  #51 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #52 = Utf8               toString

我们可以看到,常量池共包含52个常量。#1 是一个类中方法的符号引用,它由 #13#26 两个utf8编码的字符串构成;#3 是程序中定义的 String 类型的字面值 "abc",它包含指向一个utf8编码字符串 "abc" 的索引 #28

方法的调用、成员变量的访问最终都是通过运行时常量池来查找具体地址的。

String 常量池

运行时常量池有一种 String 类型的常量,即通常我们所说的字符串字面值,所有的字符串字面值组成一个 String 常量表。String常量表并不是一成不变的,程序运行时可以动态添加字符串常量,使用String的intern()可以动态的添加String常量。但

jvm 确保两个在值上完全相等的字符串字面值(即其中包含的字符序列是相同的,使用equals()来判断)指向同一个 String 实例。

如:

String s1 = "abc";

String s2 = "abc";

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

上述代码中的字符串 s1 和 s2 将指向同一个 String 实例。实际上通过查看class文件,我们可以看到,在编译后,静态常量池中已经包含了一个 String 类型的字面值 "abc",程序运行时只是从常量池中获取这个String字面值的引用地址,并赋值给变量 s1 和变量 s2。

Constant pool:
   #1 = Methodref          #6.#19         // java/lang/Object."":()V
   #2 = String             #20            // abc
   ······
   #20 = Utf8               abc

public static void main(java.lang.String[]);
    ······
    Code:
      stack=3, locals=3, args_size=1
         0: ldc           #2                  // String abc
         2: astore_1
         3: ldc           #2                  // String abc

其中,ldc 表示将一个常量加载到操作数栈。

String 的 intern() 是一个native方法,返回的是一个String对象的标准表示。当调用该方法时,如果运行时常量池中已经存在与之相等(equal())的字符串,则直接返回常量池中的字符串引用,否则将此字符串添加到池中,并返回。

String s1 = "abc";

String s2 = new String("abc");

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

System.out.println(s1.equals(s2));      //返回 true

System.out.println(s1 == s2.intern());  //返回 true

上述代码中,虽然 s1 和 s2 中的值是相同的,但是他们指向的并不是同一个对象,但 s2 的标准化表示和s1是同一个 String 对象,都是编译期确定的常量池中的 "abc"。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/65567.html

相关文章

  • 方法区到底是个什么

    摘要:那方法区里都存着什么呢先抛结论静态变量常量类信息构造方法接口定义运行时常量池存在方法区中。动态常量池运行时常量池是方法区的一部分,是一块内存区域。文件常量池将在类加载后进入方法区的运行时常量池中存放。 一、方法区与永久代 这两个是非常容易混淆的概念,永久代的对象放在方法区中,就会想当然地认为,方法区就等同于持久代的内存区域。事实上两者是这样的关系: 《Java虚拟机规范》只是规定了有方...

    binaryTree 评论0 收藏0
  • String的内存模型,什么String被设计成不可变的

    摘要:证明返回常量池中已存在的对象,不等于新建的对象。为什么要设计成一下内容来自发现百度的中文版本基本也是此文的翻译版。总之,安全性和字符串常量池缓存是被设计成不可变的主要原因。 String是Java中最常用的类,是不可变的(Immutable), 那么String是如何实现Immutable呢,String为什么要设计成不可变呢? 前言 关于String,收集一波基础,来源标明最后,不确...

    vspiders 评论0 收藏0
  • 我终于搞清楚了和String有关的那点事儿。

    摘要:为了减少在中创建的字符串的数量,字符串类维护了一个字符串常量池。但是当执行了方法后,将指向字符串常量池中的那个字符串常量。由于和都是字符串常量池中的字面量的引用,所以。究其原因,是因为常量池要保存的是已确定的字面量值。 String,是Java中除了基本数据类型以外,最为重要的一个类型了。很多人会认为他比较简单。但是和String有关的面试题有很多,下面我随便找两道面试题,看看你能不能...

    paulli3 评论0 收藏0
  • 可能是把Java内存区域讲的最清楚的一篇文章

    摘要:另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。运行时常量池运行时常量池是方法区的一部分。 写在前面(常见面试题) 基本问题: 介绍下 Java 内存区域(运行时数据区) Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么) 对象的访问定位的两种方式(句...

    RobinQu 评论0 收藏0
  • java:String

    摘要:类是类它内部的方法也默认被修饰不能重写字符串常量池当这样声明一个字符串会检测字符串常量池中是否存在这个值的字符串如果存在就直接赋值给否则创建一个新的再赋值给当连续用同样的方式声明两个字符串并作比较结果为这个操作符比较的是什么对于基本变量比较 String类是final类,它内部的方法也默认被final修饰,不能重写. 字符串常量池 当这样声明一个字符串 String str = h...

    U2FsdGVkX1x 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<