乐鱼电竞

    教育行业A股IPO第一股(股票代码 003032)

    全国咨询/投诉热线:400-618-4000

    JVM字符串底层实现原理是什么?【Java培训】

    更新时间:2020年10月28日17时26分 来源:黑马程序员 浏览次数:

    一、什么字符串会进入字符串常量池

    1. 直接写的字面量

    2. 字面量的拼接结果(注意:如果字符串拼接中有变量则结果不会进入字符串常量池)

    3. 调用String的intern方法可以将String存入字符串常量池

    二、字面量的拼接原理

    有如下示列代码

    1. package com.hgy;
    2. import java.util.Arrays;
    3. import java.util.List;
    4. public class hello {
    5. public static void main(String[] args) {
    6. String a = "hello" + " world";
    7. }
    8. }
    • 在idea中查看编译后的class文件

      1. //
      2. // Source code recreated from a .class file by IntelliJ IDEA
      3. // (powered by Fernflower decompiler)
      4. //
      5. package com.hgy;
      6. public class hello {
      7. public hello() {
      8. }
      9. public static void main(String[] args) {
      10. String a = "hello world";
      11. }
      12. }

    结论:
    以上面两个文件我们可以看出,这种字符串的拼接在编译期间就已经优化了,直接就合并为一个字符串;并且这个字符串存放在字符串常量池。

    3. 字符串和变量拼接原理

    java源码

    1. package com.hgy;
    2. import java.util.Arrays;
    3. import java.util.List;
    4. public class hello {
    5. public static void main(String[] args) {
    6. String v = "java";
    7. String a = v + "hello" + " world";
    8. }
    9. }
    • 利用jclasslib查看main方法的字节码命令
      ·如果一下名词不明白请阅读请自行了解学习java虚拟机栈
      ·我们可以发现就简单的两行代码,产生了这么多的字节码命令;在代码中我简单解释了每一行的作用,
    1. ldc #2 <java> // 从字符串常量池加载java
    2. astore_1 // 存储常量到索引为1的局部变量表中
    3. new #3 <java/lang/StringBuilder> //给StringBuilder对象分配内存空间
    4. dup
    5. invokespecial #4 <java/lang/StringBuilder.<init>> //执行StringBuilder的构造方法
    6. aload_1 //获取局部变量表索引为1的引用地址,
    7. invokevirtual #5 <java/lang/StringBuilder.append> //把上面加载的内容作为参数传递给append方法
    8. ldc #6 <hello world> // 从字符串常量池加载hello world
    9. invokevirtual #5 <java/lang/StringBuilder.append> //把上面加载的内容作为参数
    10. 传递给append方法
    11. invokevirtual #7 <java/lang/StringBuilder.toString> //调用toString方法
    12. astore_2 //结果存储到局部变量表
    13. return
    • 以上内容我们可以知道字符串拼接实际上就是创建了一个StringBuilder对象然后向里面append内容,最后调用toString方法获得结果

    3.1 为什么结果没有存储在常量池

    从上述字节码指令已经知道了字符串拼接结果是StringBuilder的toString方法的结果,那么toString里面具体做了什么事情,又是为什么结果不在常量池?
    以下是StringBuilder.toString的源码以及字节码指令

    1. @Override
    2. public String toString() {
    3. // Create a copy, don't share the array
    4. //此处value为一个char数组【我的jdk版本为jdk8】
    5. return new String(value, 0, count);
    6. }
    1. new #80 <java/lang/String>
    2. dup
    3. aload_0
    4. getfield #234 <java/lang/StringBuilder.value>
    5. iconst_0
    6. aload_0
    7. getfield #233 <java/lang/StringBuilder.count>
    8. invokespecial #291 <java/lang/String.<init>>
    9. areturn

    以上代码可以很好的解释实际上最终是调用了String的构造方法传入一个char数组,那么最终的结果肯定也就在咱么的堆空间。

    4. 为什么字符串拼接效率低

    4.1. 源码准备
    首先编写两个方法一个使用字符串拼接,一个使用StringBuilder进行拼接;

    1. public class hello {
    2. public void concatStrByDefault() {
    3. String basic = "name ";
    4. for (int i = 0; i < 100; i++) {
    5. basic += i;
    6. }
    7. System.out.println(basic);
    8. }
    9. public void concatStrByBuilder() {
    10. StringBuilder basic = new StringBuilder("name ");
    11. for (int i = 0; i < 100; i++) {
    12. basic.append(i);
    13. }
    14. System.out.println(basic.toString());
    15. }
    16. }

    4.2.字节码指令层面解析

    一上代码的执行时间长短我就不在重复测试了相信大家都会,接下来我们来一起看看这两个方法字节码指令

    concatStrByDefault方法的字节码指令如下

    简单解释下循环是在33行的goto指令调到第5行这样不断循环;并且在11行也就是循环中不断的通过new创建了StringBuilder对象,也就是循环了多少次就创建了多少个StringBuilder对象,并且如果大家看了我之前写字符串拼接原理,在StringBuilder的toString方法中还new了一个String对象;这里这么多对象的创建就必然需要垃圾回收效率自然就低了

    1. ldc #2 <name >
    2. astore_1
    3. iconst_0
    4. istore_2
    5. iload_2
    6. bipush 100
    7. if_icmpge 36 (+28)
    8. new #3 <java/lang/StringBuilder>
    9. dup
    10. invokespecial #4 <java/lang/StringBuilder.<init>>
    11. aload_1
    12. invokevirtual #5 <java/lang/StringBuilder.append>
    13. iload_2
    14. invokevirtual #6 <java/lang/StringBuilder.append>
    15. invokevirtual #7 <java/lang/StringBuilder.toString>
    16. astore_1
    17. iinc 2 by 1
    18. goto 5 (-28)
    19. getstatic #8 <java/lang/System.out>
    20. aload_1
    21. invokevirtual #9 <java/io/PrintStream.println>
    22. return

    concatStrByBuilder方法的字节码指令

    此处循环在27行的goto指令跳到12行,并且循环之间是没有创建新对象的,紧紧只是调用了append方法,这里就能很明显的看出这种方式比普通拼接少创建了很多的对象

    1. new #3 <java/lang/StringBuilder>
    2. dup
    3. ldc #2 <name >
    4. invokespecial #10 <java/lang/StringBuilder.<init>>
    5. astore_1
    6. iconst_0
    7. istore_2
    8. iload_2
    9. bipush 100
    10. if_icmpge 30 (+15)
    11. aload_1
    12. iload_2
    13. invokevirtual #6 <java/lang/StringBuilder.append>
    14. pop
    15. iinc 2 by 1
    16. goto 12 (-15)
    17. getstatic #8 <java/lang/System.out>
    18. aload_1
    19. invokevirtual #7 <java/lang/StringBuilder.toString>
    20. invokevirtual #9 <java/io/PrintStream.println>
    21. return

    4.3. 总结

    拼接效率低的主要原因也就是每一次拼接都创建了一个StringBuilder对象,并且在赋值是又需要调用toString方法,而toString方法的实现里面有new了一个String对象,所以拼接的效率很低。

    0 分享到:
    和我们在线交谈!
    【网站地图】【sitemap】