String 常量池的使用

本文探讨了Java中String常量池的工作原理,包括哪些情况下会将字符串放入常量池,以及通过具体示例展示了编译期间如何确定字符串是否进入常量池。

string constants pool 使用

我们知道 jvm 对于String 有一个常量池的分配. 哪是在什么情况下才会把string 存储到常量池中?

在 jdk1.7 以后已经把常量池从perm gen 迁移到heap(young gen and/or old gen) 里面了。
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/enhancements-7.html RFE: 6962931

什么情况下放入常量池?

1.在编译期已确定的字符串
2.显示调用String.intern()方法

第二种情况很明确,那第一种情况什么是编译期已确定呢?

  • 来看第一个demo:
public static void main(String[] args){
        String s = "sunla";
    }

这个demo里面 我们通过javap 查看下 生成的class文件

  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
         0: ldc           #16                 // String sunla
         2: astore_1
         3: return
      LineNumberTable:
        line 6: 0
        line 7: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
            3       1     1     s   Ljava/lang/String;
}

可以看到 一个字节码命令 ldc 代表的含义是 把常量池中的项压入栈中.
这就可以知道 在编译时 已经知道”sunla” 字符串 并放入到常量池中.

  • 在看个demo
    public static void main(String[] args){
        String s = new String("sunla");
    }

通过javap 查看生成的class 文件

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #16                 // class java/lang/String
         3: dup
         4: ldc           #18                 // String sunla
         6: invokespecial #20                 // Method java/lang/String."<init>":(Ljava/lang/String;)V
         9: astore_1
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  args   [Ljava/lang/String;
           10       1     1     s   Ljava/lang/String;
}
SourceFile: "StringTest2.java"

可以看到 还是生成了一个 ldc 字节码命令 把”sunla”字符串放入到字符串常量池中,并同时 调用了String 对象的实例构造方法.
那是不是说 通过 new String 产生的都会放入常量池呢?
我们在看过demo

    public static void main(String[] args){
        /** 通过UUID 生成随机字符串 */
        String s = new String(UUID.randomUUID().toString());
    }

我们在来看下class 信息

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #16                 // class java/lang/String
         3: dup
         4: invokestatic  #18                 // Method java/util/UUID.randomUUID:()Ljava/util/UUID;
         7: invokevirtual #24                 // Method java/util/UUID.toString:()Ljava/lang/String;
        10: invokespecial #28                 // Method java/lang/String."<init>":(Ljava/lang/String;)V
        13: astore_1
        14: return
      LineNumberTable:
        line 9: 0
        line 10: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  args   [Ljava/lang/String;
           14       1     1     s   Ljava/lang/String;
}
SourceFile: "StringTest2.java"

可以看到 这个例子中并没有生成 ldc 命令,就说明 没有把uuid生成的字符串放入到常量池中.
这是为什么呢?
因为在代码中是通过调用
invokestatic #18 // Method java/util/UUID.randomUUID:()
静态方法实现的,在编译期并不可知,只有在运行期才可知道.

  • 在看另一个demo
    public static void main(String[] args){
        String c = "a" + "b";
    }
  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
         0: ldc           #16                 // String ab
         2: astore_1
         3: return
      LineNumberTable:
        line 8: 0
        line 9: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
            3       1     1     c   Ljava/lang/String;
}
SourceFile: "StringTest2.java"

通过查看class 可以看到在编译成class文件时 jvm使用了合并优化功能,
把字符串”ab” 放入到字符串常量池中.

  • 在看另外一个demo
    public static void main(String[] args){
        /** 跟上面实现同样功能 */
        String a = "a";
        String b = "b";
        String c = a + b;
    }
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=4, args_size=1
         0: ldc           #16                 // String a
         2: astore_1
         3: ldc           #18                 // String b
         5: astore_2
         6: new           #20                 // class java/lang/StringBuilder
         9: dup
        10: aload_1
        11: invokestatic  #22                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
        14: invokespecial #28                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        17: aload_2
        18: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: invokevirtual #35                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        24: astore_3
        25: return
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 6
        line 11: 25
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      26     0  args   [Ljava/lang/String;
            3      23     1     a   Ljava/lang/String;
            6      20     2     b   Ljava/lang/String;
           25       1     3     c   Ljava/lang/String;
}
SourceFile: "StringTest2.java"

从这里可以很明确的看到 只生成了2个ldc 字节码命令 就是把 “a” 和 “b” 存入到常量池中了.
那为什么没有”ab” 的字符串常量呢?
因为在编译期 c引用 指向的String是不可知的,在执行过程中 a 和 b 的引用很有可能变化.
所以并不会生成”ab” 字符串常量

总结 - 可放入常量池的情况

  • 编译期可确定的字符串

    1. String a = “a”;
    2. String a = new String(“a”);
    3. String a = “a” + “b”;
  • 显示调用intern

    1. String s = new String(UUID.randomUUID().toString()).intern();

常量池的实现

有兴趣了解底层实现的同学可以去看看
hotspot源码下载路径 http://openjdk.java.net/groups/hotspot/
代码位置 : hotspot/src/share/vm/classfile/symbolTable.hpp

class StringTable : public Hashtable<oop> {
  friend class VMStructs;

private:
  // The string table
  static StringTable* _the_table;

  static oop intern(Handle string_or_null, jchar* chars, int length, TRAPS);
  oop basic_add(int index, Handle string_or_null, jchar* name, int len,
                unsigned int hashValue, TRAPS);

  oop lookup(int index, jchar* chars, int length, unsigned int hashValue);

  StringTable() : Hashtable<oop>((int)StringTableSize,
                                 sizeof (HashtableEntry<oop>)) {}

  StringTable(HashtableBucket* t, int number_of_entries)
    : Hashtable<oop>((int)StringTableSize, sizeof (HashtableEntry<oop>), t,
                     number_of_entries) {}

public:
  // The string table
  static StringTable* the_table() { return _the_table; }

  static void create_table() {
    assert(_the_table == NULL, "One string table allowed.");
    _the_table = new StringTable();
  }

  static void create_table(HashtableBucket* t, int length,
                           int number_of_entries) {
    assert(_the_table == NULL, "One string table allowed.");
    assert((size_t)length == StringTableSize * sizeof(HashtableBucket),
           "bad shared string size.");
    _the_table = new StringTable(t, number_of_entries);
  }

  // GC support
  //   Delete pointers to otherwise-unreachable objects.
  static void unlink(BoolObjectClosure* cl);

  // Invoke "f->do_oop" on the locations of all oops in the table.
  static void oops_do(OopClosure* f);

  // Probing
  static oop lookup(Symbol* symbol);

  // Interning
  static oop intern(Symbol* symbol, TRAPS);
  static oop intern(oop string, TRAPS);
  static oop intern(const char *utf8_string, TRAPS);

  // Debugging
  static void verify();

  // Sharing
  static void copy_buckets(char** top, char*end) {
    the_table()->Hashtable<oop>::copy_buckets(top, end);
  }
  static void copy_table(char** top, char*end) {
    the_table()->Hashtable<oop>::copy_table(top, end);
  }
  static void reverse() {
    the_table()->Hashtable<oop>::reverse();
  }
};
### ### Java String 常量池详解 在 Java 中,String 是一个特殊的类,它不仅被广泛使用,还通过常量池机制进行了深度优化。为了提升性能和减少内存开销,JVM 引入了 **字符串常量池StringTable)** 来管理字符串字面量。 #### 字符串常量池的定义与作用 字符串常量池是一种特殊的内存区域,用于存储字符串字面量(literal)和通过 `intern()` 方法添加的字符串对象。当使用双引号定义字符串时,JVM 会优先检查常量池中是否已存在该字符串,若存在则直接返回其引用,否则新建一个并放入池中[^2]。 例如: ```java String a = "hello"; String b = "hello"; ``` 变量 `a` 和 `b` 指向的是同一个字符串对象,因为它们都指向常量池中的同一个实例。 #### 字符串常量池的运行机制 在程序运行期间,字符串常量池会动态管理字符串实例。当使用 `new String("hello")` 创建字符串时,JVM 会先检查常量池中是否存在 `"hello"`,若不存在则将其加入常量池。随后,会在堆中创建一个新的 `String` 对象,并指向常量池中的字符串[^4]。 ```java String c = new String("hello"); ``` 上述代码中,`c` 是一个堆中的新对象,但它内部的字符数据仍然指向常量池中的 `"hello"`。 #### intern() 方法与字符串池的扩展 Java 提供了 `String.intern()` 方法用于手动将字符串加入常量池。当调用该方法时,JVM 会查找常量池中是否存在相同 Unicode 编码的字符串,如果存在则返回其引用,否则将当前字符串加入常量池并返回其引用[^3]。 ```java String d = new String("world").intern(); String e = "world"; ``` 此时,`d` 和 `e` 指向的是同一个常量池中的字符串对象。 #### 常量池的分类与关系 字符串常量池JVM 中多种常量池机制的一部分。在编译时,类的常量信息(如字符串字面量)会被存储在 **静态常量池** 中。类加载后,这些信息会被加载到 **运行时常量池** 中,而字符串常量池则是运行时常量池的一部分,专门用于管理字符串字面量和通过 `intern()` 方法加入的字符串[^1]。 例如,在编译后的 `.class` 文件中,以下代码: ```java String a = "a"; String b = "b"; String ab = "ab"; ``` 会被编译为常量池中的符号信息,在类加载后成为运行时常量池的一部分,并在运行时由 JVM 管理其在字符串常量池中的存在[^5]。 #### 总结 字符串常量池通过复用字符串对象,减少了内存的使用并提升了程序性能。它与静态常量池、运行时常量池共同构成了 Java 的常量管理机制。开发者可以通过 `intern()` 方法显式控制字符串在常量池中的存在状态,从而优化内存使用。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值