从字符串到常量池,一文看懂String类设计

从一道面试题开始

看到这个标题,你肯定以为我又要讲这道面试题了

// 这行代码创建了几个对象? String s3 = new String("1");

是的,没错,我确实要从这里开始

image-20200615221408500

这道题就算你没做过也肯定看到,总所周知,它创建了两个对象,一个位于堆上,一个位于常量池中。

这个答案粗看起来是没有任何问题的,但是仔细思考确经不起推敲。

如果你觉得我说的不对的话,那么可以思考下面这两个问题

你说它创建了两个对象,那么这两个对象分别是怎样创建的呢?我们回顾下Java创建对象的方式,一共就这么几种

使用new关键字创建对象

使用反射创建对象(包括Class类的newInstance方法,以及Constructor类的newInstance方法)

使用clone复制一个对象

反序列化得到一个对象

你说它创建了两个对象,那你告诉我除了new出来那个对象外,另外一个对象怎么创建出来的?

堆跟常量池到底什么关系?不是说在JDK1.7之后(含1.7版本)常量池已经移到了堆中了吗?如果说常量池本身就位于堆中的话,那么这种一个对象在堆中,一个对象在常量池的说法还准确吗?

如果你也产生过这些疑问的话,那么请耐心看完这篇文章!要解释上面的问题首先我们得对常量池有个准确的认知。

image-20200615221502268

常量池

通常来说,我们提到的常量池分为三种

class文件中的常量池

运行时常量池

字符串常量池

对于这三种常量池,我们需要搞懂下面几个问题?

这个常量池在哪里?

这个常量池用来干什么呢?

这三者有什么关系?

接下来,我们带着这些问题往下看

class文件中的常量池 位置在哪?

顾名思义,class文件中的常量池当然是位于class文件中,而class文件又是位于磁盘上

用来干什么的?

在学习class文件中的常量池前,我们首选需要对class文件的结构有一定了解

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文

件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数

据,没有空隙存在。

​ ------------《深入理解Java虚拟机》

整个class文件的组成可以用下图来表示

image-20200615225016604

对本文而言,我们只关注其中的常量池部分,常量池可以理解为class文件中资源仓库,它是class文件结构中与其它项目关联最多的数据类型,主要用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)
字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等
符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:

类和接口的全限定名

字段的名称和描述符

方法的名称和描述符

现在我们知道了class文件中常量池的作用:存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。很多时候知道了一个东西的概念并不能说你会了,对于程序员而言,如果你说你已经会了,那么最好的证明是你能够通过代码将其描述出来,所以,接下来,我想以一种直观的方式让大家感受到常量池的存在。通过分析一段简单代码的字节码,让大家能更好感知常量池的作用。

​ talk is cheap ,show me code

我们以下面这段代码为例,通过javap来查看class文件中的具体内容,代码如下:

/** * @author 程序员DMZ * @Date Create in 22:59 2020/6/15 * @公众号 微信搜索:程序员DMZ */ public class Main { public static void main(String[] args) { String name = "dmz"; } }

进入Main.java文件所在目录,执行命令:javac Main.java ,那么此时会在当前目录下生成对应的Main.class文件。再执行命令:javap -v -c Main.class,此时会得到如下的解析后的字节码信息

public class com.dmz.jvm.Main minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER // 这里就是常量池了 Constant pool: #1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = String #21 // dmz #3 = Class #22 // com/dmz/jvm/Main #4 = Class #23 // java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Utf8 LineNumberTable #9 = Utf8 LocalVariableTable #10 = Utf8 this #11 = Utf8 Lcom/dmz/jvm/Main; #12 = Utf8 main #13 = Utf8 ([Ljava/lang/String;)V #14 = Utf8 args #15 = Utf8 [Ljava/lang/String; #16 = Utf8 name #17 = Utf8 Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 Main.java #20 = NameAndType #5:#6 // "<init>":()V #21 = Utf8 dmz #22 = Utf8 com/dmz/jvm/Main #23 = Utf8 java/lang/Object // 下面是方法表 { public com.dmz.jvm.Main(); descriptor: ()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 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/dmz/jvm/Main; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=2, args_size=1 // 可以看到方法表中的指令引用了常量池中的常量,这也是为什么说常量池是资源仓库的原因 // 因为它会被class文件中的其它结构引用 0: ldc #2 // String dmz 2: astore_1 3: return LineNumberTable: line 9: 0 line 10: 3 LocalVariableTable: Start Length Slot Name Signature 0 4 0 args [Ljava/lang/String; 3 1 1 name Ljava/lang/String; } SourceFile: "Main.java"

在上面的字节码中,我们暂且关注常量池中的内容即可。主要看这两行

#2 = String #14 // dmz #14 = Utf8 dmz

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zypdgs.html