String

1,String的基本特性

  2,String的内存分配

   3,String基本操作

    4,字符串拼接操作

      5,intern()的使用

       6,StringTable的垃圾回收

        7,G1中String去重操作

          8,个人总结

 


 

String的基本特性

String:字符串,使用一对 “” 引起来表示

  •       String s1 = “ylcandzy”;//字面量的定义方式

  •       String s2 = new String("hello");

String声明为final的,不可被继承

 

String实现了Serializable接口;表示字符串是支持序列化的,实现Comparable接口:表示String可以比较大小

 

String在jdk8及以前内部定义了final char[] value用于存储字符串数据。jdk9时改为byte[]

 

  • String存储结构变更

        结论:String再也不用 char[] 来存储,改为 byte[] 加上编码标记,节约了一些空间

   public final class String
    implements java.io.Serializable,Comparable<String>,CharSequence{
        @Stable
        private final byte[] value;
    }

 

        那StringBuffer 和 StringBuilder 是否仍然无动于衷?

  String-related classes such as AbstractStringBuilder,StringBuilder,
  and StringBuffer will be updated to use the same representation,as will the
  HotSpot VM’s intrinsic(固有的,内置的)string operations        

 

  • String:代表不可变的字符序列。简称:不可变性
  1.           当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
  2.           当对现在的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用功能原有的value进行赋值。
  3.           当调用String的 replace() 方法修改指定字符串或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  •  通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
复制代码
package com.ylc.java05;

import org.junit.Test;

/**
 *
 *
 * String基本使用:验证String的不可变性
 * 并不会在原有的地址上进行修改,只会创建一个新的地址
 *
 * @Author ylc
 * @Date 2022/3/25 16:02
 * @Version 1.0
 */
public class StringTest01 {

    @Test
    public void test01(){
        String s1 = "abc";//字面量定义的方式,"abc"存储在字符串常量池中
        String s2 = "abc";
        s2 = "hello";

        System.out.println(s1 == s2); //判断地址
        System.out.println(s1);
    }

    @Test
    public void test02(){
        String s1 = "abc";
        String s2 = "abc";
        s2 += "ylc";

        System.out.println(s1 == s2);
        System.out.println(s2);
    }

    @Test
    public void test03(){
        String s1 = "abc";
        String s2 = s1.replace("a","y");

        System.out.println(s1 == s2);
    }
}
复制代码

字符串常量池中是不会存储相同内容的字符串的。

  • String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。
  • 使用-XX:StringTableSize可以设置StringTable的长度
  • 在jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设置没有要求。
  • 在jdk7中StringTable的长度默认值60013,1009是可以设置的最小值。
  • jdk8开始,1009是可设置的最小值。

 

String内存分配

  • 常量池就是类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它是主要使用方法有两种
  1. 直接使用双引号声明出来的String对象会直接存储在常量池中。(比如:String info = “ylcandzy”);
  2. 如果不是用双引号声明的String对象,可以使用String提供的intern()方法。
  • Java 6及以前,字符串常量池存放在永久代。
  • Java 7中字符串常量池的位置调整到Java堆中。
  • Java 8元空间,字符串常量在堆。
  • StringTable为什么要调整?
  1. permSize默认比较小
  2. 永久代垃圾回收频率低
复制代码
package com.ylc.java05;

import java.util.HashSet;
import java.util.Set;

/**
 *
 * jdk6:
 * -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
 *
 * jdk8:
 * -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
 * @Author ylc
 * @Date 2022/3/25 17:47
 * @Version 1.0
 */
public class StringTest02 {
    public static void main(String[] args) {
        //使用Set保持着常量池引用,避免full gc 回收常量池行为
        Set<String> set = new HashSet<String>();

        //在short可以取值的范围内足以让6MB的PermSize或heap产生OOM
        short i = 0;
        while (true){
            set.add(String.valueOf(i++).intern());

        }
    }
}
复制代码

String的基本操作

 

 

复制代码
package com.ylc.java05;

/**
 * @Author ylc
 * @Date 2022/3/25 18:23
 * @Version 1.0
 */
public class StringTest03 {
    public static void main(String[] args) {
        int i = 1;
        Object obj = new Object();
        StringTest03 mem = new StringTest03();
        mem.foo(obj);
    }

    private void foo(Object param){
        String str = param.toString();
        System.out.println(str);
    }
}
复制代码

字符串拼接操作

  1. 常量与常量的拼接结果在常量池,原理是编译期优化
  2. 常量池中不会存在相同内容的常量。
  3. 只要其中有一个变量,结果就是在堆中。变量拼接的原理是StringBuilder
  4. 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
复制代码
    @Test
    public void test02(){
        String s1 = "ylc";
        String s2 = "zy";

        String s3 = "ylcandzy";
        String s4 = "ylcand" + "zy";//编译期间优化
        //如果拼接符号的前后出现了变量,则相当于堆空间new String(),具体的内容为拼接的结果:ylcandzy
        String s5 = s1 + "zy";
        String s6 = "ylc" +s2;
        String s7 = s1 + s2;

        System.out.println(s3 == s4);
        System.out.println(s3 == s5);
        System.out.println(s3 == s6);
        System.out.println(s3 == s7);
        System.out.println(s5 == s6);
        System.out.println(s5 == s7);
        System.out.println(s6 == s7);
        //intern():判断字符串常量池中是否存在ylcandzy值,如果存在,则返回常量池中ylcandzy的地址;
        //如果字符串常量池不存在ylcandzy,则在常量池中加载一份ylcandzy,并返回对象的地址
        String s8 = s6.intern();
        System.out.println(s3 == s8);
    }
}
复制代码

  A:字符串变量拼接操作的底层原理

  •     如果拼接符号的前后出现了变量,则相当于堆空间new String(),具体的内容为拼接的结果:ylcandzy
  • //intern():判断字符串常量池中是否存在ylcandzy值,如果存在,则返回常量池中ylcandzy的地址;
    • //如果字符串常量池不存在ylcandzy,则在常量池中加载一份ylcandzy
  • 1,字符串拼接操作不一定使用的是StringBuilder
    • 如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译器优化,即非StringBuilder的方式
    • 针对于final修饰类,方法,基本数据类型,引用数据类型的量结构时,能使用上final的时候建议使用
     

 

复制代码
package com.ylc.java05;

import org.junit.Test;

/**
 * @Author ylc
 * @Date 2022/3/25 18:43
 * @Version 1.0
 */
public class StringTest04 {
    @Test
    public void test01(){
        String s1 = "a"+"b"+"c";//等同于“abc”
        String s2 = "abc";//"abc"一定是放到字符串常量池中,将此地址赋给s2

        /*
        最终.java编译成.class,在执行.class
        String s1 = “abc”;
        String s2 = “abc”;
         */

        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
    }

    @Test
    public void test02(){
        String s1 = "ylc";
        String s2 = "zy";

        String s3 = "ylcandzy";
        String s4 = "ylcand" + "zy";//编译期间优化
        //如果拼接符号的前后出现了变量,则相当于堆空间new String(),具体的内容为拼接的结果:ylcandzy
        String s5 = s1 + "zy";
        String s6 = "ylc" +s2;
        String s7 = s1 + s2;

        System.out.println(s3 == s4);
        System.out.println(s3 == s5);
        System.out.println(s3 == s6);
        System.out.println(s3 == s7);
        System.out.println(s5 == s6);
        System.out.println(s5 == s7);
        System.out.println(s6 == s7);
        //intern():判断字符串常量池中是否存在ylcandzy值,如果存在,则返回常量池中ylcandzy的地址;
        //如果字符串常量池不存在ylcandzy,则在常量池中加载一份ylcandzy,并返回对象的地址
        String s8 = s6.intern();
        System.out.println(s3 == s8);
    }

    @Test
    public void test03(){
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";

        /**
         *
         * 如下s1 + s2 的执行细节:(变量s是我临时定义的)
         * StringBuilder s = new StringBuilder;
         * s.append("a")
         * s.append("b")
         * s.toString()   --> 约等于 new String("ab")
         *
         * 补充:在jdk5.0之后使用的是StringBuilder,jdk5.0之前是使用的StringBuilder
         */
        String s4 = s1 + s2;
        System.out.println(s3 == s4);//false  创建了一个新对象
    }

    /*
    1,字符串拼接操作不一定使用的是StringBuilder
      如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译器优化,即非StringBuilder的方式
    2,针对于final修饰类,方法,基本数据类型,引用数据类型的量结构时,能使用上final的时候建议使用
     */

    @Test
    public void test04(){
        final String s1 = "a";
        final String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        System.out.println(s3 == s4);
    }
}
复制代码

 

B:拼接操作与append操作进行对比

体会执行效率:通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式!

详情:a StringBuilder的append()的方式:自始至终中只创建过一个StringBuilder的对象

              使用String的字符串拼接方式:创建过多个StringBuilder和String的对象

           b 使用功能String的字符串拼接方式:内存中由于创建了较多的StringBuilder和String的对象,内存占用更大;

              如果要使用GC,需要花费额外的时间。

改进时间:在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevel的情况下,

                  建议使用构造器。

                  StringBuilder s = new StringBuilder(highLevel);//new char[highLevel]

复制代码
    @Test
    public void test06(){
        long start = System.currentTimeMillis();

//        method01(100000);//4699
        method02(100000);//花费时间为: 4

        long end = System.currentTimeMillis();

        System.out.println("花费时间为: " + (end - start));

    }


    public void method01(int highLevel){
        String str = "";
        for (int i = 0; i < highLevel;i++){
            str = str + "a";//每次循环都会创建一个StringBuilder,String
        }
//        System.out.println(str);
    }

    public void method02(int highLevel){
        //只需要创建一个StringBuilder
        StringBuilder src = new StringBuilder();
        for (int i = 0; i < highLevel; i++) {
            src.append("a");
        }
//        System.out.println(src);
    }
}
复制代码

intern()使用

  1. intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
  2.    比如: String myInfo = new String(“ylc”).intern();
  3. 也就是说,如果在任意字符串上调用String.intern方法,那么其返回结果所指向那个类实例,必须和直接以常量形式出现的字符串实例完全相同。
  4.   (“a”+“b”+“c”).intern() == "abc"   //true
  5. 通俗点讲,Interned String就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。(存放在 Pool)

 

  • A:new String()到底能创建多少个对象(面试题)?
复制代码
package com.ylc.java06;

/**
 * @Author ylc
 * @Date 2022/3/26 14:21
 * @Version 1.0
 *
 * 题目:
 * new String("ab")会创建几个对象?看字节码,就两个
 *          一个对象是:new 关键字在堆空间创建的
 *          另一个对象:字符串常量池中的对象"ab".  字节码指令: ldc
 *
 * 思考:
 * new String("a") + new String("b")呢?
 *   对象1:new StringBuilder()
 *   对象2:new String("a")
 *   对象3:常量池中“a”
 *   对象4:new String("b")
 *   对象5:常量池中"b"
 *
 *   深入剖析:StringBuilder的toString();
 *        对象6:new String("ab")
 *        强调一下:toString()的调用,在字符串常量池中,没有生成"ab"
 */
public class StringNewTest {
    public static void main(String[] args) {
//        String str01 = new String("ab");

        String str02 = new String("a") + new String("b");
    }
}
复制代码

关于intern()的面试题!!!

 

复制代码
package com.ylc.java06;

/**
 * @Author ylc
 * @Date 2022/3/26 11:31
 * @Version 1.0
 *
 * 如何保证变量s指向的是字符串常量池中的数据呢?
 * 方式一: String s = “ylc”;字面量定义方式
 *
 * 方式二:  String s = new String("ylc").intern();
 *         String s = new StringBuilder("ylc").toString().intern();
 *
 */
public class StringIntern {
    public static void main(String[] args) {

        String s = new String("1");
        s.intern();//调用此方法之前,字符串常量池中已经存在“1”
        String s2 = "1";
        System.out.println(s == s2);//jdk6:false  jdk7:false


        String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("22")
        //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?  不存在
        s3.intern();//字符串常量池中生成"11".
        // 如何理解:jdk6:创建了一个新对象"11",也就是新的地址
                   //jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String
        String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池生成的"11"的地址
        System.out.println(s3 == s4);//jdk6:false    jdk7:true
    }
}
复制代码

 

inter()是使用:jdk6 vs jdk7/8

   jdk1.6中,将这个字符串对象尝试放入串池。

           如果串池中有,则并不会放入。返回已有的串池中的对象的地址

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

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

           如果串池中有。则并不会放入。返回已有的串池中的对象的地址

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

 

intern()练习1:

复制代码
package com.ylc.java06;

import com.sun.xml.internal.ws.api.model.wsdl.WSDLOutput;

/**
 * @Author ylc
 * @Date 2022/3/26 18:13
 * @Version 1.0
 */
public class StringIntern01 {
    public static void main(String[] args) {


        //
        String s = new String("a") + new String("b"); //new String("ab")
        //在上一行代码执行完以后,字符串常量池中并没有“ab”

        String s2 = s.intern();//jdk6:在串池中创建一个字符串“ab”
                               //jdk8:串池中没有创建字符串“ab”,而是创建了一个引用地址

        System.out.println(s2 == "ab");//jdk8:true  jdk6:true
        System.out.println(s == "ab");//jdk8:true   jdk6:false
    }
}
复制代码

 

 

 

 

 

 

inter()练习2:

复制代码
package com.ylc.java06;

/**
 * @Author ylc
 * @Date 2022/3/26 18:47
 * @Version 1.0
 */
public class StringIntern02 {
    public static void main(String[] args) {
        String s1 = new String("ab");//执行完以后,会在字符串常量池中会生成
//        String s2 = new String("a") + new String("b"); //执行完以后,不会在字符串常量池中生成
        s1.intern();
        String s2 = "ab";
        System.out.println(s1 == s2); //false
    }
}
复制代码

intern()空间效率测试:
   使用intern() 测试执行效率:空间使用上

   结论:对于程序中大量存在的字符串,尤其其中存在很多重复字符串时,使用intern()可以减少内存空间。

复制代码
package com.ylc.java06;

import com.ylc.day01.Test;

/**
 * @Author ylc
 * @Date 2022/3/26 22:27
 * @Version 1.0
 */
public class StringIntern03 {
    static final int MAX_COUNT = 1000 * 1000;
    static final String[] arr = new String[MAX_COUNT];

    public static void main(String[] args) {
        Integer[] data = new Integer[]{1,2,3,4,5,6,7,8,8,9,10};

        long start = System.currentTimeMillis();
        for (int i = 0; i < MAX_COUNT; i++) {
            arr[i] = new String(String.valueOf(data[i % data.length])).intern();
        }
        long end  = System.currentTimeMillis();
        System.out.println("花费的时间为:" + (end - start));
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.gc();

    }
}
复制代码

 

StringTable的垃圾回收

  -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails

 

 

复制代码
package com.ylc;

/**
 * @Author ylc
 * @Date 2022/3/27 9:27
 * @Version 1.0
 */
public class StringGCTest {
    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            String.valueOf(i).intern();
        }
    }
}
复制代码

G1的String去重操作

背景:对许多应用(有大也有小)做的测试有以下结果:

  •        堆存活数据集合里面String对象占了25%
  •        堆存活数据集合里面重复String对象有13.5%
  •        String对象的平均长度是45

Java堆中存活的数据集合差不多25%是String对象,堆上存在重复的String对象必然是一种内存的浪费。

命令行选项:

  1.       UseStringDeduplication(bool):开启String去重,默认是不开启,需要手动开启。
  2.       PrintStringDeduplicationStatistics(bool):打印详细去重统计信息。
  3.       StringDeplicationAgeThreshold(uintx):达到这个年龄的String对象被认为是去重的后选对象。

 

总结:String重点在于intern()方法,需多次理解底层,其次内存分配。整体上并不难

复制代码
String源码
public
final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; /** Cache the hash code for the string */ private int hash; // Default to 0 /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -6849794470754667710L; ........ }
复制代码

 

 

 

 

 

 

posted on   只想做加法(ylc)  阅读(156)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

导航

统计

点击右上角即可分享
微信分享提示