JVM——(11)String Table(字符串常量池)

往期文章

JVM——(1)为什么学习虚拟机
JVM——(2)聊聊JVM虚拟机
JVM——(3)类加载子系统
JVM——(4)运行时数据区的概述与程序计数器(PC寄存器)
JVM——(5)运行时数据区的虚拟机栈
JVM——(6)运行时数据区的本地方法栈
JVM——(7)运行时数据区的堆空间
JVM——(8)运行时数据区的方法区
JVM——(9)对象的实例化与访问定位
JVM——(10)执行引擎
JVM——(11)String Table(字符串常量池)
JVM——(12)垃圾回收概述
JVM——(13)垃圾回收相关算法
JVM——(14)垃圾回收相关概念的概述
JVM——(15)垃圾回收器详细篇
JVM——(16)Class文件结构一(描述介绍)
JVM——(17)Class文件结构二(解读字节码)
JVM——(18)Class文件结构三(JAVAP指令)
JVM——(19)字节码指令集与解析一(局部变量压栈、常量变量压栈、出栈局部变量表指令)
JVM——(20)字节码指令集与解析二(算数指令)
JVM——(21)字节码指令集与解析三(类型转换指令)
JVM——(22)字节码指令集与解析四(对象创建与访问指令)
JVM——(23)字节码指令集与解析五(方法调用指令与方法返回指令)
JVM——(24)字节码指令集与解析六(操作数栈管理指令)

前言

我们在实际开发当中使用String非常的广泛,那么对使用String类其实有很多角度可以去学习理解

那么本篇文章,我们从使用String的层次到开始了解分析String的实现、性能等等

一、String的基本特性


对于String我们称为字符串,使用一对 “” 引号起来表示

那么平常我们的使用有不同的定义方式如下:

  • String s1 = "xiaomingtongxue" ; 称呼为字面量的定义方式
  • String s2 = new String("hello");称呼为对象的方式

我们可以观察一下String的源代码分析看看
在这里插入图片描述

如图可以观察到String被声明为final的:表示它不可被继承

如图可以观察到String实现了Serializable接口:表示字符串是支持序列化的

如图可以观察到实现了Comparable接口:表示String可以比较大小

如图可以观察到String在jdk8及以前内部定义了final char value[]用于存储字符串数据

但是它在JDK9时改为了byte[],我们可以切换到JDK9的环境去看看String的源码
在这里插入图片描述

为什么 JDK9 改变了 String 的结构

================================

可以访问官方文档查看详细的说明:访问入口

具体我们就粘贴官网的说明进行翻译解释
在这里插入图片描述
在这里插入图片描述

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

同时基于String的数据结构,例如StringBuffer和StringBuilder也同样做了修改

对于String来说它代表了不可变的字符序列,简称:不可变性。

接下来我们使用示例体会一下它的不可变性

public void test2() {
   String s1 = "abc";
   String s2 = "abc";
   System.out.println(s1 == s2);
   System.out.println(s1);
   System.out.println(s2);
}    
//运行如下:
true
abc
abc

结论:这两个abc实际共用的是堆空间里的字符串常量池里边的同一个,所以两个引用地址是一样的

public void test2() {
   String s1 = "abc";
   String s2 = "abc";
   s1 = "hello";
   System.out.println(s1 == s2);
   System.out.println(s1);
   System.out.println(s2);
}
//运行如下:
false
abc
abc

结论:当对s1字符串重新赋值时会新建一个指定内存区域并赋值,所以在比较的时候两个引用地址不一样

public void test2() {
   String s1 = "abc";
   String s2 = "abc";
   s2 += "def";
   System.out.println(s2);
   System.out.println(s1);
}
//运行如下:
abcdef
abc

结论:当对s1字符串进行连接操作时也会新建一个指定内存区域并赋值,不在原有的value进行赋值

public void test3() {
    String s1 = "abc";
    String s2 = s1.replace('a', 'm');
    System.out.println(s1);
    System.out.println(s2);
}
//运行如下:
abc
mbc

结论:当调用string的replace()操作时也会新建一个指定内存区域并赋值,不在原有的value进行赋值

一道笔试题

================================

public class StringExer {
    String str = new String("good");
    char[] ch = {'t', 'e', 's', 't'};
    public void change(String str, char ch[]) {
        str = "test ok";
        ch[0] = 'b';
    }
    public static void main(String[] args) {
        StringExer ex = new StringExer();
        ex.change(ex.str, ex.ch);
        System.out.println(ex.str);
        System.out.println(ex.ch);
    }
}

那么当我们运行起来的时候,会输出什么呢?输出:good、best

原 str 的引用地址的内容并没有变,方法里的str = “test ok” ,其实是字符串常量池中的另一个区域(地址),将它进行赋值操作给str但并没有修改原来 str 指向的引用地址里的内容

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

字符串常量池怎么保证不会存储相同内容的?

================================

因为String Pool(字符串常量池)是一个固定大小的Hashtable,默认值大小长度是1009

JDK6中StringTable是固定的就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快,StringTablesize设置没有要求

JDK7中,StringTable的长度默认值是60013

JDK8中,StringTable的长度默认值是60013,StringTable可以设置的最小值为1009

如果放进String Pool的String非常多就会造成Hash冲突严重,从而导致链表会很长而链表长了后直接会造成的影响就是当调用String.intern()方法时性能会大幅下降

所以扩充StringTable的长度,使用-XX:StringTablesize可设置StringTable的长度

我们使用一个示例来体会一下不同版本的默认长度是多少

public static void main(String[] args) {
    //测试StringTablesize参数
    System.out.println("我来打个酱油");
    try {
        Thread.sleep(1000000);
    }catch (InterruptedException e){
        e. printStackTrace();
    }
}
JDK 6环境下的大小设置:

在这里插入图片描述

这时将项目跑起来,在打开cmd命令窗口查看一下是否已被修改为:10的大小
在这里插入图片描述

JDK 7环境下的大小设置:

在这里插入图片描述

这时将项目跑起来,在打开cmd命令窗口查看一下大小是多少
在这里插入图片描述

接下来我们再测试不同大小长度的速度是怎么样的,先生成10万个长度不超过10的字符串

public static void main(String[] args) throws IOException {
    FileWriter fw =  new FileWriter("words.txt");
    for (int i = 0; i < 100000; i++) {
        //1 - 10
       int length = (int)(Math.random() * (10 - 1 + 1) + 1);
        fw.write(getString(length) + "\n");
    }
    fw.close();
}

public static String getString(int length){
    String str = "";
    for (int i = 0; i < length; i++) {
        //65 - 90, 97-122
        int num = (int)(Math.random() * (90 - 65 + 1) + 65) + (int)(Math.random() * 2) * 32;
        str += (char)num;
    }
    return str;
}

接下来我们根据设置不同大小看看,将这10万个长度不超过10的字符串读取看看效率怎么样
在这里插入图片描述

花费时间:143ms

在这里插入图片描述

花费时间:47ms

二、String的内存分配


那么String结构主要放在哪呢?其实我们在前几篇有提到过这个放的位置

那么我们再出于完整性的考虑并且例子来说明确实是这样的结构当中

首先我们说在Java语言中有8种基本数据类型1种比较特殊的类型String

那么这九种那个唯独可以放在一起呢?那就是常量池使得它们更快更节省内存等

常量池就类似一个Java系统级别提供的缓存,8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种

  • 直接使用双引号声明出来的String对象会直接存储在常量池中。
  • 如果不是用双引号声明的String对象,可以使用String提供的intern()方法。这

Java 6 及以前,字符串常量池存放在永久代
在这里插入图片描述

Java 7中 Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内

所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了

字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String.intern()。
在这里插入图片描述

Java8元空间,字符串常量在堆

在这里插入图片描述

我们可以使用示例来体会一下字符串常量池爆出OOM的不同情况

public class StringTest3 {
    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());
        }
    }
}
JDK 6环境运行下在永久代中:

在这里插入图片描述

Exception in thread "main" java.lang.outOfMemoryError: PermGen space
at java.lang.string.intern(Native Method)
at com.atguigu.java.stringTest3.main(StringTest3. java:22)
JDK 8环境运行下在堆中:

在这里插入图片描述

运行结果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.HashMap.resize(HashMap.java:703)
    at java.util.HashMap.putVal(HashMap.java:662)
    at java.util.HashMap.put(HashMap.java:611)
    at java.util.HashSet.add(HashSet.java:219)
    at com.atguigu.java.StringTest3.main(StringTest3.java:22)
那么为什么要调整字符串常量池的位置?

================================

首先永久代的默认空间大小比较小并且垃圾回收频率低,大量的字符串无法及时回收,容易进行Full GC产生STW或者容易产生OOM:PermGen Space

而堆中空间足够大,字符串可被及时回收

在JDK 7中,interned字符串不再在Java堆的永久代中分配,而是在Java堆的主要部分(称为年轻代和年老代)中分配,与应用程序创建的其他对象一起分配

此更改将导致驻留在主Java堆中的数据更多,驻留在永久生成中的数据更少,因此可能需要调整堆大小

三、String的基本操作


我们来看一个代码示例,按照我们之前说的特性,后面同样的字符串则不会再生成

 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("6");
    System.out.println("7");
    System.out.println("8");
    System.out.println("9");
    System.out.println("10");
    //如下的字符串"1" 到 "10"不会再次加载
    System.out.println("1");
    System.out.println("2");
    System.out.println("3");
    System.out.println("4");
    System.out.println("5");
    System.out.println("6");
    System.out.println("7");
    System.out.println("8");
    System.out.println("9");
    System.out.println("10");
}

那么我们使用debug运行起来代码,看看真的是这样吗?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例

那么接下来我们再看一个列子,当我们运行起来一起分析main方法与foo方法各指向的位置

//官方示例代码
class Memory {
    public static void main(String[] args) {//line 1
        int i = 1;//line 2
        Object obj = new Object();//line 3
        Memory mem = new Memory();//line 4
        mem.foo(obj);//line 5
    }//line 9

    private void foo(Object param) {//line 6
        String str = param.toString();//line 7
        System.out.println(str);
    }//line 8
}

在这里插入图片描述

四、字符串拼接操作


我们先来看看以下代码的拼接操作,若进行匹配的话结果是什么呢?

public void test1(){
    String s1 = "a" + "b" + "c";
    String s2 = "abc";
    System.out.println(s1 == s2);
    System.out.println(s1.equals(s2));
}

结果是输出两个true、为什么呢?我们先将它进行编译一起看看.class文件内容是什么

public void test1(){
    String s1 = "abc";
    String s2 = "abc";
    System.out.println(s1 == s2);
    System.out.println(s1.equals(s2));
}

我们发现常量与常量的拼接的话,它们的结果在常量池原理是编译优化将s1等同于"abc"

同时我们也可以看看该字节码是怎么回事
在这里插入图片描述

从字节码指令看出:编译器做了优化,将 “a” + “b” + “c” 优化成了 “abc”

接下来在看看下一个示例代码

public void test2(){

    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);
    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);
    
    String s8 = s6.intern();
    System.out.println(s3 == s8);
}

那么当代码运行起来后运行结果为:true、false、false、false、false、false、false、false、

那么为什么会这样呢?我们和上面一样将它编译并且看看.class文件内容

public void test2(){

    String s1 = "javaEE";
    String s2 = "hadoop";
    String s3 = "javaEEhadoop";
    String s4 = "javaEEhadoop";
    String s5 = s1 + "hadoop";
    String s6 = "javaEE" + 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);
    
    String s8 = s6.intern();
    System.out.println(s3 == s8);
}

我们发现s4 常量与常量的拼接的话,会进行编译优化就连接成一起了

如果拼接符号的前后出现了变量则相当于在堆空间中new String(),具体的内容为拼接的结果:javaEEhadoop(s5、s6、s7出现)所以此时再进行比较的时候,结果就会为false

那么我们发现s8 = s6.intern(),那么它是什么情况呢?

如果拼接的结果调用intern()方法,根据该字符串是否在常量池中存在,分为:

  • 如果存在,则返回字符串在常量池中的地址
  • 如果字符串常量池中不存在该字符串,则在常量池中创建一份,并返回此对象的地址

而我们s6 拼接符号的前后面出现了变量则堆空间中new String(),所以字符串常量池不存在

接下来在看看下一个示例代码

public void test3(){
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    System.out.println(s3 == s4);
}

那么当代码运行起来后运行结果为:false

那么为什么会这样呢?我们一起运行分析一下字节码看看
在这里插入图片描述

我们发现s4 = s1 + s2 的时候第九行操作指令创建StringBuilder并进行了实例化赋默认值

将局部变量表索引为1的a、索引为2的ab进行append。我们可以使用临时代码展示这个操作

public void test3(){
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    /*
    如下的s1 + s2 的执行细节:(变量s是我临时定义的)
    ① StringBuilder s = new StringBuilder();
    ② s.append("a")
    ③ s.append("b")
    ④ s.toString()  --> 约等于 new String("ab"),但不等价
    补充:在jdk5.0之后使用的是StringBuilder,在jdk5.0之前使用的是StringBuffer
     */
    System.out.println(s3 == s4);
}

结论:拼接前后只要其中有一个是变量结果就在堆中。变量拼接的原理是StringBuilder

接下来在看看下一个示例代码

public void test4(){
    final String s1 = "a";
    final String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    System.out.println(s3 == s4);
}

那么当代码运行起来后运行结果为:true

那么为什么会这样呢?不是说相当于StringBuilder新建存储在堆中吗?让我们一起来分析分析
在这里插入图片描述
如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder的方式

针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final的时候建议使用上

从字节码角度来看:为变量 s4 赋值时,直接使用 #16 符号引用,即字符串常量 “ab”

接下来我们使用一个拼接操作与append 操作的效率进行对比

public void test6(){

    long start = System.currentTimeMillis();
    method1(100000);//4014
    long end = System.currentTimeMillis();
    System.out.println("花费的时间为:" + (end - start));
}

public void method1(int highLevel){
    String src = "";
    for(int i = 0;i < highLevel;i++){
        src = src + "a";//每次循环都会创建一个StringBuilder、String
    }
}
//运行结果如下:
花费的时间:4014
public void test6(){

    long start = System.currentTimeMillis();
    method2(100000);
    long end = System.currentTimeMillis();
    System.out.println("花费的时间为:" + (end - start));
}
public void method2(int highLevel){
    //只需要创建一个StringBuilder
    StringBuilder src = new StringBuilder();
    for (int i = 0; i < highLevel; i++) {
        src.append("a");
    }
}
//运行结果如下:
花费的时间:7

经过输出花费的时间我们可以体会执行效率:通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式!

原因是因为StringBuilder的append()的方式:自始至终中只创建过一个StringBuilder的对象

那么对于使用String的字符串拼接方式有不足呢:

  • 创建过多个StringBuilder和String(调的toString方法)的对象,内存占用更大;
  • 如果进行GC,需要花费额外的时间(在拼接的过程中产生的一些中间字符串可能永远也用不到,会产生大量垃圾字符串)。

五、intern()的使用


我们先看看intern在String类是什么怎么描述的呢?
在这里插入图片描述
我们看图就可以知道intern是一个native方法,调用的是底层C的方法

字符串常量池池最初是空的,由String类私有地维护。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串内容相等的字符串,则返回池中的字符串地址。否则,该字符串对象将被添加到池中,并返回对该字符串对象的地址。(这是源码里的大概翻译)

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

String myInfo = new string("I love you").intern();

我们说new String()存放在堆空间,那么就会在堆空间创建"I love you “同时去常量池判断是否有这个"I love you”,若不存在则将当前字符串放入常量池中同时返回地址给myInfo

这样的话也就说如果在任意字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true

("a"+"b"+"c").intern()=="abc"

通俗点讲interned 对于String就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意:这个值不存在会创建并存放在字符串内部池(String Intern Pool)

关于 new String() 的说明

================================

下面我们看看这个问题:new String(“ab”)会创建几个对象?

根据我们前面的思路,我们还直接观看字节码吧,看看到底做了些什么事情

  • new #2 :在堆中创建了一个 String 对象
  • ldc #3 :在字符串常量池中放入 “ab”(如果之前字符串常量池中没有 “ab” 的话)
    在这里插入图片描述

下面我们看看这个问题:new String(“a”) + new String(“b”) 会创建几个对象?

根据我们前面的思路,我们还直接观看字节码吧,看看到底做了些什么事情

  • new #2 :拼接字符串会创建一个 StringBuilder 对象
  • new #4 :创建 String 对象,对应于 new String(“a”)
  • ldc #5 :在字符串常量池中放入 “a”(如果之前字符串常量池中没有 “a” 的话)
  • new #4 :创建 String 对象,对应于 new String(“b”)
  • ldc #8 :在字符串常量池中放入 “b”(如果之前字符串常量池中没有 “b” 的话)
  • invokevirtual #9 :调用 StringBuilder 的 toString() 方法,会生成一个 String 对象
    在这里插入图片描述
    在这里插入图片描述

有了前面两道题做基础,我们接下来看看一道比较难的题目看看创建了几个对象

public class StringIntern {
    public static void main(String[] args) {
        String s = new String("1");
        s.intern();
        String s2 = "1";
        System.out.println(s == s2);
        String s3 = new String("1") + new String("1");
        s3.intern();
        String s4 = "11";
        System.out.println(s3 == s4);
    }
}

我们在JDK6的环境下运行看看输出结果是什么
在这里插入图片描述
我们运行起来,发现结果是运行结果:false、false

我们在JDK7的环境下运行看看输出结果是什么
在这里插入图片描述
我们运行起来,发现结果是运行结果:false、true

目前我们的环境是JDk7,这时我们编译看看main方法的字节码是怎么样的
在这里插入图片描述

而刚刚我们前面的第二个问题铺垫时就说到过new String() + new String()的问题解析

那么我们就分析一下s3.intern()是做了什么事情呢?

在前面new String(“1”) + new String(“1”)的时候会调用 StringBuilder 的 toString() 方法,会生成一个 String 对象为"11",但我们前面提到过在字符串常量池中并没有生成

所以当我们执行s3.intern()的时候

字符串常量池没有s3的"11",所以创建一个指向堆空间new String(“11”)的地址

执行s4 = “11"的时候常量池里有"11”,所以就会使用s3.intern()的那个指向地址,所以s3 == s4 为true

那么我们一起看看这个解释的思路图吧(JDk7 环境)
在这里插入图片描述

那么在JDK6的思路图当中就不一样,我们一起来看看
在这里插入图片描述
在JDK6当中字符串常量池并没有在堆空间,所以它会在常量池生成一个新的对象"11"并且有新的地址

所以当在JDk6中的s3 与 s4 ,它们的引用地址各不同所以s3 == s4 为false

接下来我们根据这个特性再扩展一下,看下面的代码块输出结果是什么呢?

public class StringIntern1 {
    public static void main(String[] args) {
        String s3 = new String("1") + new String("1");
        String s4 = "11";  
        String s5 = s3.intern();
        System.out.println(s3 == s4);
    }
}

我们运行起来,发现结果是运行结果:false

那么根据前面的题目分析,我们知道执行new String(“1”) + new String(“1”)

字符串常量池中并不会存储"11",当执行s4 = "11"才会在字符串常量池中存在

而s5 = s3.intern()其实是在常量池寻找是否有"11",若有则返回指向地址给到s5

此时s3的"11"是存储在堆空间当中的,但s4的"11"是存储在字符串常量池中,所以为false

小结intern()

================================

JDK 1.6中,将这个字符串对象尝试放入串池。
  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
  • 如果没有,会把此对象复制一份,放入串池,并返回串池中的新对象地址
Jdk1.7起,将这个字符串对象尝试放入串池。
  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
  • 如果没有,则会把对象的引用地址复制一份放入串池,并返回串池中的引用地址
使用intern()测试执行效率

================================

接下来我们测试一下intern()进行执行一下效率

public class StringIntern2 {
    static final int MAX_COUNT = 1000 * 10000;
    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,9,10};

        long start = System.currentTimeMillis();
        for (int i = 0; i < MAX_COUNT; i++) {
           arr[i] = new String(String.valueOf(data[i % data.length]));
        }
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为:" + (end - start));

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.gc();
    }
}
//运行结果如下:
花费时间:7307

并且我们通过Java VisualVm工具看看这段代码怎么样呢?
在这里插入图片描述

我们再看看使用intern()执行同样的需求,看看它的花费时间是多少呢?

public class StringIntern2 {
    static final int MAX_COUNT = 1000 * 10000;
    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,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();
    }
}
//运行结果如下:
花费时间:1311

并且我们通过Java VisualVm工具看看这段代码怎么样呢?
在这里插入图片描述

由此可见我们可以对比两个操作

直接 new String :每个 String 对象都是 new 出来的,所以程序需要维护大量存放在堆空间中的 String 实例,程序内存占用也会变高

使用 intern() 方法:由于数组中字符串的引用都指向字符串常量池中的字符串,所以程序需要维护的 String 对象更少,内存占用也更低

结论:

对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用intern()方法能够节省很大的内存空间。

大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用intern() 方法,就会很明显降低内存的大小

六、String Table的垃圾回收


我们刚刚演示intern()的执行效率也证明了String 存在垃圾回收,所以试用intern()时更省

接下来我们再通过下面的代码块来体会一下String的垃圾回收

public class StringGCTest {
    public static void main(String[] args) {
        for (int j = 0; j < 100000; j++) {
            String.valueOf(j).intern();
        }
    }
}

使用命令:-Xma15m- -Xnx15m XX:+PrintStringTabIeStat1stIcs -XX:+PrintGCDetaiis查看字符串常量池的信息
在这里插入图片描述

我们将循环的操作先注释掉,看看未循环添加时的字符串常量池是怎么样的
在这里插入图片描述

这时我们再运行起来看看,进行循环后的字符串常量池是怎么样的
在这里插入图片描述

若我们将for循环的次数增加到十万的话,再运行起来是怎么样的呢?
在这里插入图片描述

由十万的输出结果我们就可以知道StringTable 区发生了垃圾回收

  • 在 PSYoungGen 区发生了垃圾回收
  • Number of entries 和 Number of literals 明显没有 100000

七、G1的String去重操作


对于G1中对于String有去除重复的操作,具体详细可查看官方文档:访问入口

许多大规模的Java应用的瓶颈在于内存,测试表明在这些类型的应用里面,Java堆中存活的数据集合差不多25%是String对象。更进一步,这里面差不多一半String对象是重复的,重复的意思是说:str1.equals(str2)= true。

堆上存在重复的String对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就能避免浪费内存

观看官方文档可以知道在我们的应用中一般的堆空间里

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

比如说一下代码块,体会一下:

Strnig str1 =new String("hello");    
Strnig str2 =new String("hello");

那么对于这种情况,我们G1是怎么操作的呢?

当垃圾收集器工作的时候会访问堆上存活的对象。对每一个访问的对象都会检查是否它是候选的要去重的String对象如果是:把这个对象的一个引用插入到队列中等待后续的处理

这时有一个去重的线程在后台运行处理这个队列。处理队列的时候把元素从队列删除这个元素,然后尝试去引用已有一样的String对象

使用一个Hashtable来记录所有的被String对象使用的不重复的char数组。当去重的时候会查这个Hashtable,来看堆上是否已经存在一个一模一样的char数组

如果存在String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。如果查找失败,char数组会被插入到Hashtable,这样以后的时候就可以共享这个数组了

提示:暂时了解一下,后面会详解垃圾回收器

对于去重的命令选项如下:

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

参考资料


尚硅谷:JVM虚拟机(宋红康老师)

下面是本人的公众号:(有兴趣可以扫一下,文章会同步过去)
在这里插入图片描述

我是小白弟弟,一个在互联网行业的小白,立志成为一名架构师
https://blog.csdn.net/zhouhengzhe?t=1

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhz小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值