admin管理员组

文章数量:1579086

文章目录

  • Java 编程优化
    • 一、String 字符串优化
      • 1、String 对象的实现
      • 2、String 对象的不可变性
      • 3、String 对象的优化
        • 1、如何构建超大字符串?
        • 2、如何使用String.intern 节省内存?
        • 3. 如何使用字符串的分割方法?
    • 二、慎重使用正则表达式
      • 1、什么是正则表达式?
      • 2、正则表达式引擎
        • 1. 贪婪模式(Greedy)
        • 2. 懒惰模式(Reluctant)
        • 3. 独占模式(Possessive)
      • 3、正则表达式的优化
        • 1.少用贪婪模式,多用独占模式
        • 2.减少分支选择
        • 3.减少捕获嵌套
      • 总结

Java 编程优化

一、String 字符串优化

String 对象作为 Java 语言中重要的数据类型,是内存中占据空间大的一个对象。高效地使用字符串,可以提升系统的整体性能。

首先来看一个小问题:判断下列代码的输出,以及这么输出的原因是什么?

public static void main(String[] args) {
    String str1= "abc";
    String str2= new String("abc");
    String str3= str2.intern();
    System.out.println(str1==str2);
    System.out.println(str2==str3);
    System.out.println(str1==str3);
}

你可以带着自己的答案(或者疑惑)来学习接下来的内容。

1、String 对象的实现

在 Java 语言中,Sun 公司的工程师们对 String 对象做了大量的优化,来节约内存空间,提升 String 对象在系统中的性能。

  1. 在 Java6 以及之前的版本中,String 对象是对 char[] 进行了封装实现的对象,主要有四个成员变量:char 数组、偏移量 offset、字符数量 count、哈希值 hash。

    String 对象是通过 offset 和 count 两个属性来定位 char[] 数组,获取字符串。这么做可以高效、快速地共享数组对象,同时节省内存空间,但这种方式很有可能会导致内存泄漏。

  2. 从 Java7 版本开始到 Java8 版本,Java 对 String 类做了一些改变。String 类中不再有 offset 和 count 两个变量了。

    这样的好处是 String 对象占用的内存稍微少了些,同时, String.substring() 方法也不再共享 char[](如果共享可能导致该字符串不再被使用但却无法被GC掉),从而解决了使用该方法可能导致的内存泄漏问题。

  3. 从 Java9 版本开始,工程师将 char[] (2字节)字段改为了 byte[] (1字节)字段,又维护了一个新的属性 coder,它是一个编码格式的标识(因为外国人主要使用英文字母+各种标点,使用1字节足以表示,其他文字就通过coder标识)。

工程师为什么这样修改呢?

我们知道一个 char 字符占 16 位,2 个字节。这个情况下,存储单字节编码内的字符(占一个字节的字符)就显得非常浪费。JDK1.9 的 String 类为了节约内存空间,于是使用了占8 位,1 个字节的 byte 数组来存放字符串。而新属性 coder 的作用是,在计算字符串长度或者使用 indexOf() 函数时,我们需要根据这个字段,判断如何计算字符串长度。coder 属性默认有 0 和 1 两个值,0 代表 Latin1(单字节编码),1 代表 UTF-16。如果 String 判断字符串只包含了 Latin-1,则 coder 属性值为 0,反之则为 1。

2、String 对象的不可变性

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    ...
}

在实现代码中 String 类被 final 关键字修饰了,而且变量 char 数组也被 final 修饰了。

我们知道类被 final 修饰代表该类不可继承,而 char[] 被 final+private 修饰,代表了String 对象不可被更改。Java 实现的这个特性叫作 String 对象的不可变性,即 String 对象一旦创建成功,就不能再对它进行改变。

Java 这样做的好处在哪里呢?

  • 保证 String 对象的安全性。假设 String 对象是可变的,那么 String 对象将可能被恶意修改。
  • 保证 hash 属性值不会频繁变更,确保了唯一性,使得类似 HashMap 容器才能实现相应的 key-value 缓存功能。
  • 可以实现字符串常量池。在 Java 中,通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如 String str=“abc”;另一种是字符串变量通过 new 形式的创建,如 String str = new String(“abc”)。
    • 当代码中使用第一种方式创建字符串对象时,JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存
    • String str = new String(“abc”) 这种方式,首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,“abc"将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的"abc” 字符串,在堆内存中创建一个 String 对象,str 将引用 String 对象

3、String 对象的优化

1、如何构建超大字符串?

字符串常量

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

经过编译器自动优化后:

String str = "abcdef";

字符串变量

上面的代码编译后,你可以看到编译器同样对这段代码进行了优化。不难发现,Java 在进行字符串的拼接时,偏向使用 StringBuilder,这样可以提高程序的效率。

综上已知: 即使使用 + 号作为字符串的拼接,也一样可以被编译器优化成 StringBuilder 的方式。但再细致些,你会发现在编译器优化的代码中,每次循环都会生成一个新的StringBuilder 实例,同样也会降低系统的性能。

结论: 做字符串拼接的时候,要显示地使用 String Builder 来提升系统性能。

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

Twitter 每次发布消息状态的时候,都会产生一个地址信息,以当时 Twitter 用户的规模预估,服务器需要 32G 的内存来存储地址信息。

public class Location {
    private String city;
    private String region;
    private String countryCode;
    private double longitude;
    private double latitude;
} 

考虑到其中有很多用户在地址信息上是有重合的,比如,国家、省份、城市等,这时就可以将这部分信息单独列出一个类,以减少重复。

public class SharedLocation {
    private String city;
    private String region;
    private String countryCode;
}

public class Location {
    private SharedLocation sharedLocation;
    double longitude;
    double latitude;
}

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

SharedLocation sharedLocation = new SharedLocation();
sharedLocation.setCity(messageInfo.getCity().intern());  	
sharedLocation.setRegion(messageInfo.getCountryCode().intern());

Location location = new Location();
location.set(sharedLocation);
location.set(messageInfo.getLongitude());
location.set(messageInfo.getLatitude());

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

注意:在jdk7/8以后,字符串常量池是在堆中,这很关键!

你可以在jdk6和jdk8中运行以下代码。

public static void main(String[] args) {
    String str1 = new String("a") + new String("bc");
    str1.intern();
    String str2 = "abc";
    System.out.println(str1 == str2);
}

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

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

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

所以我们应该慎重使用 Split() 方法,我们可以用 String.indexOf() 方法代替 Split() 方法完成字符串的分割。如果实在无法满足需求,你就在使用 Split() 方法时,对回溯问题加以重视就可以了。

二、慎重使用正则表达式

String 对象优化时,提到了 Split() 方法,该方法使用的正则表达式可能引起回溯问题,今天我们就来深入了解下,这究竟是怎么回事?

1、什么是正则表达式?

正则表达式是计算机科学的一个概念,很多语言都实现了它。正则表达式使用一些特定的元字符来检索、匹配以及替换符合规则的字符串。

构造正则表达式语法的元字符,由普通字符、标准字符、限定字符(量词)、定位字符(边界字符)组成。

2、正则表达式引擎

正则表达式是一个用正则符号写出的公式,程序对这个公式进行语法分析,建立一个语法分析树,再根据这个分析树结合正则表达式的引擎生成执行程序(这个执行程序我们把它称作状态机,也叫状态自动机),用于字符匹配。而这里的正则表达式引擎就是一套核心算法,用于建立状态机。

目前实现正则表达式引擎的方式有两种:DFA 自动机(Deterministic Final Automata 确定有限状态自动机)和 NFA 自动机(Non deterministic Finite Automaton 非确定有限状态自动机)。

NFA 自动机的回溯

用 NFA 自动机实现的比较复杂的正则表达式,在匹配过程中经常会引起回溯问题。大量的回溯会长时间地占用 CPU,从而带来系统性能开销。

text=“abbc” 
regex=“ab{1,3}c”

NFA 自动机对其解析的过程是这样的:

首先,读取正则表达式第一个匹配符 a 和字符串第一个字符 a 进行比较,a 对 a,匹配。

然后,读取正则表达式第二个匹配符 b{1,3} 和字符串的第二个字符 b 进行比较,匹配。但因为 b{1,3} 表示 1-3 个 b 字符串,NFA 自动机又具有贪婪特性,所以此时不会继续读取正则表达式的下一个匹配符,而是依旧使用 b{1,3} 和字符串的第三个字符 b 进行比较,结果还是匹配。

接着继续使用 b{1,3} 和字符串的第四个字符 c 进行比较,发现不匹配了,此时就会发生回溯,已经读取的字符串第四个字符 c 将被吐出去,指针回到第三个字符 b 的位置。

那么发生回溯以后,匹配过程怎么继续呢?程序会读取正则表达式的下一个匹配符 c,和字符串中的第四个字符 c 进行比较,结果匹配,结束。

如何避免回溯问题?

既然回溯会给系统带来性能开销,那我们如何应对呢?如果你有仔细看上面那个案例的话,你会发现 NFA 自动机的贪婪特性就是导火索,这和正则表达式的匹配模式息息相关,一起来了解一下。

1. 贪婪模式(Greedy)

顾名思义,就是在数量匹配中,如果单独使用 +、 ? 、* 或{min,max} 等量词,正则表达式会匹配尽可能多的内容。

例如,上边那个例子:

text=“abbc” 
regex=“ab{1,3}c”

就是在贪婪模式下,NFA 自动机读取了 大的匹配范围,即匹配 3 个 b 字符。匹配发生了一次失败,就引起了一次回溯。如果匹配结果是“abbbc”,就会匹配成功。

2. 懒惰模式(Reluctant)

在该模式下,正则表达式会尽可能少地重复匹配字符。如果匹配成功,它会继续匹配剩余的字符串。

例如,在上面例子的字符后面加一个“?”,就可以开启懒惰模式。

text=“abc” 
regex=“ab{1,3}?c”

匹配结果是“abc”,该模式下 NFA 自动机首先选择 小的匹配范围,即匹配 1 个 b 字符,因此就避免了回溯问题。

3. 独占模式(Possessive)

同贪婪模式一样,独占模式一样会 大限度地匹配更多内容;不同的是,在独占模式下,匹配失败就会结束匹配,不会发生回溯问题。

还是上边的例子,在字符后面加一个“+”,就可以开启独占模式。

text=“abbc” 
regex=“ab{1,3}+bc”

结果是不匹配,结束匹配,不会发生回溯问题。

3、正则表达式的优化

1.少用贪婪模式,多用独占模式
2.减少分支选择

分支选择类型“(X|Y|Z)”的正则表达式会降低性能,我们在开发的时候要尽量减少使用。如果一定要用,我们可以通过以下几种方式来优化:

  • 首先,我们需要考虑选择的顺序,将比较常用的选择项放在前面,使它们可以较快地被匹配;
  • 其次,我们可以尝试提取共用模式,例如,将“(abcd|abef)”替换为“ab(cd|ef)”,后者匹配速度较快,因为 NFA 自动机会尝试匹配 ab,如果没有找到,就不会再尝试任何选项;
  • 最后,如果是简单的分支选择类型,我们可以用三次 index 代替“(X|Y|Z)”,如果测试的话,你就会发现三次 index 的效率要比“(X|Y|Z)”高出一些。
3.减少捕获嵌套

总结

正则表达式虽然小,却有着强大的匹配功能。我们经常用到它,比如,注册页面手机号或邮箱的校验。

如果使用正则表达式能使你的代码简洁方便,那么在做好性能排查的前提下,可以去使用;如果不能,那么正则表达式能不用就不用,以此避免造成更多的性能问题。

本文标签: 性能Java