JVM 虚拟机中StringTable

String 的基本特性

  • String字符串,使用一对 '" 引起来表示
    • String s1 = “str”; // 字面量的定义方式、
    • String s2 = new String(“hello”);
  • String 声明为 final的 不可以被继承
  • String 实现了 Serializable 接口: 表示字符串是支持序列化的,实现了Comparable 接口,表示String 可以比较大小
  • String 在 jdk 8 级以前内部定义了 final char[] value 用于存储字符串数据,JDK9 时候改为了byte[]
  • String 代表不可变得字符序列: 简称 不可变性。
    • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value赋值
    • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值
    • 当调用String的replace 方法修改指定字符和字符串时候,也需要重新指定内存区域值,不能使用原有的value赋值
  • 通过字面量 (区别new ) 给一个字符串赋值,此时的字符串值声明在字符串常量池。
  • 字符串常量池汇总是不会存储相同内容的字符串的
  • String的String Pool 是一个固定大小的Hashtable, 默认值大小长度是 1009, 如果放进String Pool 的String 非常多,会造成Hash冲突严重,导致链表很长,而链表很长之后会造成的影响就是当调用String.intern时 性能会大幅度下降
  • 使用 -XX:StringTableSize 可以设置 StringTable的长度度
  • 在jdk6中 StringTable 是固定,就是 1009 的长度,所以如果常量池中的字符串过多就会导致效率下降很快,StringTableSize 设置没有要求。
  • 在jdk7中,StringTable的长度默认是 60013 ,1009 是可以设置的最小值。
  • 通过 jinfo -flag StringTableSize java 进程号 查看 设置的值

String 的 内存分配

  • 在JAVA语言里有 8 种基本数据类型和比较特殊的类型String. 这些类型为了让他们在运行过程中速度更快,更节省内存。都提供了一种常量池的概念。
  • 常量池就是类似一个JAVA系统级别的缓存。 8 中基本数据类型的常量池都是系统协调的。String类型的常量池比较特殊。他的主要使用方法有两种
    • 直接使用双引号声明出来的String对象会直接存储在常量池中。
    • 如果不是使用双引号声明的String对象,可以使用String提供的intern() 方法。 将对象放入到常量池中。
  • Java 6 及以前,字符串常量池存放在永久代。
  • Java 7 中Oracle 的工程师对字符串池的逻辑做了很大的改变,即将Java 字符串常量池的位置调整到堆中。
    • 所有的字符串都保存在堆 (Heap) 中,和其他普通对象一样,这样就可以让你在进行调优的时候仅需要调整堆大小就可以了。
    • 字符串常量池概念原本使用的比较多。但是这个改动使我们有足够的理由,让我们在JAVA7中使用 String.intern();
    • Java8 元空间, 字符串常量在堆

JVM 虚拟机中StringTable_第1张图片
StringTable 为什么要调整

  • permSize 默认比较小
  • 永久代垃圾回收频率比较低

查看程序中的StringPool的 字符串个数

public static void main(String[] args) {
        System.out.println();
        System.out.println("1");
        System.out.println("2");
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println();
        System.out.println("1");
        System.out.println("2");
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
}

JVM 虚拟机中StringTable_第2张图片
可以看到我当前的字符串个数为 2474 个。 当我依次打印到 第一个 “5” 的时候
JVM 虚拟机中StringTable_第3张图片
常量池中的个数增加到了 2480 个。 当我打印到 第二个 "5"的时候。,
JVM 虚拟机中StringTable_第4张图片
还是 2480 个,也就是说字面量的String 实例。只能拥有一份。
Java 语言规范里要求完全相同的字符串字面量,应该包含同样的 Unicode 字符序列(包含同一份码点序列的常量), 并且必须是指向同一个String类实例。

字符串的拼接操作

  1. 常量与常量的拼接,结果在常量池,原理是 编译器优化。
  2. 常量池中不会存在相同内容的常量
  3. 只要其中有一个是变量,结果就在对重,变量拼接的原理是StringBuilder
  4. 如果拼接的结果调用的intern() 方法, 则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。

第一个示例

/**
 * 常量与常量的拼接,结果在常量池,原理是 编译器优化。
 */
public static void test1() {
    String str1 = "a" + "b" + "c";
    String str2 = "abc";
    System.out.println(str1 == str2);   // true
    System.out.println(str1.equals( str2));  // true 
}

通过 == 判断字符串 为 true 说明两者之间的地址是同一个

第二个示例:

 /**
  * 常量池中不会存在相同内容的常量
  */
 public static void test2() {

    /**
     * 常量池中不会存在相同内容的常量
     */
    public static void test2() {
        String str1 = "javaEE";
        String str2 = "hadoop";

        String str3 = "javaEEhadoop";
        String str4 = "javaEE" + "hadoop";
        String str5 = str1 + "hadoop";
        String str6 = "javaEE" + str2;
        String str7 = str1 + str2;


        System.out.println(str3 == str4);   // true
        System.out.println(str3 == str5);   // false
        System.out.println(str3 == str6);   // false
        System.out.println(str3 == str7);   // false
        System.out.println(str5 == str6);   // false
        System.out.println(str5 == str7);   // false
        System.out.println(str6 == str7);   // false

        //  intern() 判断字符串常量池中是否存在javaEEhadoop 值,如果存在,则返回常量池中的javaEEhadoop 的地址,
        // 如果字符串常量池中不存在, 则在常量池中加载一份 javaEEhadoop, 并返回此对象的地址
        String str8 =	str6.intern(); 	// 如果拼接的结果调用的`intern()` 方法, 则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
        System.out.println(str3 == str8); // true
    }

其实这个面试题也挺常见的。
str1 + str2 其实具体的操作就是

StringBuilder s = new StringBuilder();
s.append(str1);
s.append(str2);
s.toString();  // 内部其实就是new String("javaEEhadoop")

字符串拼接也不一定全是不相等的

/**
* 字符串拼接不一定全是使用的StringBuilder()
* 如果拼接符号左右两边都是字符串常量或者常量引用,则依然使用编译器优化,即非StringBuilder 的方式、
* 针对于 final 修饰类,方法,基本数据类型,引用数据类型的量的结构时,能使用final 尽量使用。
 */
public static void test3() {
    final String s1 = "a";
    final String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    System.out.println(s3 == s4);  /// true
}

感受一下 StringBuilder 和 字符串 + 号方式拼接的效率

    public static void main(String[] args) {

        long start = System.currentTimeMillis();

//        test4(100000);  // 字符串拼接的方式:25175
        test5(100000); // StringBuilder 的方式: 13 毫秒
        long end =System.currentTimeMillis();

        System.out.println(end - start);
    }
    public static void test4(int count) {
        String src = "";
        for (int i = 0; i < count; i ++) {
            src = src + i;
        }
    }

    public static void test5(int count) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < count; i++) {
            sb.append(i);
        }
        System.out.println(sb.toString());
    }

我这边执行, 通过+ 拼接的方式耗时 25175 ms
通过 StringBuilder 的append() 方式只有 13 ms

效率执行: 通过StringBuilder 的append() 方式添加字符串的效率远高于使用String的字符串的拼接方式!
详情:①. StringBuilder 的 append() 方式,自始至终,都只是一个 StringBuilder 的对象,而 String 的拼接方式,每次拼接都需要创建一次 StringBuilder 对象

②. 内存中由于创建了比较多的StringBuilder和String 对象,内存占用更大,如果需要进行GC,则需要花费更多的时间。
③. 在实际开发中,如果基本确定前前后后添加的字符串长度不会高于某个限定的值的情况下,建议使用构造器创建StringBuilder 对象, StringBuilder s = new StringBuilder(highLevel);

intern() 方法的使用

如果不是双引号声明的String对象,可以使用String提供的intern() 方法,intern() 会从字符串常量池中查询当前字符串是否存在,如果存在就将当前字符串放入常量池中

  • 比如String myInfo = new String(“I love you”).intern();
    也就是说,如果在任意字符串上调用 String.intern() 方法,那么其返回的结果所指向的那个实例,必须和直接以常量形式出现的字符串示例完全相同,因此,下列表达式的值必定为 true.
  • ("a" + "b" + "c").inter() == "abc"
    通俗点将,Interned String 就是确保字符串在内存中只有一份拷贝,这样可以节省内存空间,加快字符串操作任务的执行速度,注意这个值会被存放在字符串内部池 (String Intern Pool)

面试题:

  1. new String(“ab”) 会创建几个对象?
    我们来看一下字节码文件就可以发现创建得是两个对象
public static void main(String[] args) {
	String str = new String("ab");
}

字节码文件对应得部分

 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           #2                  // class java/lang/String
         3: dup
         4: ldc           #3                  // String ab
         6: invokespecial #4                  // Method java/lang/String."":(Ljava/lang/String;)V
         9: astore_1
        10: return
      LineNumberTable:
        line 34: 0
        line 36: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  args   [Ljava/lang/String;
           10       1     1   str   Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args

可以看到 当前 new 出来的对象实在堆空间创建得,另一个对象是字符串穿常量池中得对象。 字节码指令: ldc

  1. new String(“a”) + new String(“b”)
    JAVA 源代码
public static void main(String[] args) {
    String s1 = new String("1") + new String("2");
}

class 字节码文件对应部分

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=2, args_size=1
         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."":()V
         7: new           #4                  // class java/lang/String
        10: dup
        11: ldc           #5                  // String 1
        13: invokespecial #6                  // Method java/lang/String."":(Ljava/lang/String;)V
        16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: new           #4                  // class java/lang/String
        22: dup
        23: ldc           #8                  // String 2
        25: invokespecial #6                  // Method java/lang/String."":(Ljava/lang/String;)V
        28: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        31: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: astore_1
        35: return
      LineNumberTable:
        line 36: 0
        line 37: 35
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      36     0  args   [Ljava/lang/String;
           35       1     1    s1   Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args

从字节码指令可以看出来,实际上是以下几个对象:
对象 1 : StringBuilder 对象
对象 2 : String 对象 new String("a")
对象 3 : 常量池中的 a
对象 4 : Striing 对象 new String("b")
对象 5 : 常量池中的 b
最后调用了 StringBuilder 的 toString() 方法, 内部返回了一个 new String(value, 0, count);

  1. 以下打印的值为多少
    java 源代码
public static void main(String[] args) {
    String s = new String("1");  // 创建 String 对象的同时,在字符串常量池中也添加了一个 "1" 的常量。
    s.intern();			// 调用 intern 并不会创建一个新的常量对象。
    String s2 = "1";	// 所以 字面量赋值 "1" 的方式是在常量池中的 "1" 而此时的 s 指向的是 堆中的 "1" 
    System.out.println(s == s2);  // jdk6: false  jdk7/8: false	 // 所以此处的 == 判断比较地址 所以为 false

    String s3 = new String("1") + new String("1"); 	// 当使用 new String("1") + new String("1") 的时候,并没有在 常量池中创建 "11" 的常量。
    s3.intern();	// 所以当使用 intern() 方法的时候。  jdk6: 是会在常量池中创建一个新的对象 "11" 也就是一个新的地址。 但是 jdk7 的时候, 此时在常量池中并没有创建 "11" , 而是创建了一个指向堆空间的一个引用。所以 此时的常量池的引用地址还是存在于 对空间中。
    String s4 = "11";// 当时用 jdk6 的时候, 使用 s4 = "11" 的赋值引用指向的常量池中的全新的 "11" 对象,而使用jdk7的时候,s4 ="11" 的指向的是存在于常量池中的一个指向堆空间 "11"的地址。 所以
    System.out.println(s3 == s4);  // jdk6: false  jdk7/8: true  在使用== 的时候, jdk6 和 jkdk7 会有不同的结果。
}

你可能感兴趣的:(jvm)