Java 正則表達式 StackOverflowError 問題及其優化

正則能夠看作一門 DSL,但它卻應用極其普遍,能夠輕鬆解決不少場景下的字符串匹配、篩選問題。同時呢有句老話:php

「 若是你有一個問題,用正則表達式解決,那麼你如今就有兩個問題了。」html

Some people, when confronted with a problem, think "I know, I'll use regular expressions." Now they have two problems.java

今天咱們就來聊聊 Java 正則表達式 StackOverflowError 的問題及其一些優化點。正則表達式

一、問題

最近,有同事發現一段正則在本地怎麼跑都沒問題,可是放到 Hadoop 集羣上總會時不時的拋 StackOverflowError 。express

代碼我先簡化下:性能優化

package java8test;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Test {

	public static void main(String[] args) {

		final String TEST_REGEX = "([=+]|[\\s]|[\\p{P}]|[A-Za-z0-9]|[\u4E00-\u9FA5])+";
		StringBuilder line = new StringBuilder();
		System.out.println("++++++++++++++++++++++++++++++");
		for (int i = 0; i < 10; i++) {
			line.append(
					"http://hh.ooxx.com/ershoufang/?PGTID=14366988648680=+.7342327926307917&ClickID=1&key=%2525u7261%2525u4E39%2525u5BCC%2525u8D35%2525u82B1%2525u56ED&sourcetype=1_5");
			line.append(
					"http://wiki.corp.com/index.php?title=Track%E6%A0%87%E5%87%86%E6%97%A5%E5%BF%97Hive%E8%A1%A8-%E5%8D%B3%E6%B8%85%E6%B4%97%E5%90%8E%E7%9A%84%E6%97%A5%E5%BF%97");
			line.append(
					"http://www.baidu.com/s?ie=UTF-8&wd=58%cd%ac%b3%c7%b6%fe%ca%d6%b3%b5%b2%e2%ca%d4%ca%fd%be%dd&tn=11000003_hao_dg");
			line.append("http://cs.ooxx.com/yewu/?key=城&cmcskey=的設計費開始低&final=1&jump=1&specialtype=gls");
			line.append(
					"http%3A%2F%2Fcq.ooxx.com%2Fjob%2F%3Fkey%3D%25E7%25BD%2591%25E4%25B8%258A%25E5%2585%25BC%25E8%2581%258C%26cmcskey%3D%25E7%25BD%2591%25E4%25B8%258A%25E5%2585%25BC%25E8%2581%258C%26final%3D1%26jump%3D2%26specialtype%3Dgls%26canclequery%3Disbiz%253D0%26sourcetype%3D4");
		}
		line.append(" \001 11111111111111111111111111");
		Pattern p_a = null;
		try {
			p_a = Pattern.compile(TEST_REGEX);
			Matcher m_a = p_a.matcher(line);
			while (m_a.find()) {
				String a = m_a.group();
				System.out.println(a);
			}
		} catch (Exception e) {
			// TODO: handle exception
		}

		System.out.println("line size: " + line.length());
	}
}

執行以後的結果是:app

++++++++++++++++++++++++++++++
Exception in thread "main" java.lang.StackOverflowError
	at java.util.regex.Pattern$Loop.match(Unknown Source)
	at java.util.regex.Pattern$GroupTail.match(Unknown Source)
	at java.util.regex.Pattern$BranchConn.match(Unknown Source)
	at java.util.regex.Pattern$CharProperty.match(Unknown Source)
......

起初這個問題是從集羣上拋出來的,你們能夠看到這個異常有兩個特色:jvm

(1)不可用 Exception 捕獲,由於 Error 直接繼承自 Throwable 而非 Exception,因此即便你要捕獲也應當捕獲 Error。工具

(2)另一點是你們能夠看到拋出的錯誤並無指明行號,當這段代碼混在一個數百行的工具類,有數十條相似的正則的時候,無疑給定位問題帶來了難度,這就須要咱們能有必定的單元測試能力。oop

注:

(1)若是你的環境沒有拋出上述錯誤,嘗試調大 for 循環的次數或者指定 jvm 參數:-Xss1k

(2)若是你還不明白 StackOverflowError 是什麼含義,能夠參考上一篇文章:JVM 運行時數據區簡介

二、問題分析

正則表達式引擎分紅兩類,一類稱爲DFA(肯定性有窮自動機),另外一類稱爲NFA(非肯定性有窮自動機)。兩類引擎要順利工做,都必須有一個正則式和一個文本串。DFA捏着文本串去比較正則式,看到一個子正則式,就把可能的匹配串全標註出來,而後再看正則式的下一個部分,根據新的匹配結果更新標註。而NFA是捏着正則式去比文本,吃掉一個字符,就把它跟正則式比較,匹配就記下來,而後接着往下幹。一旦不匹配,就把剛吃的這個字符吐出來,一個個的吐,直到回到上一次匹配的地方。 

     DFA與NFA機制上的不一樣帶來5個影響: 

     1. DFA 對於文本串裏的每個字符只需掃描一次,比較快,但特性較少;NFA要翻來覆去吃字符、吐字符,速度慢,可是特性豐富,因此反而應用普遍,當今主要的正則表達式引擎,如Perl、Ruby、Python的re模塊、Java和.NET的regex庫,都是NFA的。 

     2. 只有NFA才支持lazy和backreference等特性; 

     3. NFA急於邀功請賞,因此最左子正則式優先匹配成功,所以偶爾會錯過最佳匹配結果;DFA則是「最長的左子正則式優先匹配成功」。 

     4. NFA缺省採用greedy量詞; 

     5. NFA可能會陷入遞歸調用的陷阱而表現得性能極差。 

在使用正則表達式的時候,底層是經過遞歸方式調用執行的,每一層的遞歸都會在棧線程的大小中佔必定內存,若是遞歸的層次不少,就會報出stackOverFlowError異常。因此在使用正則的時候實際上是有利有弊的。

Java程序中,每一個線程都有本身的Stack Space。這個Stack Space不是來自Heap的分配。因此Stack Space的大小不會受到-Xmx和-Xms的影響,這2個JVM參數僅僅是影響Heap的大小。Stack Space用來作方法的遞歸調用時壓入Stack Frame。因此當遞歸調用太深的時候,就有可能耗盡Stack Space,爆出StackOverflow的錯誤。Stack Space的大小隨着OS,JVM以及環境變量的大小而發生變化。通常說來默認的大小是512K。在64位的系統中,這個Stack Space值會更大。通常說來,Stack Space爲128K是夠用的。這時你說須要作的就是觀察。若是你的程序沒有爆出StackOverflow的錯誤,可使用-Xss來調整Stack Space的大小爲128K。(eg:-Xss128K)

文章開頭的問題能夠簡單理解爲方法的嵌套調用層次太深,上層的方法棧一直得不到釋放,致使棧空間不足。

下面咱們要作的就是了解一些正則性能的優化點,規避這種深層次的遞歸調用。

三、Java 正則的一些優化點

3.1 Pattern.compile() 預編譯表達式

若是在程序中屢次使用同一個正則表達式,必定要用Pattern.compile()編譯,代替直接使用Pattern.matches()。若是一次次對同一個正則表達式使用Pattern.matches(),例如在循環中,沒有編譯的正則表達式消耗比較大。由於matches()方法每次都會預編譯使用的表達式。另外,記住你能夠經過調用reset()方法對不一樣的輸入字符串重複使用Matcher對象。

3.2 留意選擇(Beware of alternation)

相似「(X|Y|Z)」的正則表達式有下降速度的壞名聲,因此要多留心。首先,考慮選擇的順序,那麼要將比較經常使用的選擇項放在前面,所以它們能夠較快被匹配。另外,嘗試提取共用模式;例如將「(abcd|abef)」替換爲「ab(cd|ef)」。後者匹配速度較快,由於NFA會嘗試匹配ab,若是沒有找到就再也不嘗試任何選擇項。(在當前狀況下,只有兩個選擇項。若是有不少選擇項,速度將會有顯著的提高。)選擇的確會下降程序的速度。在個人測試中,表達式「.*(abcd|efgh|ijkl).*」要比調用String.indexOf()三次——每次針對表達式中的一個選項——慢三倍。

3.3 減小分組與嵌套

若是你實際並不須要獲取一個分組內的文本,那麼就使用非捕獲分組。例如使用「(?:X)」代替「(X)」。

總結下來就是:減小分支選擇、減小捕獲嵌套、減小貪婪匹配

四、解決方案

4.1 臨時工方案

try...catch.../增長-Xss,治標不治本,不推薦。

4.2 優化正則纔是王道

4.2.1 語法層面優化

根據 3.2 提到的,咱們這樣優化下:

final String TEST_REGEX = "([=+\\s\\p{P}A-Za-z0-9\u4E00-\u9FA5])+";

經測試,JVM 參數不變的狀況下,for 循環 100w 次直到 OOM 了都不會再發生文章開頭的棧溢出的問題了。

4.2.2 業務邏輯層面優化

因爲我不清楚做者的業務場景,很差作業務優化,總的原則是當你的正則太複雜的時候,能夠考慮邏輯拆分,或者部分不走正則,若是把正則當作萬能工具可能會得不償失。

總結:在字符串查找與匹配領域,正則能夠說幾乎是「萬能」的,可是許多場景下,它的代價不容小覷,如何寫出高效率、可維護的正則或者怎麼能避開正則都是值得我們思考的問題。

五、NFA引擎正則性能優化Tips

  • 1. 優先選擇最左端的匹配結果

  • 2.標準量詞優先匹配

好比'.*[0-9][0-9]' 來匹配字符串"abcd12efghijklmnopqrstuvw",這時候的匹配方式是‘.*’先匹配了整行,可是不能知足以後的兩個數字的匹配,因此‘.*’就退還一個字符‘w’,仍是沒法匹配,繼續退還一個‘v’,循環退還字符到‘2’發現匹配了一個,可是仍是沒法匹配兩個數字,因此繼續退還‘1’

  • 3.謹慎使用捕獲性括號(),選擇使用非捕獲性括號(?:expression)

捕獲性括號須要消耗一部份內存

  • 4.使用字符組代替分支(替換)條件

例如用[a-d] 代替 a|b|c|d避免沒必要要的回溯

  • 5.不要濫用字符組(單個字符時不要用字符組)

\. 代替 [.]

  • 6.使用錨點^ $ \b 加速定位

  • 7.從兩次中提取必須元素

a{2,4} 寫成 aa{0,2}

  • 8.提取多選結構開頭的相同字符

the|this 改爲th(?:e|is)

  • 9.選擇字符串中最常出現的字符串放到分支最前面

  • 10.能懶則懶,不要貪婪

在 * + {m,n}後面加上問好?就會變成非貪婪模式

總結:引用CFC4N大牛的一句話 濫用. 點號  * 星號  +加號  ()括號 是不環保,不負責任的作法 !

  • 11.簡單字符串處理應避免使用正則表達式

Refer:

[1] 關於Java正則引發的StackOverFlowError問題以及解決方案

http://blog.csdn.net/qq522935502/article/details/8161273

[2] Java正則與棧溢出

http://daimojingdeyu.iteye.com/blog/385304

[3] 優化Java中的正則表達式

http://blog.csdn.net/mydeman/article/details/1800636

[4] 從一個正則表達式形成的StackOverflowError提及

http://ren.iteye.com/blog/1828562

[5] 正則表達式(三):Unicode諸問題(下)

http://www.infoq.com/cn/news/2011/03/regular-expressions-unicode-2

http://www.infoq.com/cn/author/%E4%BD%99%E6%99%9F

[6] StackOverflowError when matching large input using RegEx

http://stackoverflow.com/questions/15082010/stackoverflowerror-when-matching-large-input-using-regex

[7] try/catch on stack overflows in java?

http://stackoverflow.com/questions/2535723/try-catch-on-stack-overflows-in-java

[8] Java正則達式引發死循環問題解決辦法

http://blog.csdn.net/shixing_11/article/details/5997567

[9] JAVA 正則表達式的溢出問題 及不徹底解決方案

http://www.blogjava.net/roymoro/archive/2011/04/28/349163.html

[10] NFA引擎正則優化TIPS、Perl正則技巧及正則性能評測方法  

http://danqingdani.blog.163.com/blog/static/18609419520144523853586/

[11] Java正則引起的思考

http://blogread.cn/it/article/5982?f=wb

[12] 進階正則表達式

http://www.barretlee.com/blog/2014/01/18/cb-how-regular-expressions-work/

[13] 一個由正則表達式引起的血案

http://bit.ly/2vlKfIf

相關文章
相關標籤/搜索