03 | 字符串性能优化不容小觑,百M内存轻松存储几十G数据

一、前言

1、String 对象作为 Java 语言中重要的数据类型,是内存中占据空间最大的一个对象

二、String 对象是如何实现的

1、从 Java7 版本开始到 Java8 版本

A、String 类中不再有 offset 和 count 两个变量了。这样的好处是 String 对象占用的内存稍微少了些,

B、同时,String.substring 方法也不再共享 char[],从而解决了使用该方法可能导致的内存泄漏问题。

2、从 Java9 版本开始,

A、将 char[]字段改为了 byte[]字段。一个 char 字符占 16 位,2 个字节。这个情况下,存储单字节编码内的字符(占一个字节的字符)就显得非常浪费。1 个字节的 byte 数组来存放字符串节约内存空间。

B、coder,它是一个编码格式的标识,在计算字符串长度或者使用 indexOf()函数时,我们需要根据这个字段,判断如何计算字符串长度。coder 属性默认有 0 和 1 两个值,0 代表 Latin-1(单字节编码),1 代表 UTF-16。如果 String 判断字符串只包含了 Latin-1,则 coder 属性值为 0,反之则为 1。

三、String 对象的不可变性

1、在实现代码中 String 类被 final 关键字修饰了

2、变量 char 数组也被 final 修饰了

3、代表了 String 对象不可被更改,不可变性,即 String 对象一旦创建成功,就不能再对它进行改变

4、为什么要这么做?

A、保证 String 对象的安全性。

B、,保证 hash 属性值不会频繁变更,确保了唯一性,使得类似 HashMap 容器才能实现相应的 key-value 缓存功能。

C、可以实现字符串常量池,通常有两种创建字符串对象的方式

a、一种是通过字符串常量的方式创建,如 String str=“abc”

a1、JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建

a2、这种方式可以减少同一个值的字符串对象的重复创建,节约内存

b、另一种是字符串变量通过 new 形式的创建,如 String str = new String(“abc”)

b1、首先在编译类文件时,"abc"常量字符串将会放入到常量结构

b2、在类加载时,“abc"将会在常量池中创建

b3、其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的"abc” 字符串,在堆内存中创建一个 String 对象

b4、最后,str 将引用 String 对象

4、什么是对象和对象引用

A、对一个 String 对象 str 赋值“hello”,然后又让 str 值为“world”,这个时候 str 的值变成了“world”。那么 str 值确实改变了,为什么我还说 String 对象不可变呢?

a、第一次赋值的时候,创建了一个“hello”对象,str 引用指向“hello”地址

b、第二次赋值的时候,又重新创建了一个对象“world”,str 引用指向了“world”

c、但“hello”对象依然存在于内存中。

B、原理
a、对象在内存中是一块内存地址,str 则是一个指向该内存地址的引用,并不是对象本身

b、真正的对象hello依然还在内存中,保持着String的不可变性。

四、String对象的优化

1、如何构建超大字符串

A、编译器会自动优化代码例子1


String str= "ab" + "cd" + "ef";

String str= "abcdef";

a、将第一个的代码优化为第二个,避免了在创建字符串对象时创建多个字符串对象占用内存空间 

B、编译器优化代码例子2


String str = "abcdef";

for(int i=0; i<1000; i++) {
      str = str + i;
}


String str = "abcdef";

for(int i=0; i<1000; i++) {
            str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}

a 、Java 在进行字符串的拼接时,偏向使用 StringBuilder,这样可以提高程序的效率

b、但是,每次循环都会生成一个新的 StringBuilder 实例,同样也会降低系统的性能。

c、所以,建议平时做字符串拼接的时候,要显示地使用 String Builder 来提升系统性能。

d、如果在多线程编程中,String 对象的拼接涉及到线程安全,你可以使用 StringBuffer

但由于 StringBuffer 是线程安全的,涉及到锁竞争,所以从性能上来说,要比 StringBuilder 差一些。

C、怎么使用SpringBuilder

a、创建 StringBuilder 对象

StringBuilder sb = new StringBuilder();

 b、初始化一个初始字符串

StringBuilder sb = new StringBuilder("initial string");

c、添加字符串

sb.append("hello");
sb.append(" ");
sb.append("world");

 d、替换字符串

sb.replace(0, 5, "hi");

以上代码将替换从索引 0 到索引 4 的字符为字符串 "hi"

e、 删除字符串

sb.delete(0, 3);

以上代码将删除从索引 0 到索引 2 的字符 

f、获取字符串

String result = sb.toString();

以上代码将返回 StringBuilder 对象中的字符串 

D、怎么使用SpringBuffer

a、提供了一个线程安全的环形缓冲区实现,可以用来存储任意类型的对象

b、创建一个 SpringBuffer 对象,指定缓冲区的容量大小和元素类型

SpringBuffer<Integer> buffer = new SpringBuffer<>(10, Integer.class);

c、向缓冲区中添加元素:(add

buffer.add(1);
buffer.add(2);
buffer.add(3);

 d、从缓冲区中读取元素:(get)

Integer value = buffer.get();

e、如果缓冲区已满,则添加元素时会自动覆盖最早添加的元素

f、如果缓冲区为空,则从缓冲区中读取元素时会阻塞当前线程,直到有元素可读,如果你不想让当前线程一直阻塞,可以设置超时时间,当超时时间到达时,会抛出 TimeoutException 异常:

try {
    Integer value = buffer.get(500, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
    System.out.println("Timeout!");
}

2、如何使用 String.intern 节省内存

A、在每次赋值的时候使用 String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,这样一开始创建的哪些没被引用的对象就会被回收掉


String a =new String("abc").intern();
String b = new String("abc").intern();
        
if(a==b) {
    System.out.print("a==b");
}

 输出结果


a==b

a、创建 a 变量时,调用 new Sting() 会在堆内存中创建一个 String 对象,String 对象中的 char 数组将会引用常量池中字符串

b、在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用

c、创建 b 变量时,调用 new Sting() 会在堆内存中创建一个 String 对象,String 对象中的 char 数组将会引用常量池中字符串,在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用

d、而在堆内存中的两个对象,由于没有引用指向它,将会被垃圾回收。所以 a 和 b 引用的是同一个对象。

B、原理

a、在字符串常量中,默认会将对象放入常量池(即使用上面的第一种方法创建的字符串);

b、在字符串变量中,对象是会创建在堆内存中(即使用上面的new方法),同时也会在常量池中创建一个字符串对象,String 对象中的 char 数组将会引用常量池中的 char 数组,并返回堆内存对象引用

c、如果调用 intern 方法,会去查看字符串常量池中是否有等于该对象的字符串的引用,如果没有,把首次遇到的字符串的引用添加到常量池中;如果有,就返回常量池中的字符串引用

C、String 字符串的创建分配内存地址情况

使用 intern 方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担

3、如何使用字符串的分割方法?

A、Split() 方法使用了正则表达式实现了其强大的分割功能,而正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题,很可能导致 CPU 居高不下。

B、可以用 String.indexOf() 方法代替 Split() 方法完成字符串的分割

五、其他

1、使用字符串常量池,Java可以在内存中存储所有相同值的String对象的单个实例,而不是每次都创建一个新的对象,由于String对象是不可变的,所以可以确保字符串常量池中的对象不会被修改,保持了常量池里的值的唯一性

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一点知趣

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

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

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

打赏作者

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

抵扣说明:

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

余额充值