从字符串到常量池,一文看懂String类(1)

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 从字符串到常量池,一文看懂String类(1)

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


从一道面试题开始


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

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

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


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

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


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


  • 使用new关键字创建对象
  • 使用反射创建对象(包括Class类的newInstance方法,以及Constructor类的newInstance方法)
  • 使用clone复制一个对象
  • 反序列化得到一个对象

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


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


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


常量池


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


  • class文件中的常量池
  • 运行时常量池
  • 字符串常量池


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

  1. 这个常量池在哪里?
  2. 这个常量池用来干什么呢?
  3. 这三者有什么关系?


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


class文件中的常量池


位置在哪?



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


用来干什么的?


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

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

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

据,没有空隙存在。


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


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

微信图片_20221113182931.png

对本文而言,我们只关注其中的常量池部分,常量池可以理解为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

如果要看懂这两行代码,我们需要对常量池中String类型常量的结构有一定了解,其结构如下:

image.png

对应到我们上面的字节码中,tag=String,index=#14,所以我们可以知道,#2是一个字面量为#14的字符串类型常量。而#14对应的字面量信息(一个Utf8类型的常量)就是dmz。


常量池作为资源仓库,最大的用处在于被class文件中的其它结构所引用,这个时候我们再将注意力放到main方法上来,对应的就是这三条指令

0: ldc           #2                  // String dmz
2: astore_1
3: return

ldc:这个指令的作用是将对应的常量的引用压入操作数栈,在执行ldc指令时会触发对它的符号引用进行解析,在上面例子中对应的符号引用就是#2,也就是常量池中的第二个元素(这里就能看出方法表中就引用了常量池中的资源)


astore_1:将操作数栈底元素弹出,存储到局部变量表中的1号元素

return:方法返回值为void,标志方法执行完成,将方法对应栈帧从栈中弹出


下面我用画图的方式来画出整个流程,主要分为四步


  1. 解析ldc指令的符号引用(#2)
  2. 将#2对应的常量的引用压入到操作数栈顶
  3. 将操作数栈的元素弹出并存储到局部变量表中
  4. 执行return指令,方法执行结束,弹出栈区该方法对应的栈帧


第一步:

微信图片_20221113183350.png

在解析#2这个符号引用时,会先到字符串常量池中查找是否存在对应字符串实例的引用,如果有的话,那么直接返回这个字符串实例的引用,如果没有的话,会创建一个字符串实例,那么将其添加到字符串常量池中(实际上是将其引用放入到一个哈希表中),之后再返回这个字符串实例对象的引用。


到这里也能回答我们之前提出的那个问题了,一个对象是new出来的,另外一个是在解析常量池的时候JVM自动创建的


第二步:

2020040700014127.png

将第一步得到的引用压入到操作数栈,此时这个字符串实例同时被操作数栈以及字符串常量池引用。

第三步:

2020040700014127.png

操作数栈中的引用弹出,并赋值给局部变量表中的1号位置元素,到这一步其实执行完了String name = "dmz"这行代码。此时局部变量表中储存着一个指向堆中字符串实例的引用,并且这个字符串实例同时也被字符串常量池引用。


第四步:

这一步我就不画图了,就是方法执行完成,栈帧弹出,非常简单。

在上文中,我多次提到了字符串常量池,它到底是个什么东西呢?我们还是分为两部分讨论

1.位置在哪?

2.用来干什么的?


相关文章
|
16天前
|
索引 Python
String(字符串)
String(字符串)。
25 3
|
2月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
58 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
|
2月前
|
NoSQL Redis
Redis 字符串(String)
10月更文挑战第16天
43 4
|
2月前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
34 2
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
22 1
|
2月前
|
数据可视化 Java
让星星月亮告诉你,通过反射创建类的实例对象,并通过Unsafe theUnsafe来修改实例对象的私有的String类型的成员属性的值
本文介绍了如何使用 Unsafe 类通过反射机制修改对象的私有属性值。主要包括: 1. 获取 Unsafe 的 theUnsafe 属性:通过反射获取 Unsafe类的私有静态属性theUnsafe,并放开其访问权限,以便后续操作 2. 利用反射创建 User 类的实例对象:通过反射创建User类的实例对象,并定义预期值 3. 利用反射获取实例对象的name属性并修改:通过反射获取 User类实例对象的私有属性name,使用 Unsafe`的compareAndSwapObject方法直接在内存地址上修改属性值 核心代码展示了详细的步骤和逻辑,确保了对私有属性的修改不受 JVM 访问权限的限制
56 4
|
2月前
|
canal 安全 索引
(StringBuffer和StringBuilder)以及回文串,字符串经典习题
(StringBuffer和StringBuilder)以及回文串,字符串经典习题
37 5
|
2月前
|
存储 安全 Java
【一步一步了解Java系列】:认识String类
【一步一步了解Java系列】:认识String类
25 2
|
2月前
|
C语言 C++
C++番外篇——string类的实现
C++番外篇——string类的实现
20 0
|
2月前
|
C++ 容器
C++入门7——string类的使用-2
C++入门7——string类的使用-2
21 0