编程
Varamyr – 让Nook 2也可以轻松阅读中文的epub
2好啦,这个标题有点标题党的嫌疑,前几天入手了Nook 2,但是苦于Nook对中文支持并不好,看中文的epub直接显示方块或者问号之类的乱码。
网上看了下这个问题的大致解决方法有两个:一个是Root掉Nook;另一个往epub文件中塞入一段CSS,再在Nook中看的时候只需要选择Publisher Defaults就可以正常显示中文。
第一个方法风险太高,可能导致Nook变砖;
第二个方法的话可以用Calibre之类的工具来完成,但是Calibre这个东西本身太重了,每次打开关闭很耗时间,UI又丑,实在不想用。
所以,我需要一个命令行的工具来完成往epub中塞入一段CSS这个任务,这样可以和*nix下的其他工具结合起来使用,检测一个文件夹中如果新加入了epub文件,就直接进行转换,让整个过程完全自动化。现在这个命令行工具就是Varamyr(Varamyr是马丁大爷的冰与火之歌的第五部《魔龙的狂舞》序言里面的一个狼灵,打酱油的角色。)
运行Varamyr你需要
- JRE环境
- *nix系统,不支持windows也不会支持windows
- 下载varamyr.jar
如何使用?
- 运行命令:
java -jar varamyr.jar <path-to-epub-file>,<path-to-epub-file>为你需要修改的epub文件的路径 - 看看在你修改的epub文件的路径下是不是多了一个类似
xxxx-varamyr.epub的文件。
源代码 & 原理
- 源代码放在github上了:https://github.com/khotyn/Varamyr
- 原理很简单,不用讲了,直接看源代码吧。
New Year, New Slash-to-Search
0大家新年好,赶在2012来临之前,对之前写的一个Chrome小插件Slash-to-Search做了一些小的改进,新年新气象,如果你还在用这个插件,那么来看看吧:

What’s New?
- 选项页面有了新UI,如上图。
- 规则的添加和显示集成到了一起。
- 去除了规则到处功能,让插件更小巧。
《精通正则表达式》阅读笔记
3花了近三周的晚上的时间把《精通正则表达式》这本书看完了,说是看完,其实是只看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的可见性问题。
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直接在堆外分配的就属于这个部分。
