JVM-StringTable(三)

一、常量池与串池StringTable的关系

public class StringTableStudy {
	public static void main(String[] args) {
		String s1 = "a";
		String s2 = "b";
		String s3 = "ab";
		String s4 = "b";
		String s5 = "ab";
	}
}

将上述代码先编译成字节码,再反编译,得到如下虚拟机指令(部分截图):
常量池:
在这里插入图片描述

方法内的指令:
在这里插入图片描述
局部变量表:
在这里插入图片描述

StringTable在数据结构上,是一个哈希表,下面介绍一下指令执行时,运行时常量池和StringTable的工作关系:

  1. 由前面的内容可以知道,常量池存在于字节码文件,当该类被加载,它的常量池信息就会放入运行时常量池。此时a b ab仍旧是运行常量池种的符号,还没变为java字符串对象。
  2. 程序计数器记下0,开始执行code下的指令" ldc #2",执行到 ldc #2时,会根据#2去运行常量池中找到a,并创建出a的字符串对象,然后存入StingTable(串池)。【此时StringTable内有[“a”]】
  3. 程序计数器记下2,开始执行“astore_1”,将a字符串对象存入LocalVariableTable(方法栈内局部变量)的s1对象。【此时LocalVariableTable内有[s1]】
  4. 同理的创建s2,s3对象时也是走2、3步骤一样。【此时,StringTable内有[“a”,“b”,“ab”],LocalVariableTable内有[s1,s2,s3]】
  5. 执行到“ 9: ldc #3”创建s4对象时,先根据#3找到b,然后发现StringTable已经存在"a"的字符串对象,无需再创建,就直接将s4引用到其上面。同理,s5与s4步骤一样。

常量池与串池StringTable的关系:在常量池中的字符串常量,会被创建并存入StringTable内,若StringTable内已经存在了,就直接使用无需再重新创建。

注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。

二、拼接字符串变量对象

public class HelloWorld {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4=s1+s2;//new StringBuilder().append("a").append("2").toString()  new String("ab")
        System.out.println(s3==s4);//false
//结果为false,因为s3是存在于串池之中,s4是由StringBuffer的toString方法所返回的一个对象,存在于堆内存之中
    }
}

输出结果为:false
用反编译来解释:
在这里插入图片描述
通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()
最后的toString方法的返回值是一个新的字符串
,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中.

三、拼接字符串常量对象

public class HelloWorld {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4=s1+s2;//new StringBuilder().a|ppend("a").append("2").toString()  new String("ab")
        String s5="a"+"b";
        System.out.println(s5==s3);//true
    }
}

输出结果为:true
反编译解释:
在这里插入图片描述

  • 使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以s5直接从串池中获取值,所以进行的操作和 s3= “ab” 一致
  • 使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuilder来创建

四、intern方法 JDK1.8

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,则放入成功

  • 如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象
例子1:
在这里插入图片描述
例子2:

public class HelloWorld {

    public static void main(String[] args) {
        String x = "ab";
        String s = new String("a") + new String("b");

        String s2 = s.intern();//将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,这两种情况都会把串池中的对象返回
        System.out.println(s2 == x);//true
        System.out.println(s == x);//false
    }
}


五、intern方法 JDK1.6

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中
  • 如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象

六、面试题 JDK1.8

package com.itcast.itheima.xpp;

public class main {
    public static void main(String[] args) {

        String s1="a";
        String s2="b";
        String s3="a"+"b";
        String s4=s1+s2;
        String s5="ab";
        String s6=s4.intern();
        System.out.println(s3==s4);//false
        System.out.println(s3==s5);//true
        System.out.println(s3==s6);//true

        String x2=new String("c")+new String("d");
        String x1="cd";
        x2.intern();
        System.out.println(x1==x2);//false


        String x4=new String("e")+new String("f");
        x4.intern();
        String x3="ef";
        System.out.println(x3==x4);//true

    }
}

七、 StringTable 位置

在这里插入图片描述
JDK1.6 时,StringTable是属于常量池的一部分。
JDK1.8 以后,StringTable是放在堆中的。

验证JDK1.8的StringTable:

/**
 * 演示 StringTable 位置
 * 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
 * 在jdk6下设置 -XX:MaxPermSize=10m
 */
public class Demo1_6 {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
            for (int j = 0; j < 260000; j++) {
                list.add(String.valueOf(j).intern());
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

在这里插入图片描述
在这里插入图片描述
验证JDK1.6的StringTable内存:
在这里插入图片描述

八、StringTable 垃圾回收

StringTable在内存紧张时,会发生垃圾回收。

演示案例:

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

设置虚拟机参数:-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
堆内存为10M,输出StringTable信息,打印GC信息。
设置i为100,输出结果:
在这里插入图片描述
没有发生GC,且StringTable statistics这栏显示着Stringtable的信息。

设置i为10000,输出结果:
在这里插入图片描述
在这里插入图片描述
发生了GC,而且Stringtabl的信息没有增加,说明达到了最大值。

九、StringTable 性能调优

调优的两个方案:

  • 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
  • 考虑是否将字符串对象入池,存在堆中的字符串对象可以重复的,但可以通过intern方法入池减少重复,保证相同的地址在StringTable中只存储一份

9.1 演示增加桶的个数提升性能

/**
 * 演示串池大小对性能的影响
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 */
public class Demo1_24 {

    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null) {
                    break;
                }
                line.intern();
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
        }
    }
}

linux.words文件含很多字符串,程序将读取这些字符串,并存入串池中,最后打印需要的时间。

(1)设置虚拟机参数:堆大小为500M,且打印StringTable信息
-Xmx:设置JVM最大可用内存为500M。
-Xms:设置JVM促使内存为500M。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
在这里插入图片描述
运行结果:
在这里插入图片描述

在这里插入图片描述
未指定StringTable大小时,StringTable的桶个数默认为60013,入池的总时间为300。

(2)追加虚拟机参数,指定StringTable的大小为1009
在这里插入图片描述
输出结果:
在这里插入图片描述
在这里插入图片描述
入池时间耗费8453,桶个数1009。

显然,Stringtable越小,入池时间越慢,反之越大。

9.2 演示通过入池减少内存消耗

/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {

    public static void main(String[] args) throws IOException {

        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line);//强引用,防止垃圾回收
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

由代码可以见,重复读取文件内容。
运行代码后,通过 jvisualvm工具查看内存占用情况如下:
在这里插入图片描述
修改代码,做入池操作

address.add(line);//强引用,防止垃圾回收

重新运行,再查看内存情况:
在这里插入图片描述
显然,添加了入池操作,能减少内存空间消耗。因为重复的字符串对象不会重新保存到串池中。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值