0
点赞
收藏
分享

微信扫一扫

StringTable小结

科牛 2022-02-24 阅读 100
java

小结来源:尚硅谷宋红康JVM全套教程
内容参考:语雀——JVM笔记

String概述

  1. String属于引用数据类型,意为字符串类型,表示时需用" "引起来
  2. String在声明时被final修饰,表明String类不能被继承
    回顾:final关键字知识点,final修饰类表明该类不可被继承;修饰方法表明该方法不能被重写;修饰变量时变量成为常量,创建的字符串是否声明为final在内存中有区别,在本文后面会谈到
  3. String实现了Serializable接口,表明字符串是支持序列化的
  4. String实现了Comparable接口,重写了compareTo(obj)方法,按字典序排序
    回顾:String类中重写了Object类的equals()方法,重写以后,比较的不是引用地址,而是比较两个对象的"实体内容"
  5. String在jdk8及以前内部定义了final char[] value用于存储字符串数据,JDK9时改为使用byte[],并编码标志表明使用的是哪种编码:ISO-8859-1/Latin-1(每个字符一个字节)或UTF-16(每个字符两个字节)
  6. 通过字面量的方式给一个字符串赋值,此时的字符串值声明在字符串常量池中
    回顾:jdk1.6及以前,字符串常量池和静态变量在方法区中,方法区是一个概念,当时具体由永久代(PermGen)实现;jdk1.7时,逐渐“去永久代”,字符串常量池和静态变量放在堆(Heap)中;jdk1.8时,字符串常量池和静态变量仍然放在堆中,方法区由元空间(MetaSpace)实现,元空间占用的是本地内存,大小仅受本地内存限制
    Q:为什么StringTable要调整位置?A:①jdk7中将StringTable放到了堆空间中,因为永久代的回收效率很低,在full gc的时候才会触发,而full gc是老年代的空间不足、永久代不足时才会触发,这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。②更多的数据留在堆中,进行调优应用时仅需要调整堆的大小就可以

String的不可变性

  1. 对字符串重新赋值时,需要重新指定内存区域赋值,不能修改原有的字符串
  2. 对现的字符串进行连接操作时,也需要重新指定内存区域赋值,不能在原有的字符串后追加
  3. 调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能直接修改原有的值
  4. 字符串常量池是不会存储相同内容的字符串的,因为它的底层是固定大小的Hashtable,可使用-XX:StringTablesize设置StringTable的长度:jdk1.6中StringTable是固定的,就是1009的长度,若存储的字符串过多就会导致效率下降;jdk1.7中,StringTable的长度默认值是60013;JDK1.8中,1009是可以设置的最小长度

String的实例化

  1. 通过字面量定义,直接将字符串用双引号声明,这种方式声明出来的String对象会直接存储在常量池中,又因为字符串常量池中每种字符串只能有一个,所以用这种方式声明相同字符串的对象的地址值一定相等,注意,用字面量定义的也是变量,只有"str"这种表示或者用final修饰的是常量

  2. 通过创建对象(new String())的方式,这种方式声明出来的对象保存的是其在堆空间中的地址值

  3. 通过已有的字符串拼接,有多种可能

    ①常量与常量拼接:拼接结果在字符串常量池中,这是由于javac的编译优化,因为常量一定是不会改变的量,所以在编译阶段,javac就敢将它给简化,将两个常量字符串优化成一个字符串
    ②变量用final修饰:
    未用final修饰的变量,需用StringBuilder拼接
    变量用final修饰后即为常量,会在编译期进行代码优化,因此在实际开发中,能够使用final的,尽量使用final修饰
    ③常量与变量拼接:若其中有一个是变量(不是直接用" "引起来,而是用某个符号如s1等),则最后结果为堆空间中分配的某一地址值,其原理是StringBuilder拼接
    ④ 拼接结果调用intern()方法:若字符串常量池中不存在该字符串,将该字符串放入字符串常量池中,并返回其地址值,具体使用见后面小节

常量与常量拼接:
  public static void test1() {
      // 前端编译期会进行代码优化将"a" + "b" + "c"优化为"abc"
      // 通过idea直接看对应的反编译的class文件,会显示 String s1 = "abc"; 说明做了代码优化
      String s1 = "a" + "b" + "c";  
      String s2 = "abc"; 
  
      // true,有上述可知,s1和s2实际上指向字符串常量池中的同一个值
      System.out.println(s1 == s2);//==比较的是地址值 
  }
变量是否用final修饰:
public void test2(){
    String s0 = "beijing";
    String s1 = "bei";
    String s2 = "jing";
    String s3 = s1 + s2;
    System.out.println(s0 == s3); // false s3指向对象实例(堆空间中),s0指向字符串常量池中的"beijing"
    String s7 = "shanxi";
    final String s4 = "shan";
    final String s5 = "xi";
    String s6 = s4 + s5;
    System.out.println(s6 == s7); // true s4和s5是final修饰的,编译期优化,相当于"shan"+"xi"
}
常量与变量拼接:
public static void test3() {
    String s1 = "javaEE";
    String s2 = "hadoop";

    String s3 = "javaEEhadoop";
    String s4 = "javaEE" + "hadoop";    
    String s5 = s1 + "hadoop";
    String s6 = "javaEE" + s2;
    String s7 = s1 + s2;

    System.out.println(s3 == s4); // true 编译期优化
    System.out.println(s3 == s5); // false s1是变量,不能编译期优化
    System.out.println(s3 == s6); // false s2是变量,不能编译期优化
    System.out.println(s3 == s7); // false s1、s2都是变量
    System.out.println(s5 == s6); // false s5、s6 不同的对象实例
    System.out.println(s5 == s7); // false s5、s7 不同的对象实例
    System.out.println(s6 == s7); // false s6、s7 不同的对象实例

    String s8 = s6.intern();
    System.out.println(s3 == s8); // true intern之后,s8和s3一样,指向字符串常量池中的"javaEEhadoop"
}

面试题:String s = new String(“abc”);用这种方式创建对象,在内存中创建了几个对象?
答:两个,一个是堆空间中new结构,另一个是常量池中的数据:“abc”

public class test4 {
    public static void main(String[] args) {
        String s=new String("abc");
    }
}
字节码:
 0 new #2 <java/lang/String>
 3 dup
 4 ldc #3 <abc>
 6 invokespecial #4 <java/lang/String.<init>>
 9 astore_1
10 return

字符串拼接时创建了几个对象呢?

public class test5 {
    public static void main(String[] args) {
        String s1=new String("ab")+new String("c");
    }
}
 0 new #2 <java/lang/StringBuilder>
 3 dup
 4 invokespecial #3 <java/lang/StringBuilder.<init>>
 7 new #4 <java/lang/String>
10 dup
11 ldc #5 <ab>
13 invokespecial #6 <java/lang/String.<init>>
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <c>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>
34 astore_1
35 return

从字节码中可以看出,首先创建了一个StringBuilder对象,然后创建了一个String对象,接着再创建了"ab",使用append()方法添加到StringBuilder中,再又创建了一个String对象,接着再创建了"c",使用append()方法添加到StringBuilder中,目前创建了5个对象,但实际上toString()方法中还有一个对象,总共6个对象
在这里插入图片描述
而且通过StringBuilder的append方式的速度,要比直接对String使用“+”拼接的方式快很多,因此,在实际开发中,对于需要多次或大量拼接的操作,在不考虑线程安全问题时,应该尽可能使用StringBuilder进行append操作
除此之外,如果提前知道需要拼接String的个数,就应该直接使用StringBuilder的带参构造器(空参默认初始化大小是16)指定capacity,以减少扩容的次数,以此提高字符串方面的运行效率

intern相关题


当调用intern方法时,如果池子里不包含了与这个String对象相等的字符串,这个String对象将被添加到池中,并返回这个String对象的引用

/**
 * ① String s = new String("1")
 * 创建了两个对象
 * 		堆空间中一个new对象
 * 		字符串常量池中一个字符串常量"1"(注意:此时字符串常量池中已有"1")
 * ② s.intern()由于字符串常量池中已存在"1",所以 s 还是指向的是堆空间中的对象地址
 * s2 指向的是堆空间中常量池中"1"的地址
 * 所以不相等
 */
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s==s2); // jdk1.6 false jdk7/8 false

/*
 * ① String s3 = new String("1") + new String("1")
 * 等价于new String("11"),但是,常量池中并不生成字符串"11",只有"1";
 *
 * ② s3.intern()
 * 由于此时常量池中并无"11",所以jdk6创建了一个新的"11",jdk7创建了一个指向堆空间中分配的地址(new String())的地址,存入常量池
 * s4区常量池中找"11",发现其指向s3记录的对象的地址,所以他也指向该地址
 * 所以s3 和 s4 指向的是同一个地址
*/
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3==s4); //jdk1.6 false jdk7/8 true

注意:
JDK1.6中,将这个字符串对象尝试放入串池。

● 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
● 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址

JDK1.7起,将这个字符串对象尝试放入串池。

● 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
● 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址(这样做能节省空间)
在这里插入图片描述
**intern()优点:**对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用intern()方法能够节省内存空间。

今晚时间不够啦,StringTable的垃圾回收相关知识后期将与垃圾回收机制一起学习小结~

举报

相关推荐

0 条评论