本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
Java关于字符串的优化学习
程序猿小军 · 134浏览 · 发布于2023-07-10 +关注

String 对象是我们使用最频繁的一个对象类型,但它的性能问题却是最容易被忽略的。String 对象作为 Java 语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地使用字符串,可以提升系统的整体性能。

String 存储变化

随着 Java 版本的更迭,工程师们对 String 对象做了大量的优化,来节省存储空间。



1、在 Java6 以及之前的版本中,String 对象是对 char 数组进行了封装实现的对象,主要有四个成员变量:char 数组、偏移量 offset、字符数量 count、哈希值 hash。String 对象是通过 offset 和 count 两个属性来定位 char[]数组,获取字符串。这么做可以高效、快速地共享数组对象,同时节省内存空间,但这种方式很有可能会导致内存泄漏。

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
  ...
}

2、 从 Java7 版本开始到 Java8 版本,Java 对 String 类做了一些改变。String 类中不再有 offset 和 count 两个变量了。这样的好处是 String 对象占用的内存稍微少了些,同时,String.substring 方法也不再共享 char[],从而解决了使用该方法可能导致的内存泄漏问题。

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

  /** Cache the hash code for the string */
  private int hash; // Default to 0

  .....
}

3、从 Java9 版本开始,工程师将 char[]字段改为了 byte[]字段,又维护了一个新的属性 coder,它是一个编码格式的标识。

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

  private final byte coder;

  /** Cache the hash code for the string */
  private int hash; // Default to 0
  
  .....
}

至于为何要这样改?我们知道一个 char 字符占 16 位,2 个字节。这个情况下,存储单字节编码内的字符(占一个字节的字符)就显得非常浪费。JDK1.9 的 String 类为了节约内存空间,于是使用了占 8 位,1 个字节的 byte 数组来存放字符串。

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

public char[] toCharArray() {
  return isLatin1() ? StringLatin1.toChars(value)
    : StringUTF16.toChars(value);
}

private boolean isLatin1() {
  return COMPACT_STRINGS && coder == LATIN1;
}

static final byte LATIN1 = 0;
static final byte UTF16  = 1;

以前学习 String 源码时,只关注了 JDK8 的源码,对于 JDK9 的改动没有了解,既然把 char[] 改为了 byte[] 数组,那么我第一时间就想去了解 String 是如何存储汉字的?

关于这部分内容可以参考网友的这篇文章《Java中如何存储汉字》,通俗易懂。

String学习

关于字符串的优化,首先需要学习字符串常量池和 String 源码,在此基础上,非常容易理解字符串的优化。

这里推荐几篇我之前写的文章:

Java 中方法区与常量池

Java 基础:String 类源码分析

Java 基础:String——常量池与 intern

String优化

1、构建超长字符串

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();
}

不过平时做字符串拼接时,尽量使用 StringBuilder,如果涉及到线程安全,则推荐使用 StringBuffer。

2、使用 String.intern 节省内存

关于 String.intern 的详细讲解可以参考 Java 基础:String——常量池与 intern。

这里只提一下何时使用 String.intern 可以节省内存,我们通过一个简单的示例代码来看一下:


通过 new 关键字创建 String 对象时,不管字符串常量池中是否存在“hresh”值的引用,都会在堆中创建一个值为 “hresh” 的对象,如果不调用 intern 方法,则 name 指向的是 o1 对象的引用,而非字符串常量池中关于 o2 对象的引用。如果调用了 intern 方法,则 copyName2 指向的是字符串常量池中关于 o2 对象的引用。如下图所示:

因为 o3 对象没有引用指向它,在未来的垃圾回收中会被标记,直到被回收。

那么有人可能会问,如果 copyName2 指向 o3,并且没有 copyName1 变量,那么 o2 对象是否会被回收呢?

个人理解如下:首先字符串常量池是有大小限制的,在不超限的前提下,是不会清除字符串常量池中的无用引用,那么 o2 对象就不会被垃圾回收。如果字符串常量池满了,那么可能会检查是否有变量指向该引用,如果没有则可能回收 o2 对象。

关于 intern 方法的使用注意事项:字符串常量池类似于于一个存在引用的链表,如果放进来的引用非常多,就会造成 hash 冲突,导致链表过长,当调用 String#intern() 时会需要到链表上一个一个找,从而导致性能大幅度下降。

3、慎用 Split()方法

Split() 方法使用了正则表达式实现了其强大的分割功能,而正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题,很可能导致 CPU 居高不下。我们可以用 String.indexOf() 方法代替 Split() 方法完成字符串的分割。

关于正则表达式为何会引起回溯问题,我们接下来详细介绍。

扩展

有这样一个问题:Java中的String有没有长度限制?

在 String 源码分析中,关于其构造方法的实现比较多,其中有几个是支持用户传入 length 来执行长度的:

public String(byte bytes[], int offset, int length)

可以看到,这里面的参数 length 是使用 int 类型定义的,那么也就是说,String 定义的时候,最大支持的长度就是 int 的最大范围值。

根据 Integer 类的定义,java.lang.Integer#MAX_VALUE的最大值是2^31 – 1;

那么,我们是不是就可以认为 String 能支持的最大长度就是这个值了呢?

其实并不是,这个值只是在运行期,我们构造 String 的时候可以支持的一个最大长度,而实际上,在运行期,定义字符串的时候也是有长度限制的。

如以下代码:

String s = "11111...1111";//其中有10万个字符"1"

当我们使用如上形式定义一个字符串的时候,当我们执行 javac 编译时,是会抛出异常的,提示如下:


那么,明明 String 的构造函数指定的长度是可以支持2147483647(2^31 – 1)的,为什么像以上形式定义的时候无法编译呢?

常量池限制

原因在于: 形如String s = "xxx";定义String的时候,xxx被我们称之为字面量,这种字面量在编译之后会以常量的形式进入到 Class 文件常量池。 要进入常量池,就要遵守常量池的约束条件。

Class 文件常量池的格式规定:其字符串常量的长度不能超过65535( 2^16 – 1 ), 当参数类型为 String,并且长度大于等于 65535 的时候,就会导致编译失败。

运行期限制

首先我们查看以下代码:

错误: 常量字符串过长

在运行期,长度不能超过 Int 的范围,否则会抛异常。

推荐阅读:String长度限制

总结

关于字符串的内容学习已经写了好几篇文章了,直到本篇文章结束,才感觉差不多掌握了。对一个小小的字符串了解不够深入,使用不够恰当,很可能引发线上事故。

Java 自身在迭代过程中通过不断地更改成员变量,节约内存空间,对 String 对象进行优化。我们有必要深入学习一遍 String 对象,理解其背后的知识后,使用时才会得心应手。

相关推荐

PHP实现部分字符隐藏

沙雕mars · 1325浏览 · 2019-04-28 09:47:56
Java中ArrayList和LinkedList区别

kenrry1992 · 908浏览 · 2019-05-08 21:14:54
Tomcat 下载及安装配置

manongba · 970浏览 · 2019-05-13 21:03:56
JAVA变量介绍

manongba · 963浏览 · 2019-05-13 21:05:52
什么是SpringBoot

iamitnan · 1086浏览 · 2019-05-14 22:20:36
加载中

0评论

评论
我是来自差了一点掉完头发的程序猿,小军,希望在这里可以向各位大佬们学习。
分类专栏
小鸟云服务器
扫码进入手机网页