1. 字符串生成过程
我们都知道 String s = "hello java"; 会将 "hello java" 放入字符串常量池, 但是从 jvm 的角度来看字符串和三个常量池有关, class 常量池, 运行时常量池, 全局字符串常量池(也就是常说的字符串常量池)
第一个是 class 的常量池, 看一下下面这个代码
- public class StringTest {
- public void test1() {
- String s = "hello java";
- }
- }
如果用 javap -v StringTest.class 来查看他的字节码文件, 代码如下
- Constant pool:
- #1 = Methodref #4.#17 // java/lang/Object."<init>":()V
- #2 = String #18 // hello java
- ...
- #18 = Utf8 hello java
- #2 表示有一个字符串的索引指向 #18, 一个 utf8 编码的字符串字面量, 这个 #18 只代表由 utf 编码的数据, 不是 java 对象,#2 是 java 对象, 但是他现在还没有初始化,#18 是在文件编译后就生成的
utf8 字面量字符串, 他在项目启动加载类时就进入了运行时常量池.
那么就有一个问题,#2 什么时候初始化, 以及什么时候进入全局字符串常量池 (也就是平常说的字符串常量池) 呢? 我们继续看字节码
源码:
- public void test1() {
- String s = "hello java";
- }
字节码:
- public void test1();
- 0: ldc #2 // String hello java
- 2: astore_1
- 3: return
为了方便看, 去掉部分代码
我们看字节码第一行, 很明显的看到他调用了 #2,ldc 的作用是将常量池中的数据加载到操作数栈中(简单来说就是进行数据操作的地方), 这个时候 #2 肯定要初始化生成 java 对象了.
如果是 java7 及以后的版本这时候 jvm 会在堆中创建一个 "hello java" 的对象,. 然后将这个对象的引用放入全局字符串变量池 (也是在堆中) 中, 当以后出现 "hello java", 能在全局字符串变量池找到, 就不会再生成对象.
如果是 java6 版本 jvm 会在 permGen 中创建一个 "hello java" 的对象,. 然后将这个对象的引用放入全局字符串变量池 (在 permGen) 中, 当以后出现 "hello java", 能在全局字符串变量池找到, 就不会再生成对象.
所以全局字符串常量池中存放的只是索引, 他类似于 java 中的 HashMap,key 是字面量字符串, value 是是指向真正字符串对象的引用.
2.String.intern
JDK1.6 中, intern()方法会把首次遇到的字符串实例复制到永久代中然后把这个字符串的引用放入全局字符串常量池, 返回的也是永久代中这个字符串的实例的引用.
JDK1.7 中, intern()方法首次遇到字符串实例时不会在复制实例, 直接把这个实例的引用存入全局字符串常量池, 返回的就是这个字符串实例的引用.
那就有一个疑问, 为什么 jdk1.6 要重新复制一份呢? 因为 1.6 时字符串常量池在永久代, 而通过 new 产生的字符串在堆中, 两个区域内存隔离, 永久代无法存堆中的引用,
1.7 时代, jvm 把字符串常量池移到了堆中, 所以在 1.7 中就不用创造实例了.
- /**
- * jdk 1.8
- */
- public static void main(String[] args) {
- // 调用了 new String
- String s1 = new StringBuilder("lh").append("cy").toString();
- String s2 = s1.intern();
- //true
- System.out.println(s1 == s2);
- }
3. 字符串相加
3.1 编译时确定的字符串相加
- // 源代码
- public static void main(String[] args) {
- String s = "lh" + "cy";
- }
- // 字节码类的常量池
- Constant pool:
- #1 = Methodref #4.#20 // java/lang/Object."<init>":()V
- #2 = String #21 // lhcy
- #21 = Utf8 lhcy
- // 字节码操作指令
- 0: ldc #2 // String lhcy
- 2: astore_1
- 3: return
可以看见 "lh" 和 "cy" 被拼成了 "lhcy", 这个结论大家应该早就知道, 这里从字节码角度来看一下.
3.2 运行时确定的字符串相加
源代码:
- public String test(String s1, String s2) {
- return s1 + s2;
- }
字节码:
- // 新建一个 StringBuilder 对象
- 0: new #2 // class java/lang/StringBuilder
- // 复制新建的 StringBuilder 对象
- 3: dup
- // 消耗刚才复制的 StringBuilder 对象用于初始化
- 4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
- // 加载 s1
- 7: aload_1
- // 拼接到 StringBuilder
- 8: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
加载 s2
- 11: aload_2
- // 拼接到 StringBuilder
- 12: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
- // 调用 StringBuilder 的 toString 方法
- 15: invokevirtual #5 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
- 18: areturn
根据注释应该知道了上述代码创建了一个 StringBuilder 然后 append, 最后 toString. 那么又有一个小疑问, 既然两个字符串相加生成了 StringBuilder, 那么我们还
手动创建 StringBuilder 干嘛, 为什么不让我们之间使用 "+" 来拼接字符串. 那么请看下面的代码
- // 源代码
- public static void main(String[] args) {
- String s = "";
- for (int i = 0; i < 100; i++) {
- s += i;
- }
- }
- //jvm 实际执行的代码
- public static void main(String[] args) {
- String s = "";
- for (int i = 0; i < 100; i++) {
- StringBuilder stringBuilder = new StringBuilder();
- s = stringBuilder.append(s).append(i).toString();
- }
- }
从代码块中, 我们发现当每次要给字符串赋值时, StringBuilder 就会调用 toString 来新建字符串, jvm 并不知道你只需要循环后的结果, 在其中创建了大量无用的 String 对象, 不仅耗时
创建了对象, 并且占用了大量内存, 从而加快了 gc 的频率, 对系统运行非常不利.
总结
String 是一个常用的类, 基本使用非常简单, 但是他的底层实现非常复杂, c++ 基础不错的同学可以去看一下 String.intern()的源码和 ldc 的源码.
来源: https://www.cnblogs.com/zhandouBlog/p/10315371.html