JVM字符串相关(上)

字符串相关的操作对我们平时的开发是很重要的,这次就从底层出发讲讲字符串相关内容。

String基本特性

创建字符串的方式

大致分为两种,一种是直接加双引号,一种是使用new关键字,代码如下

String s1="hello bb";
String s2="hi baobao";

类相关

请看如下代码

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
//.......
}

final
String类是不可继承的,因为加了final关键字,且人家已经写的很完美了,无需我们扩展。

Serializable
实现了序列化接口,所以在一些远程传输方面的序列化与反序列化方面也是支持的。

Comparable
实现了比较接口,让其可以根据一定的规则进行排序,或者比较字符串序列。

总结三点

  • 不可继承
  • 序列化
  • 可比较

不可变性

从三个角度来证实字符串的不可变性

1.字符串的重新赋值
重新赋值,不会修改原先内存区域的value,而是新创建一个value

public class Demo1 {
    public static void main(String[] args) {
        String s1 = "bb";
        String s2 = "bb";	
	System.out.println(s1 == s2);//true
        s1 = "hello";
        System.out.println(s1 == s2);//false
    }
}

其实这个例子并不明显,首先我们要知道,s1被赋值了bb,那么就会被放入常量池中,由于常量池中不会有相同的常量,所以s1,s2指向的是同一个常量,而s1被赋值为hello,对原有的bb是不会进行修改的,只不过多了一个常量,如下图所示。image.png

2.字符串连接操作

public class Demo2 {
    public static void main(String[] args) {
        String s1 = "bb111";
        String s2 = "bb";
        String s3 = s2 + "111";
        System.out.println(s1 == s3);//false
    }
}

对已有的字符串的拼接操作,也需要重新指定内存区域赋值,不能对原value进行修改,简单来讲,拼接后相当于创建了新的对象,如图所示。image.png

3.字符串替换操作

public class Demo3 {
    public static void main(String[] args) {
        String s1 = "bb111";
        String s2 = "bb222";
        String s3 = s2.replace("2", "1");
        System.out.println(s3);//bb111
        System.out.println(s1 == s3);//false
    }
}

对字符串进行替换操作,内容上来讲是一样的,但是得到的结果是false,原因还在于s3也是新创建的对象,与常量池对象做比较,地址不同,自然是不一样的,如图所示。

image.png

字符串在jdk9的改变

private final char value[];//jdk8

 private final byte[] value;//jdk9
 private final byte coder; //jdk9

我截取了部分代码,可以看到8我们用的是char数组,而到了9用byte数组加上coder编码,为什么要这样呢?直接贴官方解释。image.png

总结下,原因就在于官方语言是英语,一般都是占1个字节的空间,使用char会造成空间浪费,而如果我们要使用中文,需要修改coder变量的标志位为UTF-16。

不存储相同字符串

重点内容!!!!!

注意,是字符串常量池,而不是字符串本身。

字符串常量池的底层是用HashTable实现的,类似于HashMap,是由数组+链表的形式,所以说不允许重复,并且内部维护了一个默认大小的StringTable,如果字符串常量过多,就很容易造成hash冲突,使得链表过长,效率变低。

jdk7和7dk8的时候,默认长度为60013,但jdk8有一点不同,长度最小为1009,但jdk7是没有限制的。

字符串内存分配

字符串内部维护了一个常量池,只有两种方法能够使得字符串进入常量池。

  • 直接使用字面量赋值操作
  • 使用intern()方法

在jdk6及以前,字符串常量池是放在永久代的,但是jdk7及以后就去掉了永久代,统一将字符串常量池放入堆空间中了,我们从原因分析,因为永久代的调优比起堆来说要困难,且gc的频率也很低,容易造成字符串的堆积,这么一想确实应该放在堆中。

案例分析

下面是对象创建的过程,试试能不能动手画一下内存结构图呢

public class Memory {
    public static void main(String[] args) {
        int i = 1;
        Object obj = new Object();
        Memory mem = new Memory();
        mem.foo(obj);
    }

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

图示如下,最终str指向堆空间的对象,因为没有使用字面量或者intern,所以不会放入常量池中,这点需要注意。

image.png

字符串拼接的情况

对于字符串拼接的,我总结了以下四种情况image.png

案例1

public class Demo1 {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "a" + "b" + "c";
        System.out.println(s1==s2);//true
    }
}

结果是true,因为字符串在编译期间进行了优化,这样的常量拼接就相当于直接定义"abc"。

案例2

public class Demo2 {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "a";
        String s3 = s2 + "bc";
        System.out.println(s1==s2);//false
    }
}

之前其实也提到过,现在拿出来在说一下,内部细节是这样的。

  1. 先new StringBuilder()对象
  2. StringBuilder对象.append(”a“) 追加a字符串
  3. StringBuilder对象.append(”bc“) 追加bc字符串
  4. StringBuilder对象.toString() 转化为字符串

最终的结果约等于创建了新的对象,所以返回false。

案例3

public class Demo3 {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = new String("abc");
        System.out.println(s1 == s2);//false
        s2 = s2.intern();
        System.out.println(s1 == s2);//true
    }
}

可以看到,第一次比较,两个对象的地址是不相同的,但是后面执行了intern方法,会先判断下abc是否存放在常量池,没有就放入,有的话就重新指向。

案例4

public class Demo4 {
    public static void main(String[] args) {
        String s1 = "abc";
        final String s2 = "a";
        final String s3 = "bc";
        String s4 = s2 + s3;
        System.out.println(s1 == s4);//true
        //其实就是等价于
        //s4="a"+"bc";
    }
}

我们可以看到,s4变量有一个拼接的操作,但是最终得到的结果竟是和s1相等的,也就是指向了同一常量,因为final变量会在编译期就进行优化,就相当于常量的拼接。

这里还需要给出一点小建议,我们在平时开发的时候,对于那些不会改变的变量和类,建议加上final,这使得一些代码在编译期就确定下来,也算是一个小优化吧。

测试拼接与追加操作

出于测试方便,我引入了hutool的包,依赖如下

<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.6.3</version>
 </dependency>

定义两个方法,各执行拼接操作十万次

public class Demo8 {
    public static void main(String[] args) {
        f1();
        f2();
        f3();
    }

    public static void f1() {
        TimeInterval timer = DateUtil.timer();
        String s = "";
        for (int i = 0; i < 100000; i++) {
            s += "b";
        }
        long interval = timer.interval();//花费毫秒数
        System.out.println("字符串拼接花费时间:"+interval);
    }

    public static void f2() {
        TimeInterval timer = DateUtil.timer();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 100000; i++) {
            sb.append("b");
        }
        long interval = timer.interval();//花费毫秒数
        System.out.println("sb append花费时间:"+interval);
    }

}

测试结果如下

字符串拼接花费时间:2897
sb append花费时间:4

可以看到,使用append操作能够大大提高效率,原因在于字符串的拼接在循环体中会频繁创建StringBuilder 对象的,大部分时间其实是创建对象导致的。

小优化

当然我们还可以优化,如果我们在确定了拼接对象大概需要多少容量,就可以提前指定,代码如下,这次拼接一百万次。

public static void f2() {
        TimeInterval timer = DateUtil.timer();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 1000000; i++) {
            sb.append("b");
        }
        long interval = timer.interval();//花费毫秒数
        System.out.println("sb append花费时间:"+interval);
    }
	public static void f3() {
        TimeInterval timer = DateUtil.timer();
        StringBuilder sb = new StringBuilder(1000000);
        for (int i = 0; i < 1000000; i++) {
            sb.append("b");
        }
        long interval = timer.interval();//花费毫秒数
        System.out.println("sb优化 append花费时间:"+interval);
}

结果如下,效果还是有一些的。

sb append花费时间:26
sb优化 append花费时间:12
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值