admin管理员组

文章数量:1579374

各文章各专题涉及到还原实践中的场景,原理,方法,模型,代码,原则,设计等;精心打造系列分享,阅读者仔细了解,必定有所收获,也可以收藏,日后工作中参考。

线程与JVM调优专题:设计模式调优-性能设计沉思录(10)_luozhonghua2000的博客-CSDN博客

分库分表中间件专题:分片proxy验证-shardingsphere源码解读(5)_luozhonghua2000的博客-CSDN博客

工具方法篇:内存持续上升排查方法-性能设计沉思录(3)_luozhonghua2000的博客-CSDN博客

 本文涉及9部分的前3部分内容理论和实践:

      性能测试工具,字符串性能优化,正则表达式性能优化,ArrayList还是LinkedList?Stream性能优化,HashMap优化,如何解决高并发下I/O瓶颈?避免使用Java序列化,如何优化RPC网络通信?深入了解NIO的优化实现原理

部分1:性能测试工具

熟练掌握一款性能测试工具,是我们必备的一项技能。他不仅可以帮助我们模拟测试场景 (包括并发、复杂的组合场景),还能将测试结果转化成数据或图形,帮助我们更直观地了 解系统性能。

常用的性能测试工具

对于开发人员来说,首选是一些开源免费的性能(压力)测试软件,例如 ab(ApacheBench)、JMeter 等;对于专业的测试团队来说,付费版的 LoadRunner 是首选。当然,也有很多公司是自行开发了一套量身定做的性能测试软件,优点是定制化强, 缺点则是通用性差。

接下来,我会为你重点介绍 ab 和 JMeter 两款测试工具的特点以及常规的使用方法。

1.ab

ab 测试工具是 Apache 提供的一款测试工具,具有简单易上手的特点,在测试 Web 服务 时非常实用。 ab 可以在 Windows 系统中使用,也可以在 Linux 系统中使用。这里我说下在 Linux 系统 中的安装方法,非常简单,只需要在 Linux 系统中输入 yum-y install httpd-tools 命令, 就可以了。

安装成功后,输入 ab 命令,可以看到以下提示:

ab 工具用来测试 post get 接口请求非常便捷,可以通过参数指定请求数、并发数、请求 参数等。例如,一个测试并发用户数为 10、请求数量为 100 的的 post 请求输入如下:

ab -n 100 -c 10 -p 'post.txt' -T 'application/x-www-form-urlencoded' 'http://test.api"

post.txt 为存放 post 参数的文档,存储格式如下:

usernanme=test&password=test&sex=1

附上几个常用参数的含义:

-n:总请求次数(最小默认为 1);

-c:并发次数(最小默认为 1 且不能大于总请求次数,例如:10 个请求,10 个并发, 实际就是 1 人请求 1 次);

-p:post 参数文档路径(-p 和 -T 参数要配合使用);

-T:header 头内容类型(此处切记是大写英文字母 T)。

当我们测试一个 get 请求接口时,可以直接在链接的后面带上请求的参数:

ab -c 10 -n 100 http://www.test.api/test/login?userName=test&password=test

输出结果如下:

以上输出中,有几项性能指标可以提供给你参考使用:

Requests per second:吞吐率,指某个并发用户数下单位时间内处理的请求数;

Time per request:上面的是用户平均请求等待时间,指处理完成所有请求数所花费的 时间 /(总请求数 / 并发用户数);

Time per request:下面的是服务器平均请求处理时间,指处理完成所有请求数所花费 的时间 / 总请求数;

Percentage of the requests served within a certain time:每秒请求时间分布情况, 指在整个请求中,每个请求的时间长度的分布情况,例如有 50% 的请求响应在 8ms 内,66% 的请求响应在 10ms 内,说明有 16% 的请求在 8ms~10ms 之间

2.JMeter

 JMeter 是 Apache 提供的一款功能性比较全的性能测试工具,同样可以在 Windows 和 Linux 环境下安装使用。

JMeter 在 Windows 环境下使用了图形界面,可以通过图形界面来编写测试用例,具有易 学和易操作的特点。

JMeter 不仅可以实现简单的并发性能测试,还可以实现复杂的宏基准测试。我们可以通过 录制脚本的方式,在 JMeter 实现整个业务流程的测试。JMeter 也支持通过 csv 文件导入 参数变量,实现用多样化的参数测试系统性能。

Windows 下的 JMeter 安装非常简单,在官网下载安装包,解压后即可使用。如果你需要 打开图形化界面,那就进入到 bin 目录下,找到 jmeter.bat 文件,双击运行该文件就可以 了。

 JMeter 的功能非常全面,我在这里简单介绍下如何录制测试脚本,并使用 JMeter 测试业 务的性能。 录制 JMeter 脚本的方法有很多,一种是使用 Jmeter 自身的代理录制,另一种是使用 Badboy 这款软件录制,还有一种是我下面要讲的,通过安装浏览器插件的方式实现脚本的 录制,这种方式非常简单,不用做任何设置。 首先我们安装一个录制测试脚本的插件,叫做 BlazeMeter 插件。你可以在 Chrome 应用 商店中找到它,然后点击安装, 如图所示:

然后使用谷歌账号登录这款插件,如果不登录,我们将无法生成 JMeter 文件,安装以及登 录成功后的界面如下图所示:

最后点击开始,就可以录制脚本了。录制成功后,点击保存为 JMX 文件,我们就可以通过 JMeter 打开这个文件,看到录制的脚本了,如下图所示

这个时候,我们还需要创建一个查看结果树,用来可视化查看运行的性能结果集合

 设置好结果树之后,我们可以对线程组的并发用户数以及循环调用次数进行设置:

 设置成功之后,点击运行,我们可以看到运行的结果:

 JMeter 的测试结果与 ab 的测试结果的指标参数差不多,这里我就不再重复讲解了。

3.LoadRunner

LoadRunner 是一款商业版的测试工具,并且 License 的售价不低。 作为一款专业的性能测试工具,LoadRunner 在性能压测时,表现得非常稳定和高效。相 比 JMeter,LoadRunner 可以模拟出不同的内网 IP 地址,通过分配不同的 IP 地址给测试 的用户,模拟真实环境下的用户。这里我就不展开详述了

总结

三种常用的性能测试工具就介绍完了,最后我把今天的主要内容为你总结了一张图。

现在测试工具非常多,包括阿里云的 PTS 测试工具也很好用,但每款测试工具其实都有自 己的优缺点。个人建议,还是在熟练掌握其中一款测试工具的前提下,再去探索其他测试工 具的使用方法会更好

部分2:字符串性能优化

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

接下来我们就从 String 对象的实现、特性以及实际使用中的优化这三个方面入手,深入了 解

在开始之前,我想先问你一个小问题,也是我在招聘时,经常会问到面试者的一道题。虽是 老生常谈了,但错误率依然很高,当然也有一些面试者答对了,但能解释清楚答案背后原理 的人少之又少。问题如下: 通过三种不同的方式创建了三个对象,再依次两两匹配,每组被匹配的两个对象是否相等? 代码如下:

String str1= "abc"; String str2= new String("abc"); String str3= str2.intern();

assertSame(str1==str2); assertSame(str2==str3); assertSame(str1==str3)

你可以先想想答案,以及这样回答的原因。希望通过今天的学习,你能拿到满分。

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[],从而解决了使用该方法可能导致的内存泄漏问 题。

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

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

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

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

String 对象的不可变性

了解了 String 对象的实现后,你有没有发现在实现代码中 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 对象。

这里附上一个你可能会想到的经典反例。

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

首先,我来解释下什么是对象和对象引用。Java 初学者往往对此存在误区,特别是一些从 PHP 转 Java 的同学。在 Java 中要比较两个对象是否相等,往往是用 ==,而要判断两个 对象的值是否相等,则需要用 equals 方法来判断。

这是因为 str 只是 String 对象的引用,并不是对象本身。对象在内存中是一块内存地址, str 则是一个指向该内存地址的引用。所以在刚刚我们说的这个例子中,第一次赋值的时 候,创建了一个“hello”对象,str 引用指向“hello”地址;第二次赋值的时候,又重新 创建了一个对象“world”,str 引用指向了“world”,但“hello”对象依然存在于内存 中。

也就是说 str 并不是对象,而只是一个对象引用。真正的对象依然还在内存中,没有被改 变

String 对象的优化

了解了 String 对象的实现原理和特性,接下来我们就结合实际场景,看看如何优化 String 对象的使用,优化的过程中又有哪些需要注意的地方。

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

编程过程中,字符串的拼接很常见。前面我讲过 String 对象是不可变的,如果我们使用 String 对象相加,拼接我们想要的字符串,是不是就会产生多个对象呢?例如以下代码:

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

分析代码可知:首先会生成 ab 对象,再生成 abcd 对象,最后生成 abcdef 对象,从理论 上来说,这段代码是低效的。

但实际运行中,我们发现只有一个对象生成,这是为什么呢?难道我们的理论判断错了?我 们再来看编译后的代码,你会发现编译器自动优化了这行代码,如下:

String str= "abcdef";

上面我介绍的是字符串常量的累计,我们再来看看字符串变量的累计又是怎样的呢?

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

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

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

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

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

如果在多线程编程中,String 对象的拼接涉及到线程安全,你可以使用 StringBuffer。但 是要注意,由于 StringBuffer 是线程安全的,涉及到锁竞争,所以从性能上来说,要比 StringBuilder 差一些。

扩展多线程:上下文切换调优-性能设计沉思录(9)_luozhonghua2000的博客-CSDN博客

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

讲完了构建字符串,我们再来讨论下 String 对象的存储问题。先看一个案例。

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

通过优化,数据存储大小减到了 20G 左右。但对于内存存储这个数据来说,依然很大,怎 么办呢?

这个案例来自一位 Twitter 工程师在 QCon 全球软件开发大会上的演讲,他们想到的解决 方法,就是使用 String.intern 来节省内存空间,从而优化 String 对象的存储。

具体做法就是,在每次赋值的时候使用 String 的 intern 方法,如果常量池中有相同值,就 会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。这种方式可以使重 复性非常高的地址信息存储大小从 20G 降到几百兆。

SharedLocation sharedLocation = new SharedLocation(); sharedLocation.setCity(messageInfo.getCity().intern()); sharedLocation.setCount sharedLocation.setRegion(messageInfo.getCountryCode().intern()); Location location = new Location(); location.set(sharedLocation); location.set(messageInfo.getLongitude()); location.set(messageInfo.getLatitude());

为了更好地理解,我们再来通过一个简单的例子,回顾下其中的原理:

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

输出结果:a==b

在字符串常量中,默认会将对象放入常量池;在字符串变量中,对象是会创建在堆内存中, 同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。

了解了原理,我们再一起看看上边的例子。

在一开始创建 a 变量时,会在堆内存中创建一个对象,同时会在加载类时,在常量池中创 建一个字符串对象,在调用 intern 方法之后,会去常量池中查找是否有等于该字符串的对 象,有就返回引用。

在创建 b 字符串变量时,也会在堆中创建一个对象,此时常量池中有该字符串对象,就不 再创建。调用 intern 方法则会去常量池中判断是否有等于该字符串的对象,发现有等 于"abc"字符串的对象,就直接返回引用。而在堆内存中的对象,由于没有引用指向它,将 会被垃圾回收。所以 a 和 b 引用的是同一个对象。

下面我用一张图来总结下 String 字符串的创建分配内存地址情况:

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

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

最后我想跟你聊聊字符串的分割,这种方法在编码中也很最常见。Split() 方法使用了正则 表达式实现了其强大的分割功能,而正则表达式的性能是非常不稳定的,使用不恰当会引起 回溯问题,很可能导致 CPU 居高不下。 所以我们应该慎重使用 Split() 方法,我们可以用 String.indexOf() 方法代替 Split() 方法完 成字符串的分割。如果实在无法满足需求,你就在使用 Split() 方法时,对回溯问题加以重 视就可以了。

总结

这一讲中,我们认识到做好 String 字符串性能优化,可以提高系统的整体性能。在这个理 论基础上,Java 版本在迭代中通过不断地更改成员变量,节约内存空间,对 String 对象进 行优化。 我们还特别提到了 String 对象的不可变性,正是这个特性实现了字符串常量池,通过减少 同一个值的字符串对象的重复创建,进一步节约内存。 但也是因为这个特性,我们在做长字符串拼接时,需要显示使用 StringBuilder,以提高字 符串的拼接性能。最后,在优化方面,我们还可以使用 intern 方法,让变量字符串对象重 复使用常量池中相同值的对象,进而节约内存。 最后再分享一个个人观点。那就是千里之堤,溃于蚁穴。日常编程中,我们往往可能就是对 一个小小的字符串了解不够深入,使用不够恰当,从而引发线上事故

部分3:慎重使用正则表达式

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

开始之前,我们先来看一个案例,可以帮助你更好地理解内容。

在一次小型项目开发中,我遇到过这样一个问题。为了宣传新品,我们开发了一个小程序, 按照之前评估的访问量,这次活动预计参与用户量 30W+,TPS(每秒事务处理量)最高 3000 左右

这个结果来自我对接口做的微基准性能测试。我习惯使用 ab 工具(通过 yum -y install httpd-tools 可以快速安装)在另一台机器上对 http 请求接口进行测试。

我可以通过设置 -n 请求数 /-c 并发用户数来模拟线上的峰值请求,再通过 TPS、RT(每秒 响应时间)以及每秒请求时间分布情况这三个指标来衡量接口的性能,如下图所示(图中隐 藏部分为我的服务器地址):

就在做性能测试的时候,我发现有一个提交接口的 TPS 一直上不去,按理说这个业务非常 简单,存在性能瓶颈的可能性并不大。

我迅速使用了排除法查找问题。首先将方法里面的业务代码全部注释,留一个空方法在这 里,再看性能如何。这种方式能够很好地区分是框架性能问题,还是业务代码性能问题。

我快速定位到了是业务代码问题,就马上逐一查看代码查找原因。我将插入数据库操作代码 加上之后,TPS 稍微下降了,但还是没有找到原因。最后,就只剩下 Split() 方法操作了, 果然,我将 Split() 方法加入之后,TPS 明显下降了。

可是一个 Split() 方法为什么会影响到 TPS 呢?下面我们就来了解下正则表达式的相关内 容,学完了答案也就出来了。

什么是正则表达式?

很基础,这里带你简单回顾一下。 正则表达式是计算机科学的一个概念,很多语言都实现了它。正则表达式使用一些特定的元 字符来检索、匹配以及替换符合规则的字符串。 构造正则表达式语法的元字符,由普通字符、标准字符、限定字符(量词)、定位字符(边 界字符)组成。详情可见下图:

正则表达式引擎

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

而这里的正则表达式引擎就是一套核心算法,用于建立状态机。

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

对比来看,构造 DFA 自动机的代价远大于 NFA 自动机,但 DFA 自动机的执行效率高于 NFA 自动机。

假设一个字符串的长度是 n,如果用 DFA 自动机作为正则表达式引擎,则匹配的时间复杂 度为 O(n);如果用 NFA 自动机作为正则表达式引擎,由于 NFA 自动机在匹配过程中存在 大量的分支和回溯,假设 NFA 的状态数为 s,则该匹配算法的时间复杂度为 O(ns)。

NFA 自动机的优势是支持更多功能。例如,捕获 group、环视、占有优先量词等高级功 能。这些功能都是基于子表达式独立进行匹配,因此在编程语言里,使用的正则表达式库都 是基于 NFA 实现的。

那么 NFA 自动机到底是怎么进行匹配的呢?我以下面的字符和表达式来举例说明。

text=“aabcab” regex=“bc”

NFA 自动机会读取正则表达式的每一个字符,拿去和目标字符串匹配,匹配成功就换正则 表达式的下一个字符,反之就继续和目标字符串的下一个字符进行匹配。分解一下过程。

首先,读取正则表达式的第一个匹配符和字符串的第一个字符进行比较,b 对 a,不匹配; 继续换字符串的下一个字符,也是 a,不匹配;继续换下一个,是 b,匹配

然后,同理,读取正则表达式的第二个匹配符和字符串的第四个字符进行比较,c 对 c,匹 配;继续读取正则表达式的下一个字符,然而后面已经没有可匹配的字符了,结束。

这就是 NFA 自动机的匹配过程,虽然在实际应用中,碰到的正则表达式都要比这复杂,但 匹配方法是一样的

NFA 自动机的回溯

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

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

这个例子,匹配目的比较简单。匹配以 a 开头,以 c 结尾,中间有 1-3 个 b 字符的字符 串。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”,就会匹配成功。

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

2. 懒惰模式(Reluctant)

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

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

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

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

3. 独占模式(Possessive)

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

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

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

结果是不匹配,结束匹配,不会发生回溯问题。讲到这里,你应该非常清楚了,避免回溯的 方法就是:使用懒惰模式和独占模式。

还有开头那道“一个 split() 方法为什么会影响到 TPS”的存疑,你应该也清楚了吧?

我使用了 split() 方法提取域名,并检查请求参数是否符合规定。split() 在匹配分组时遇到 特殊字符产生了大量回溯,我当时是在正则表达式后加了一个需要匹配的字符和“+”,解 决了这个问题。

\\?(([A-Za-z0-9-~_=%]++\\&{0,1})+)

正则表达式的优化

正则表达式带来的性能问题,给我敲了个警钟,在这里我也希望分享给你一些心得。任何一 个细节问题,都有可能导致性能问题,而这背后折射出来的是我们对这项技术的了解不够透彻。所以我鼓励你学习性能调优,要掌握方法论,学会透过现象看本质。下面我就总结几种 正则表达式的优化方法给你。

1. 少用贪婪模式,多用独占模式

贪婪模式会引起回溯问题,我们可以使用独占模式来避免回溯。前面详解过了,这里我就不 再解释

2. 减少分支选择

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

首先,我们需要考虑选择的顺序,将比较常用的选择项放在前面,使它们可以较快地被匹 配;

其次,我们可以尝试提取共用模式,例如,将“(abcd|abef)”替换为“ab(cd|ef)”,后者 匹配速度较快,因为 NFA 自动机会尝试匹配 ab,如果没有找到,就不会再尝试任何选 项;

最后,如果是简单的分支选择类型,我们可以用三次 index 代替“(X|Y|Z)”,如果测试的 话,你就会发现三次 index 的效率要比“(X|Y|Z)”高出一些。

3. 减少捕获嵌套

在讲这个方法之前,我先简单介绍下什么是捕获组和非捕获组。

捕获组是指把正则表达式中,子表达式匹配的内容保存到以数字编号或显式命名的数组中, 方便后面引用。一般一个 () 就是一个捕获组,捕获组可以进行嵌套。

非捕获组则是指参与匹配却不进行分组编号的捕获组,其表达式一般由(?:exp)组成。

在正则表达式中,每个捕获组都有一个编号,编号 0 代表整个匹配到的内容。我们可以看 下面的例子:

public static void main( String[] args ){
String text = "<input high=\"20\" weight=\"70\">test</input>";
String reg="(<input.*?>)(.*?)(</input>)";
Pattern p = Patternpile(reg);
Matcher m = p.matcher(text);
while(m.find()) {
System.out.println(m.group(0));// 整个匹配到的内容
System.out.println(m.group(1));//(<input.*?>)
System.out.println(m.group(2));//(.*?)
System.out.println(m.group(3));//(</input>)
}
}

运行结果:

<input high=\"20\" weight=\"70\">test</input>
<input high=\"20\" weight=\"70\">
test
</input>

如果你并不需要获取某一个分组内的文本,那么就使用非捕获分组。例如,使 用“(?:X)”代替“(X)”,我们再看下面的例子:

public static void main( String[] args )
{
String text = "<input high=\"20\" weight=\"70\">test</input>";
String reg="(?:<input.*?>)(.*?)(?:</input>)";
Pattern p = Patternpile(reg);
Matcher m = p.matcher(text);
while(m.find()) {
System.out.println(m.group(0));// 整个匹配到的内容
System.out.println(m.group(1));//(.*?)
}
}

运行结果:

<input high=\"20\" weight=\"70\">test</input>
test

综上可知:减少不需要获取的分组,可以提高正则表达式的性能。

总结

正则表达式虽然小,却有着强大的匹配功能。我们经常用到它,比如,注册页面手机号或邮 箱的校验。 但很多时候,我们又会因为它小而忽略它的使用规则,测试用例中又没有覆盖到一些特殊用 例,不乏上线就中招的情况发生。 综合我以往的经验来看,如果使用正则表达式能使你的代码简洁方便,那么在做好性能排查 的前提下,可以去使用;如果不能,那么正则表达式能不用就不用,以此避免造成更多的性 能问题。

本文标签: 沉思全集性能Java