`
daimojingdeyu
  • 浏览: 274884 次
  • 性别: Icon_minigender_1
  • 来自: 山东
社区版块
存档分类
最新评论

Java正则与栈溢出

    博客分类:
  • Java
阅读更多
使用RegexBuddy测试正确的正则,放入到java代码中会抛出stackoverflowerror.
开始以为是文件太长,将文件减短后还是会有问题,觉得应该是java内置的正则的问题,网上Google了一下,遇到这个问题的人多了去了。
java内置的正则解析器有问题,不过可以绕过去。有两个较好用的库,这两个库都不会发生StackOverFlowerError(这里有点错误,使用jregex还是会发生溢出的)。
pattwo :http://www.javaregex.com
jregex :http://sourceforge.net/projects/freshmeat_jregex

另有一篇参考文档:
http://www.javaworld.com/javaworld/jw-09-2007/jw-09-optimizingregex.html

-----------------------------------------2009.5.25日凌晨更新---------------------
这两天觉得正则表达式在匹配时会溢出的问题还是没有搞清楚,所以就又google了一下,发现距离答案又近了一些。栈溢出并不是java的bug,这和正则表达式引擎的算法有关系。主要有两种正则引擎:DFA和NFA。
有一往篇文章介绍的挺好,这里拿过来用一下

引用

1. 了解正则表达式的历史
正则表达式萌芽于1940年代的神经生理学研究,由著名数学家Stephen Kleene第一个正式描述。具体地说,Kleene归纳了前述的神经生理学研究,在一篇题为《正则集代数》的论文中定义了“正则集”,并在其上定义了一个代数系统,并且引入了一种记号系统来描述正则集,这种记号系统被他称为“正则表达式”。在理论数学的圈子里被研究了几十年之后,1968年,后来发明了 UNIX系统的Ken Thompson第一个把正则表达式用于计算机领域,开发了qed和grep两个实用文本处理工具,取得了巨大成功。在此后十几年里,一大批一流计算机科学家和黑客对正则表达式进行了密集的研究和实践。在1980年代早期,UNIX运动的两个中心贝尔实验室和加州大学伯克利分校分别围绕grep工具对正则表达式引擎进行了研究和实现。与之同时,编译器“龙书”的作者Alfred Aho开发了Egrep工具,大大扩展和增强了正则表达式的功能。此后,他又与《C程序设计语言》的作者Brian Kernighan等三人一起发明了流行的awk文本编辑语言。到了1986年,正则表达式迎来了一次飞跃。先是C语言顶级黑客Henry Spencer以源代码形式发布了一个用C语言写成的正则表达式程序库(当时还不叫open source),从而把正则表达式的奥妙带入寻常百姓家,然后是技术怪杰Larry Wall横空出世,发布了Perl语言的第一个版本。自那以后,Perl一直是正则表达式的旗手,可以说,今天正则表达式的标准和地位是由Perl塑造的。Perl 5.x发布以后,正则表达式进入了稳定成熟期,其强大能力已经征服了几乎所有主流语言平台,成为每个专业开发者都必须掌握的基本工具。

2. 掌握一门正则表达式语言
使用正则表达式有两种方法,一种是通过程序库,另一种是通过内置了正则表达式引擎的语言本身。前者的代表是 Java、.NET、C/C++、Python,后者的代表则是Perl、Ruby、JavaScript和一些新兴语言,如Groovy等。如果学习正则表达式的目标仅仅是应付日常应用,则通过程序库使用就可以。但只有掌握一门正则表达式语言,才能够将正则表达式变成编程的直觉本能,达到较高的水准。不但如此,正则表达式语言也能够在实践中提供更高的开发和执行效率。因此,有心者应当掌握一门正则表达式语言。

3. 理解DFA和NFA
正则表达式引擎分成两类,一类称为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量词(见item 4);
5. NFA可能会陷入递归调用的陷阱而表现得性能极差。

我这里举一个例子来说明第3个影响。

例如用正则式/perl|perlman/来匹配文本 ‘perlman book’。如果是NFA,则以正则式为导向,手里捏着正则式,眼睛看着文本,一个字符一个字符的吃,吃完 ‘perl’ 以后,跟第一个子正则式/perl/已经匹配上了,于是记录在案,往下再看,吃进一个 ‘m’,这下糟了,跟子式/perl/不匹配了,于是把m吐出来,向上汇报说成功匹配 ‘perl’,不再关心其他,也不尝试后面那个子正则式/perlman/,自然也就看不到那个更好的答案了。

如果是DFA,它是以文本为导向,手里捏着文本,眼睛看着正则式,一口一口的吃。吃到/p/,就在手里的 ‘p’ 上打一个钩,记上一笔,说这个字符已经匹配上了,然后往下吃。当看到 /perl/ 之后,DFA不会停,会尝试再吃一口。这时候,第一个子正则式已经山穷水尽了,没得吃了,于是就甩掉它,去吃第二个子正则式的/m/。这一吃好了,因为又匹配上了,于是接着往下吃。直到把正则式吃完,心满意足往上报告说成功匹配了 ‘perlman’。

由此可知,要让NFA正确工作,应该使用 /perlman|perl/ 模式。

通过以上例子,可以理解为什么NFA是最左子式匹配,而DFA是最长左子式匹配。实际上,如果仔细分析,关于NFA和DFA的不同之处,都可以找出道理。而明白这些道理,对于有效应用正则表达式是非常有意义的。
4. 理解greedy和lazy量词
由于日常遇到的正则表达式引擎全都是NFA,所以缺省都采用greedy量词。Greedy量词的意思不难理解,就是对于/.*/、/w+/这样的“重复n”次的模式,以贪婪方式进行,尽可能匹配更多字符,直到不得以罢手为止。

举一个例子,以 /<.*>/ 模式匹配 ‘<book> <title> Perl Hacks </title> </book>t’文本,匹配结果不是 ‘<book>’,而是 ‘<book> <title> Perl Hacks </title> </book>’。原因就在于NFA引擎以贪婪方式执行“重复n次”的命令。让我们来仔细分析一下这个过程。

条款3指出,NFA的模型是以正则式为导向,拿着正则式吃文本。在上面的例子里,当它拿着/.*/这个正则式去吃文本的时候,缺省情况下它就这么一路吃下去,即使碰到 ‘>’字符也不罢手——既然 /./ 是匹配任意字符, ‘>’ 当然也可以匹配!所以就尽管吃下去,直到吃完遇到结尾(包括t字符)也不觉得有什么不对。这个时候它突然发现,在正则表达式最后还有一个 />/,于是慌了神,知道吃多了,于是就开始一个字符一个字符的往回吐,直到吐出倒数第二个字符 ‘>’,完成了与正则式的匹配,才长舒一口气,向上汇报,匹配字符串从第一个字符 ‘<’ 开始,到倒数第二个字符 ‘>’结束,即‘<book> <title> Perl Hacks </title> </book>’。

Greedy量词的行为有时确实是用户所需要的,有时则不是。比如在这个例子里,用户可能实际上想得到的是 ‘book’ 串。怎么办呢?这时候lazy量词就派上用场了。把模式改为/<.*?>/就可以得到 ‘book’。这个加在 ‘*’号后面的 ‘?’ 把greedy量词行为变成lazy量词行为,从而由尽量多吃变为尽量少吃,只要吃到一个 ‘>’立刻停止。

问号在正则表达式里用途最广泛,这里是很重要的一个用途。


5. 理解backtracking
在条款4的基础上解释backtracking就很容易了。当NFA发现自己吃多了,一个一个往回吐,边吐边找匹配,这个过程叫做backtracking。由于存在这个过程,在NFA匹配过程中,特别是在编写不合理的正则式匹配过程中,文本被反复扫描,效率损失是不小的。明白这个道理,对于写出高效的正则表达式很有帮助。

从上面应该可以看出,由于java使用的是NFA的正则引擎,所以在处理长字符串的匹配时发生栈溢出是可以理解的,因为这种引擎支持回溯,而字符的匹配是通过栈来完成的回溯。
文本的内容毕竟有限,如果真的发生溢出了可以使用VM的-Xss参数来增加程序的栈空间,可以通过-Xss90M,来指定使用90M的栈空间,这样就基本上不会发生溢出了,除非真的有很大的文本,这个时候就没有好办法,因为-Xss指定的栈空间好像不能大于90多M,再大点的话虚拟机启动不了,就异常退出了,这个还没有明白为什么。
还有一个没有明白的问题:回溯时使用的栈,应该可以使用Stack对象吧,这样就可以将对象产生在堆空间上了,为什么不这样做,还是不能这样做?后面再研究一下代码看看
分享到:
评论

相关推荐

    Java开发技术大全(500个源代码).

    overflowExample.java 演示溢出 precedence.java 演示自加运算符的优先级 primeNumber.java 输出100-200之间的所有素数 ranking.java 评定成绩等级 rankingBySwitch.java 用switch语句评定成绩等级 ...

    java面试知识点易错难点总结

    - **递归调用**:函数调用自身的方法,需要注意避免无限递归导致栈溢出。 #### 数组 - **内存布局**:数组的引用存放在栈内存中,实际元素存放在堆内存中。这种引用传递机制使得多个引用指向同一个数组时,对其中...

    Java开发与技术挑战——关于技术的技术思考.docx

    Java开发与技术挑战涉及到多个层次的知识点,包括Java语言的学习路径、开发工程师的成长历程、系统架构设计、技术选型以及面对的技术挑战。以下是对这些内容的详细解释: 1. **Java学习路径**: - 初学者通常从...

    C、C++和Java安全编码实践提示与技巧

    ### C、C++和Java安全编码实践提示与技巧 #### 注入攻击防范 在软件开发过程中,特别是使用C、C++或Java等语言时,安全编码至关重要。本篇重点介绍几种常见的安全问题及其防范措施,尤其关注的是如何避免注入攻击...

    java常见错误集合以及描述

    #### 十、StackOverflowError (栈溢出错误) **描述**:当方法递归调用层次过深导致栈空间不足时会发生此错误。 **示例代码**: ```java public static void main(String[] args) { method(); } private static ...

    java最新面试宝典

    - 内存溢出与内存泄漏的区别。 - GC算法的种类及其优缺点。 #### 三、类加载 **3.1 类的加载过程** - **知识点概述:** - 加载阶段:找到类的二进制数据并转化为`Class`对象。 - 验证阶段:确保类文件符合规范...

    近年Java面试问题列表

    - 正则表达式的语法和匹配规则,以及在Java中的使用。 14. **JVM底层**: - 类加载机制、类加载器、内存模型、JVM调优等概念。 15. **Java最佳实践**: - 如何编写可读、可维护的代码,以及代码重构的技巧。 ...

    Java安全与质量编码规范.docx

    - **避免使用递归:** 除非特殊情况,否则应避免使用递归,以防栈溢出。 - **sleep()参数控制:** 合理设置sleep()函数的参数,避免过度占用CPU资源。 - **正则使用建议:** 对复杂的正则表达式进行优化,提高...

    《剑指Offer》Java代码(高清带目录) (1).pdf

    53. 正则表达式匹配:考察正则表达式的使用以及匹配算法。 54. 表示数值的字符串:涉及字符串解析以及数值类型的判断。 55. 字符流中第一个不重复的字符:考察字符流的处理以及字符计数。 56. 链表中环的入口节点...

    美团Java 岗 154+道面试题

    - **内存泄漏与内存溢出**:了解常见原因和排查方法。 9. **反射与注解** - **反射**:理解反射的原理,能动态创建对象、调用方法。 - **注解**:了解注解的元注解、自定义注解及处理器。 10. **网络编程** - ...

    面试题大礼包 各类的总结

    5. **内存模型与垃圾回收**:JVM内存区域(堆、栈、方法区、本地方法栈、程序计数器),垃圾回收机制(GC),内存泄漏与内存溢出问题。 6. **多线程**:线程的创建方式(实现Runnable接口和继承Thread类),线程...

    JAVA开发面试附答案

    - **递归调用**:深度过大的递归调用会导致栈溢出。 - **大对象分配**:分配过大对象,无法找到足够的连续内存空间。 - **内存泄漏**:不再使用的对象未能及时回收。 #### 15. Redis数据类型知识点 **知识点概述:...

    Java递归遍历文件目录代码实例

    * 需要注意递归函数的调用次数,以避免栈溢出错误。 * 需要注意文件目录的结构和文件的数量,以避免程序的运行时间过长。 Java递归遍历文件目录代码实例是Java中的一种常见的遍历方法,它可以对文件目录中的文件和...

    leetcode题库-Leetcode_java:Leetcode刷题记录,使用JAVA,每天一题,由易到难

    在Java中,递归函数需要注意防止栈溢出。 - **循环**:循环结构如`for`、`while`在LeetCode中广泛使用,特别是在遍历数据结构和执行重复任务时。 5. **效率优化**: - **空间复杂度与时间复杂度**:理解和优化...

    Spring Boot 个人实例项目,主要用于个人初代java优化及相关算法及初代神经网络自主学习程序的了解及学习展示.zip

    4. **过拟合与正则化**:为了避免模型在训练集上表现过好而在测试集上表现不佳,可以使用正则化(L1、L2)或早停策略等方法。 5. **激活函数**:不同的激活函数如 Tanh、ReLU 和 Leaky ReLU 影响网络的非线性表达...

    批量查找未打开的文本内的内容

    - Java中可以使用`java.util.regex`包进行正则表达式的匹配,`java.nio.file`包则提供了读取文件的便捷方法。 - C++中可以利用标准库中的`&lt;regex&gt;`头文件进行正则匹配,`&lt;fstream&gt;`用于文件操作。 2. **文件遍历*...

    攻击JavaWeb应用1-9[JavaWeb安全系列]

    理解栈和堆内存分配、使用安全的库函数(如Java的StringBuffer代替StringBuilder)可以降低溢出风险。 以上只是JavaWeb安全的基本方面,实际应用中还需要结合OWASP Top Ten等安全指南,持续学习和实践,以确保Web...

    布尔翻译逆波兰式,完整实验报告,写上班级姓名即可

    3. 栈溢出或空栈错误,需要检查代码中的边界条件。 通过理解和实践布尔翻译逆波兰式,不仅可以加深对布尔代数的理解,还能锻炼编程和算法设计能力,这对于计算机科学的学习者来说是非常宝贵的经验。

    20180916BIGO面试准备

    2. **堆和栈的区别及栈溢出原因** - **栈**: 大小固定,用于存储局部变量、函数调用信息等。Windows 32位平台下默认大小约为1MB,实际使用量取决于线程调用方式。 - **堆**: 动态变化,用于动态分配内存。其大小取...

    JavaDev:此回购是练习和笔记的集合,其中包含对各种普通Java库的深入了解以及类似工具的不同之处,特别注意了不直观的结果和行为

    - **内存模型**: 理解Java内存区域(堆、栈、方法区等)以及内存溢出问题。 - **异常处理**: 深入理解checked和unchecked异常的区别,以及如何正确抛出和捕获异常。 - **String不可变性**: String对象的不可变性...

Global site tag (gtag.js) - Google Analytics