《精通正则表达式》阅读笔记
2花了近三周的晚上的时间把《精通正则表达式》这本书看完了,说是看完,其实是只看3,4,5,6四章,前面的和后面的几章按照作者的建议觉得自己没有必要去看了。毕竟对于这本书来说,这四章才是重点,在这四章中,作者就已经把正则表达式的语法,正则表达式引擎的工作原理,以及正则表达式的调优讲完。下面是我在看这四章的过程中的一些笔记和总结。
正则表达式的语法
对于正则表达式的语法,相信大多数人都和我一样知道一些基本的语法,但是对于一些不常见的语法还是非常陌生,这边就把这些比较陌生的正则表达式语法记录一下:
- 分组且捕获的文本括号:
(),文本被捕获以后可以通过反向引用(\1,\2等等)来引用。 - 分组但是不捕获的文本括号:
(?:),文本被捕获后不能通过反向引用来引用到。 - 忽略优先量词:
*?,+?,??,{min,max}?:量词在正常情况下是“匹配优先”的,会尽量匹配更多的内容(Greedy),忽略优先量词则优先匹配尽量少的内容。 - 占有优先量词:
?+,*+,++,{m,n}+,与匹配优先量词很相似,只是从来不交还已经匹配的字符,即如果已经匹配到了,就放弃该量词表达式内的所有的回溯可能都抛弃。 - 固化分组:
(?>…),使用固化分组的匹配与正常的匹配无差别,但是如果匹配进行到此结构之后,那么此结构体中所有的备用状态都会被放弃。占有优先量词可以看做是固化分组的特殊情况。 - 条件判断:
(?if then|else):测试if条件表达式,如果测试为真,则执行then部分表达式,如果测试失败,则执行else部分表达式,if条件表达式是特殊的表达式,大概有两种情况,一种是对捕获型括号的特殊引用,比如(?(1))这样,会测试第一组捕获型括号是否参与了匹配;另一种是对环视进行匹配,如果环视能够匹配,则返回true。 - 环视,即零宽断言:
(?=…)(?!…)(?<=…)(?<!…),具体的解释可以参考我之前关于零宽断言的一篇文章:使用零宽断言来匹配不包含连续字符串的行。 - 匹配上一次匹配结束的位置:\G,首先出现在Perl中,在用于迭代匹配的时候比较有用。
正则表达式引擎的工作原理
一般上正则表达式的引擎可以分为两种:NFA和DFA,分别表示非确定性有穷自动机和确定性有穷自动机,这两者背后的理论基础大可不必去了解,只需要知道NFA引擎是在进行匹配的时候是表达式主导的,而DFA引擎在进行匹配的时候是文本主导的,下面具体来拿一个例子来将述下这两者的区别:
NFA引擎工作原理
对于NFA引擎,它会先拿出正则表达式的第一部分,然后看看文本有没有符合这个部分的,如果是,则继续表达式的下一部分进行匹配。
现在假设我们有一个正则表达式是a+b,需要用它去匹配aaaaab这个字符串,它的匹配过程大概如下:
- 首先找到正则表达式的第一个部分
a+ - 然后看文本是否匹配到了这个表达式,在文本中,前面的5个a是能够匹配
a+的,那么继续正则表达式的下面部分 - 后面是一个
b,它能够匹配到文本中5个a后面的b,也匹配成功。 - 这样正则表达式的所有部分都能够匹配,所以整个表达式就能够匹配成功。
NFA引擎还有一个重要的特性,这个特性对于理解NFA引擎的原理也是非常关键:回溯。具体来就讲,在遇到两个可能成功的分支时,会先尝试一个可能性,然后记住另一个,如果这个可能性失败了,那么就回溯到之前的可能,回溯的一个重要的原则是,如果需要在“进行尝试”和“跳过尝试”之间选择,对于匹配优先量词,引擎会优先选择“进行尝试”,对于忽略优先量词,引擎会优先选择“跳过尝试”;在进行回溯时,选择回溯的路径是距离最近存储的路径,即基于LIFO原则。
DFA引擎工作原理
对于DFA引擎,DFA引擎会在扫描子字符串时,记录当前有效的所有可能匹配。这个我们就拿书中的tonight的例子来讲。
现在我们拿to(nite|knight|night)来匹配tonight,它的匹配过程大致如下:
- 引擎看拿到文本中的
t,然后找到了表达式中的t可以与之匹配,这样,表达式就可能能够匹配到文本,这样引擎就会添加这样一种潜在的可能。 - 然后引擎继续按照文本往下移动到了
n,这个时候,表达式的多选分支里面只有两个分支可以与之匹配,knight这个分支不能匹配,有效的可能就变成了两个。 - 继续往下走到文本中的
g,这个nite这个分支也不能匹配了,有效的可能变成了一个。 - 最后当匹配到之后的
t时,night这个分支显然能够匹配,这样引擎发现匹配已经成功。
NFA引擎和DFA引擎的各自优缺点:
- 从上面的讲解中可以了解到,NFA引擎因为是正则表达式主导的,可能对文本中的同一个部分进行反复的扫描,DFA则只会对文本进行一次扫描,这样NFA引擎的速度就相对DFA要慢。
- 另外NFA是正则表达式主导的,所以不同的正则表达式对速度的影响比较大,但是DFA因为是文本主导,所以不同的正则表达式对其速度影响不大。
- 从上面一条我们也可以知道,NFA的可玩性就比较大了,相对来说比DFA要灵活很多。其实后面的调优也都是针对NFA引擎的,因为DFA实在是没有什么好调优。
正则表达式调优
正则表达式的调优是在了解了正则表达式的工作原理的基础上进行的,主要是针对NFA引擎,看完正则表达式的调优这一章以后,我自己总结了一条调优的规律:在正则表达式中尽量使用确定的字符:
比较(night|nite|nittt|niasdf)和ni(ght|te|ttt|asdf)这两个正则表达式,因为后面一个正则表达式把各个多选分支中的ni提出来了,这样引擎就能够通过字符扫描的方式快速找到匹配文本中的ni部分,然后再开始匹配后面的多选分支;而前面的那个表达式,引擎在文本的每一个位置都要尝试四个分支,显然速度要慢很多。相比而言,后一个正则表达式比前一个要“确定”,匹配的速度就更快。
针对这一条规律,还可以有很多调优的方式,比如能够加锚点就加锚点之类的,具体的大家可以翻看书的第六章。
总结
应该说这本书觉得是一本值得阅读的书,花时间去看绝对是值得的,特别是当你会一点正则表达式后再去看,收获可能更大。不过这书的翻译看起来有点吃力,读起来不是很顺畅,或许是因为正则表达式本身看起来就有点累,所以看这本书的过程中最好保持头脑清醒,保持注意力集中,这样才能跟上作者的思路。
内存屏障
2前几天一直在看Java内存模型(JMM)这个东西,在了解JMM的过程中也不断地遇到了内存屏障这个名词,遂花了一点时间去了解了一下。
什么是内存屏障
内存屏障,说白了就是一个指令,其作用正如其名字里面所描述的,起到的是屏障的作用,具体地来说,内存屏障这条指令的作用是让屏障指令前面的指令不会被重排序到屏障指令之后,屏障指令之后的指令也不会被重排序到屏障指令之前,相当于一道屏障挡在了指令之间,前面和后面的指令都不能跨过屏障(关于指令重排序,这里不再解释,有兴趣了解的可以去Google一下)。
内存屏障的种类
鉴于CPU和编译器都可能对指令进行重排序,所以我将内存屏障分为两种:
- 编译器级别的:防止编译器对指令进行重排序,例如GCC的
asm volatile("" ::: "memory")等等。 - 处理器级别的:防止处理器对指令进行重排序,例如X86的
lock,lfence(),sfenec()等等(关于处理器级别的内存屏障如何实现,可以参考这篇文章)。
Java的volatile如何通过内存屏障保证其语义的:
首先,需要知道Java中的volatile有两个语义:
- 线程对volatile变量的读写都会马上反映到主存上,对其他线程马上可见。
- 对volatile变量的读写操作不会和其他对共享内存进行操作的指令重排序。
跟踪HotSpot JVM中对volatile的变量的写操作可以发现,其在写操作之后插入了一条指令
asm volatile ("lock; addl $0,0(%%esp)" ::: "cc", "memory");
我们可以把这个指令分成两部分来看:
asm volatile ("" ::: "memory"):这个是GCC中常用的编译器级别的内存屏障,首先这是一条汇编指令,加入了volatile表示指令不会被重排,后面的clobber list里面加入了”memory”表示告诉编译器,指令对主存做了修改,所有CPU中的缓存都失效了,后面的指令需要重新从主存中重新Load数据。lock; addl $0,0(%%esp):对于这一条指令,首先看addl $0,0(%%esp)这个部分,这个部分实际上是个无用操作,如果esp栈顶是0的话,就给栈顶加上一个0,由于这个操作会影响到程序状态字寄存器,所以在后面的clobber list中加入了cc(Condition Code Registry,也称为Flag Registry或Programm Status Word Registry);再看这个汇编指令前面加入了一个lock前缀,这个lock前缀的意思是执行指令的CPU会发出LOCK#信号,独占系统主线直到前面的读写操作全部完成并且执行完加了lock前缀的这条指令,在这个过程中其他CPU的读写操作全部被Hold住(参考Intel的开发手册)。所以lock前缀实际上可以看作是一个多处理器的内存屏障。
这两个部分合起来就起到了一个完整的内存屏障,即对编译器有效,也对处理器有效,另外也保证了volatile的可见性问题。
周末骑行–苏杭往返
2上周末和@woodcafe @厚脸皮后端 @Trinea一起完成了苏杭自行车两天往返,那两天的最低温度大概都在零度左右,最高温度也不过摄氏八度,骑车只要一停下来变马上觉得冷,除了中间在苏州睡了7个多小时之外,剩下的时间基本都在路上,去苏州花了将近14个小时,回杭州花了11个小时,第一次体验了在冬天长时间骑车是什么感觉。这也应该是今年做过的最累的一件事情了,比半程马拉松还要累,不过路上也总算有收获一些沿途的美丽风景,聊以慰藉。
周六从杭州到苏州的路上走地匆匆,边找路,边赶路,在乌镇的时候还碰上@Trinea的车爆胎,耽搁了一点时间。赶到吴江防护林的时候,夜色已经悄悄降临,看到一轮明月从林间升起,才想起早上刚刚在新闻里面看到说今天有月全食,兴奋不已,随即在林字里停了一会儿,小桥,人家,明月,桥下流水,流水中的月影,看到这幅风景,便觉得不虚此行。不过没有单反相机,同行的几位也没有带单反,不能留下那一时的美,甚为遗憾。
到了吴江南北快速干线上的时候,月食开始,一路追着月亮骑过去,看月亮慢慢从盈变缺,几个小时便看到了月的阴晴圆缺,只怕之后再也看不到这样的景色,可惜的是等到月全食完全发生,我们已经到了市区,忙着找旅店,找填肚子的地方,没有看到红月。
有了第一天的经历,第二天从苏州回杭州便觉得轻松了很多,特地在吴江防护林停了一下,掏出手机拍了几张照片:
一直都呆在办公室里面,或者宅在家里,整个秋天就这样晃荡着过了,在初冬看到这样的红叶也算是对秋天的告别:

这张是@woodcafe拍的,吴江防护林里面有蛮多这样的风景,路左右的树连成了一片:

过了吴江防护林,很快便到了浙江境内,也就是乌镇,在乌镇的一座桥上偶遇落日,刚好一条运沙船从桥下穿过,也是很久没有见过的美景:

今年已经快要结束了,应该不会再出去骑车了,明年希望能够和不同的人出去骑车,到不同的地方去,看不同的风景,遇上不同的故事,这样才有意思,不是吗?
另:@woodcafe准备这个春节去环海南岛,大概要花上十天左右,我没有这么多时间可以用,去不成,只能在这里祝顺利了。
海伯利安 — 朝圣者们沉痛的故事
2
昨天晚上看海伯利安看到凌晨一点半,终于把最后一个故事给看完了。海伯利安绝对是我迄今为止看到的最好看的科幻小说,甚至比刘慈欣的三体系列,阿西莫夫的基地系列都要精彩很多。
海伯利安这个书名取自英国诗人约翰·济慈未完成的同名长诗海伯利安,整本书的内容是七个不同职业的人被选择作为最后一批朝圣者前往伯劳鸟和光阴冢所在的偏远星球海伯利安朝圣,他们在朝圣的途中讲述了各自背后沉痛的故事,以及各自和伯劳鸟、光阴冢之间的丝丝联系。
通过朝圣者们的故事,作者也给我们描述了故事发生的宏大背景,地球灭亡,人类将其活动范围扩大到了银河系跨度1000光年的小旋臂内,人类政府霸主组成环网星球,霸主背后又有神秘的人工智能,以及在太空中飘荡的人类分支驱逐者。
作为朝圣的目的地以及联系朝圣者们的线索,伯劳鸟和光阴冢应该是整本书中最神秘的东西了,但是看到最后,我仍然无法搞懂这两者到底是什么?牧师的十字形到底什么?战士的情人莫尼塔到底是不是就是伯劳鸟?诗人的海伯利安诗篇讲述的到底是什么?学者的女儿为什么受到时间潮汐影响后时间是倒着走的?侦探的情人之前到底为什么要去海伯利安朝圣?领事打开光阴冢后会发生什么?而那个没有讲述自己故事的圣徒背后到底又有什么样沉痛的故事?
除了书名取自济慈的长诗海伯利安以外,本书中还有非常多的关于济慈的东西,海伯利安的后续安迪密恩(Endymion)的名字也是取自济慈的同名长诗,故事发生的星球名字叫海伯利安,朝圣者中的侦探的赛伯人情人甚至就是人工智能造出来重现济慈的生活的,不得不说,整本书就是作者丹·西蒙斯在向济慈致敬。
除了济慈,你还能够在书中看到乔叟、叶芝等人的身影,经常能够看到一些名家出现在本书中,让你不得不感慨下作者的博学。
无疑,海伯利安是科幻小说中的史诗巨作,作为科幻迷的你千万不要错过。
JVM规范之内存结构
0原文地址:http://www.goldendoc.org/2011/11/jvm_memory_management/
JVM规范中一块很重要的地方就是内存的管理,JVM规范里面在功能上对JVM的内存进行了一定的划分,理解这一块内容对理解JVM的规范有一定的帮助。当然,就像许多规范一样,JVM规范虽然对内存进行了划分,但是各个JVM的实现却不一定会做这样的划分,说地确切一点是负责对应功能的内存肯定在每一个JVM的实现里面都是有的(如果没有提供参数给程序员调整的话,有些对你来说就是不可见的),但是其实现方式可以是五花八门的。另外,JVM对内存的分配和管理事实上是和Java Class文件的格式,JVM的运行机制都有密切相关,如果对这些东西有所了解,那么对内存的管理理解也会有所帮助。
先来看一张图来直观地感受下JVM规范里面是怎样对JVM的内存进行功能上的区分的:
在这张图里面,首先根据内存是线程私有的还是线程共享的,把内存区域分成了两块:
线程私有
这一块区域主要由程序计数器(PC),虚拟机栈(Virtual Machine Stack)和本地方法栈(Native Method Stack)组成
- 程序计数器:同计算机的程序计数器;如果正在执行Java方法,则指向虚拟机字节码指令的地址,如果是native的,这个计算器的值为空。
- 虚拟机栈:虚拟机栈是和线程的生命周期相同的,每创建一个线程,就会创建一个虚拟机栈。而在一个线程中,每调用一个方法,就会在虚拟机栈中创建一个栈帧(Stack Frame),用于保存方法相关的信息,栈帧的具体结构看上图的箭头左边部分:
- 本地变量表(Local Variable):用于保存本地变量,这里的本地变量是指方法的入参以及在方法中声明的变量,如果在编译Java文件的时候加上”-g”参数,然后用”javap -p -verbose”打印出class文件后可以看到一个方法的本地变量表的内容,下面是一个简单的“Hello, world!”程序的main方法的本地变量表:
LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String;
- 操作数栈(Operand Stack):首先要了解JVM的架构是基于栈的指令集,指令的操作都是对栈的操作,所以方法运行的时候的一些参数和中间变量都必须先放入栈中才能够被操作,这里面的栈指的就是操作数栈,注意不要和前面提到的虚拟机栈混为一谈。
- 对运行时常量池的引用(Runtime Constant Pool Reference):这里面主要保存对在方法中调用的方法的符号引用,这些符号引用会在运行时被解析成对方法的真正引用。
- 本地变量表(Local Variable):用于保存本地变量,这里的本地变量是指方法的入参以及在方法中声明的变量,如果在编译Java文件的时候加上”-g”参数,然后用”javap -p -verbose”打印出class文件后可以看到一个方法的本地变量表的内容,下面是一个简单的“Hello, world!”程序的main方法的本地变量表:
- 本地方法栈:本地方法栈的功能和虚拟机栈的作用相同,不同的是本地方法栈服务的对象是本地方法。
线程共享
线程共享这一块区域说起来就一块东西:堆;稍微细分一下可以分为方法区(Method Area)和”存放类实例的区域”
- 方法区:主要存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译以后的代码,其中运行时常量池也在方法区中。
- “存放类实例的区域”:这一部分内存是存放对象,所有实例化的对象都会放在这个地方。
直接内存
前面看到的都是JVM的运行时数据区的部分,JVM的内存中还有另外一部分叫直接内存(Direct Memory,对应上面的图中的最下方的地方),它不是JVM运行时数据区的一部分,不受JVM的GC管理。其中Java NIO中的Buffer直接在堆外分配的就属于这个部分。
