標籤:
正則可以看做一門 DSL,但它卻應用極其廣泛,可以輕鬆解決很多情境下的字串匹配、篩選問題。同時呢有句老話:
“ 如果你有一個問題,用Regex解決,那麼你現在就有兩個問題了。”
Some people, when confronted with a problem, think "I know, I‘ll use regular expressions." Now they have two problems.
今天我們就來聊聊 Java Regex StackOverflowError 的問題及其一些最佳化點。
1、問題
最近,有同事發現一段正則在本地怎麼跑都沒問題,但是放到 Hadoop 叢集上總會時不時的拋 StackOverflowError 。
代碼我先簡化下:
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());}}
執行之後的結果是:
++++++++++++++++++++++++++++++Exception in thread "main" java.lang.StackOverflowErrorat 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)......
起初這個問題是從叢集上拋出來的,大家可以看到這個異常有兩個特點:
(1)不可用 Exception 捕獲,因為 Error 直接繼承自 Throwable 而非 Exception,所以即使你要捕獲也應當捕獲 Error。
(2)另外一點是大家可以看到拋出的錯誤並沒有指明行號,當這段代碼混在一個數百行的工具類,有數十條類似的正則的時候,無疑給定位問題帶來了難度,這就需要我們能有一定的單元測試能力。
註:
(1)如果你的環境沒有拋出上述錯誤,嘗試調大 for 迴圈的次數或者指定 jvm 參數:-Xss1k
(2)如果你還不明白 StackOverflowError 是什麼含義,可以參考上一篇文章:JVM 運行時資料區簡介
2、問題分析
Regex引擎分成兩類,一類稱為DFA(確定性有窮自動機),另一類稱為NFA(非確定性有窮自動機)。兩類引擎要順利工作,都必須有一個正則式和一個文本串。DFA捏著文本串去比較正則式,看到一個子正則式,就把可能的匹配串全標註出來,然後再看正則式的下一個部分,根據新的匹配結果更新標註。而NFA是捏著正則式去比文本,吃掉一個字元,就把它跟正則式比較,匹配就記下來,然後接著往下幹。一旦不匹配,就把剛吃的這個字元吐出來,一個個的吐,直到回到上一次匹配的地方。
DFA與NFA機制上的不同帶來5個影響:
1. DFA 對於文本串裡的每一個字元只需掃描一次,比較快,但特性較少;NFA要翻來覆去吃字元、吐字元,速度慢,但是特性豐富,所以反而應用廣泛,當今主要的Regex引擎,如Perl、Ruby、Python的re模組、Java和.NET的regex庫,都是NFA的。
2. 只有NFA才支援lazy和backreference等特性;
3. NFA急於邀功請賞,所以最左子正則式優先匹配成功,因此偶爾會錯過首選結果;DFA則是“最長的左子正則式優先匹配成功”。
4. NFA預設採用greedy量詞;
5. NFA可能會陷入遞迴調用的陷阱而表現得效能極差。
在使用Regex的時候,底層是通過遞迴方式調用執行的,每一層的遞迴都會在棧線程的大小中佔一定記憶體,如果遞迴的層次很多,就會報出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)
文章開頭的問題可以簡單理解為方法的嵌套調用層次太深,上層的方法棧一直得不到釋放,導致棧空間不足。
下面我們要做的就是瞭解一些正則效能的最佳化點,規避這種深層次的遞迴調用。
3、Java 正則的一些最佳化點3.1 Pattern.compile() 先行編譯運算式
如果在程式中多次使用同一個Regex,一定要用Pattern.compile()編譯,代替直接使用Pattern.matches()。如果一次次對同一個Regex使用Pattern.matches(),例如在迴圈中,沒有編譯的Regex消耗比較大。因為matches()方法每次都會先行編譯使用的運算式。另外,記住你可以通過調用reset()方法對不同的輸入字串重複使用Matcher對象。
3.2 留意選擇(Beware of alternation)
類似“(X|Y|Z)”的Regex有降低速度的壞名聲,所以要多留心。首先,考慮選擇的順序,那麼要將比較常用的選擇項放在前面,因此它們可以較快被匹配。另外,嘗試提取共用模式;例如將“(abcd|abef)”替換為“ab(cd|ef)”。後者匹配速度較快,因為NFA會嘗試匹配ab,如果沒有找到就不再嘗試任何選擇項。(在當前情況下,只有兩個選擇項。如果有很多選擇項,速度將會有顯著的提升。)選擇的確會降低程式的速度。在我的測試中,運算式“.*(abcd|efgh|ijkl).*”要比調用String.indexOf()三次——每次針對錶達式中的一個選項——慢三倍。
3.3 減少分組與嵌套
如果你實際並不需要擷取一個分組內的文本,那麼就使用非捕獲分組。例如使用“(?:X)”代替“(X)”。
總結下來就是:減少分支選擇、減少捕獲嵌套、減少貪婪匹配
4、解決方案4.1 臨時工方案
try...catch.../增加-Xss,治標不治本,不推薦。
4.2 最佳化正則才是王道4.2.1 文法層面最佳化
根據 2.2 提到的,我們這樣最佳化下:
final String TEST_REGEX = "([=+\\s\\p{P}A-Za-z0-9\u4E00-\u9FA5])+";
經測試,JVM 參數不變的情況下,for 迴圈 100w 次直到 OOM 了都不會再發生文章開頭的棧溢出的問題了。
4.2.2 商務邏輯層面最佳化
由於我不清楚作者的業務情境,不好做業務最佳化,總的原則是當你的正則太複雜的時候,可以考慮邏輯拆分,或者部分不走正則,如果把正則當做萬能工具可能會得不償失。
總結:在字串尋找與匹配領域,正則可以說幾乎是“萬能”的,但是許多情境下,它的代價不容小覷,如何寫出高效率、可維護的正則或者怎麼能避開正則都是值得咱們思考的問題。
Refer:
[1] 關於Java正則引起的StackOverFlowError問題以及解決方案
http://blog.csdn.net/qq522935502/article/details/8161273
[2] Java正則與棧溢出
http://daimojingdeyu.iteye.com/blog/385304
[3] 最佳化Java中的Regex
http://blog.csdn.net/mydeman/article/details/1800636
[4] 從一個Regex造成的StackOverflowError說起
http://ren.iteye.com/blog/1828562
[5] Regex(三):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 Regex的溢出問題 及不完全解決方案
http://www.blogjava.net/roymoro/archive/2011/04/28/349163.html
聊聊 Java Regex StackOverflowError 問題及其最佳化