Archerda's Blog

Programmer. Meditating.


  • 首页

  • 归档

  • 标签

Google's Java Style

发表于 2015-09-23

为何需要编程规范

记得以前刚开始写C的时候,能写出来代码就好。后来看了好基友的代码后,感受到了满满的恶意,于是被教育了。后来开始慢慢理解编程规范的作用,以及它的必要性。编程规范多少算是软件工程领域里面的概念,很多程序员认为“编程规范是浪费时间的”、“我有自己的规范”等,其实我大部分是因为他们(她?)没有体会到编程规范所带来的好处。

按我的理解,遵循标准的编程规范有以下几个好处:

  • 容易理解代码。Always,如果一份代码遵循一定的规范,每个类、每个方法都声明了它的作用、参数意义等,那么很多时候我们只要看它的文件结构就知道了这些代码要干什么了。
  • 增加代码好感度。面对陌生的东西,我们都会感到害怕。而有统一的规范,你看一眼代码,就会觉得它们很眼熟。那么畏难心理自然消退。
  • 容易维护。

目前,自己主要在学习Java方面的技术,而且是个Google粉,所以尝试完全去遵循Google的Java规范。下面是它们的规范文档,中文是由Hawstein大神翻译的。实践篇尤其值得一读。

English

Google Java Style

中文

Hawstein’Blog:Google Java编程风格指南

什么是事务

发表于 2015-09-22

概述

事务是DBMS中业务操作的执行单位,事务的ACID性质是数据库一致性的基本保证。保障并发执行时事务满足ACID的技术就是数据库的并发控制,保障在数据库发生故障时依然满足ACID的技术就是数据库的故障恢复。并发控制和故障恢复是数据库系统管理的基本内容,所以事务概念和事务ACID性质也就成为了数据库管理的重要基础。

事务概念与性质

我们先来看一个例子:由账户A转账金额X到账户B。这是一个非常经典的例子。这个业务可以分解成2个基本的操作:

  1. 从A中减少金额X;
  2. 在B中添加金额X;

这2个动作构成了一个不可分割的整体,要么一起执行,要么都不执行。如果只执行前一个动作而忽略了后一个动作,那么将导致数据错乱。这种“不可分割”的业务单位对于数据库业务的并发控制和故障恢复非常重要、非常必要,这就是“事务”的基本概念。

事务概念

事务(transaction),是DBMS中的基本执行单位。根本特征在于集中了数据库应用方面的若干操作,这些操作构成一个操作序列,要么全做,要么全不做,整个序列是一个不可分割的“原子化”单位。

在数据库系统中,一个事务是指:由一系列数据库操作组成的一个完整的逻辑过程。例如银行转帐,从原账户扣除金额,以及向目标账户添加金额,这两个数据库操作的总和,构成一个完整的逻辑过程,不可拆分。

事务性质

在数据库事务处理过程中,事务的正常状态是由“ACID”性质予以保证的。

  1. 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  2. 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  3. 隔离性(Isolation):多个事务并发执行与这些事务单独执行的结果“等效”。当两个或者多个事务并发访问(此处访问指查询和修改的操作)数据库的同一数据时所表现出的相互关系。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  4. 持久性(Durability):在事务完成以后,该事务对数据库所作的更改便持久地保存在数据库之中,并且是完全的。

事务操作与状态

在数据库运行过程中,事务可以由下述的4个基本部分组成:

  1. 开始(begin):开始执行事务。
  2. 执行(read and write):事务对数据进行读或写操作。
  3. 提交(commit):事务完成所有操作,同时保存结果,标志着事务的成功完成。
  4. 回滚(rollback):事务未完成所有所做,重新返回到事务开始,标志着事务的撤销。

根据事务的上述操作,可以得到事务的各种状态:

  1. 活动状态(active):事务处于运行当中。
  2. 局部提交状态(partial committed):表明事务的最后语句已经被执行。
  3. 提交状态(committed):事务执行成功,执行结果写入到数据库中。
  4. 失败状态(failed):事务无法正常进行。
  5. 终止状态(abort):回到事务执行前的初始状态。

事务操作与状态之间的关系如下图:
事务操作与状态之间的关系

可以得出以下结论:

  • 事务一般由“事务开始”启动,到“事务提交”或“事务回滚”结束。
  • 在事务开始后,它不断做READ或WRITE操作,但此时WRITE操作仅将数据写入磁盘缓冲区,并不是真正写入到数据库中。
  • 在事务执行过程中会产生2种情况:一是顺利执行,此时事务继续执行其后的操作;二是产生故障等原因而终止。

SQL事务机制

事务处理语句

  1. 事务开始语句:BEGIN TRANSACTION.
  2. 事务提交语句:COMMIT TRANSACTION.
  3. 事务回滚语句:ROLLBACK TRANSACTION.
  4. 事务存储点语句:SAVE TRANSACTION, RELEASE TRANSACTION.

参考文档

  • 数据库系统教程 叶小平著 清华大学出版社
  • 维基百科:ACID

JVM 如何确定对象已死

发表于 2015-09-21

在Java堆里面存放着Java世界中几乎所有的对象实例,GC前,第一件事情就是确定这些对象中哪些嗨“存活”着,哪些已经“死去”(即不可能再被使用的对象)。

引用计数算法

一个常用的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它的时候,这个计数器+1;当引用失效的时候,这个计数器-1.当计数器=0的时候,则说明这个对象不可能再被使用。这个叫“引用计数算法”(Reference Counting)。

客观地说,RC算法的实现简单,效率也很高,在大部分情况下它都是一个不错的算法(Objective-C中就使用它来管理内存)。然后JVM中却没有使用它来管理内存,其中最主要的原因就是RC算法无法处理循环引用的问题(还有一个问题就是频繁的更新引用计数会降低运行效率)。

可达性分析算法

在主流的商业程序语言(Java、C#、甚至古老的Lisp)的主流实现中,都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路是:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没用任何引用链相连时(从图论的角度来看,就是GC Roots到这个对象不可达),则说明这个对象是不可用的。如下图所示,,对象object5、object6、objectt7虽然互相关联,但是它们到GC Roots是不可达的,所以它们会被判定是可回收对象。
可达性分析算法判定对象是否可回收

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 本地方法栈中JNI(即一般说的native方法)引用的对象;(ps.栈中数据不受GC影响)
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;

再谈引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法来判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK1.2之前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用2种状态,对于一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能够保存在内存中;如果内存空间在进行GC后非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的场景。

在JDK1.2后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用的引用强度逐渐减弱。

类型 作用
强引用 就是指在代码中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,GC永远不会回收它们;
软引用 用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之内进行二次GC。如果这次GC后还没有足够的内存,才会抛出OOM异常。在JDK1.2后,提供了SoftReference类来实现软引用;
弱引用 也是用来描述非必需的对象的,但是它的强度比弱引用更弱一些,被弱引用关联的对象只能生存到下一次GC之前。当GC的时候,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2后,提供了WeakReference类来实现弱引用;
虚引用 也称为幽灵或者幻影引用。它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过一个虚引用来取得一个对象实例。为一个对象引入虚引用关联的唯一目的就是能在这个对象被回收器回收之前收到一个系统通知。在JDK1.2后,提供了PhantomReference类实现虚引用。

生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经过两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。然后这个对象会放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalize线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不会承诺会等待它运行结束,原因是如果一个对象的finalize()方法执行缓慢,或者发生了死循环,将很可能导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()中成功拯救自己—只要重新与引用链上的任何一个对象建立关联即可,比如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那么在第二次标记的时候它将被移除“即将回收”的队列;如果这个对象这个时候还没有逃脱,那基本上它就真的被回收了。来看演示代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.archerda.oom;
/**
* 代码演示的目的有2:1
* 1.对象可以在GC的时候自救;
* 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统调用一次
*
* Created by Archerda on 15/9/21.
*/
public class FinalizeEscapeGC {
public static Object SAVE_HOOK = null;
@Override
protected void finalize() throws Throwable{
super.finalize();
System.out.println("finalize method executed.");
FinalizeEscapeGC.SAVE_HOOK = this; // 拯救自己
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
SAVE_HOOK = null;
System.gc();
Thread.sleep(500); // 因为finalize线程的优先级很低,所有暂停0.5s来等地它
if(SAVE_HOOK != null) {
System.out.println("Yes, I'm still alive.");
} else {
System.out.println("No, I'm dead.");
}
}
}

运行结果:

1
2
finalize method executed.
Yes, I'm still alive.

从上面的运行结果可以看出,SAVE_HOOK对象的finalize()方法确实被垃圾回收器触发过,并且在回收之前成功逃脱了。

如果我们把FinalizeEscapeGC.SAVE_HOOK = this 注释掉,那么运行结果:

1
2
finalize method executed.
No, I'm dead.

这个时候SAVE_HOOK没有重新关联到引用链中,所以没有逃脱GC的回收。

还有个现象,我们把上面代码的main()方法改下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
SAVE_HOOK = null;
System.gc();
Thread.sleep(500); // 因为finalize线程的优先级很低,所有暂停0.5s来等地它
if(SAVE_HOOK != null) {
System.out.println("Yes, I'm still alive.");
} else {
System.out.println("No, I'm dead.");
}
// 下面这段代码与上面的完全一样,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
Thread.sleep(500); // 因为finalize线程的优先级很低,所有暂停0.5s来等地它
if(SAVE_HOOK != null) {
System.out.println("Yes, I'm still alive.");
} else {
System.out.println("No, I'm dead.");
}
}

运行结果:

1
2
3
finalize method executed.
Yes, I'm still alive.
No, I'm dead.

代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次逃脱失败。这时因为一个对象的finalize()方法都只会被系统自动调通一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败。也就是说,对象只能自救一次。

值得注意的是,建议大家尽量避免使用finalize()方法。因为他不是C/C++中的析构函数,而是Java刚诞生的时候为了使C/C++程序员更容易接受它所做出的一个妥协。它的运行代价高昂、不确定性大,无法保证各个对象的调用顺序。有些教材中描述它适合做“关闭外部资源”之类的工作,这完全是对这个方法用途的一种安慰,finalize()方法能做的所有工作,使用try-catch或者其他方式都可以做的更好、更及时。所以,忘记这个方法吧!!!

回收方法区

很多人认为方法区(或者说HotSpot虚拟机中的永久代)是没有垃圾回收的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区中实现垃圾回收,而且在方法区中进行垃圾回收的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾回收一般可以回收70%~95%的空间,而永久代中的垃圾收集效率却远低于此。

永久代中的垃圾回收主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例:假如一个字符串“abc”已经进入常量池了,当时当前系统没有任何一个String对象是叫做“abc”,也没有其他地方引用了这个字面量,如果这时发生了GC,而且必要的话,这个“abc”常量就回被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否“无用”的条件则要苛刻很多。类需要同时满足下面3个条件才能算是“无用的类”:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
  2. 加载该类的ClassLoader已经被回收;
  3. 该类队形的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里所说的“可以”,而并不是和对象一样,对象的是不使用了就必然会回收。

在大量使用反射、动态代理、CGLib等字节码技术框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备卸载类的功能,以保证虚拟机永久代不会溢出。


参考文档

  • 深入理解Java虚拟机(第二版) 周志明著
  • 维基百科:引用计数
  • 维基百科:定義可達性

JVM 内存溢出异常实例

发表于 2015-09-20

在JVM规范的描述中,除了PC外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能性。为了对这些异常进一步理解,我们将通过若干实例来验证异常发生的场景,并且会介绍几个与内存相关的最基本的虚拟机参数。

其实写记录这篇文章的原因有2个:

  1. 通过代码验证Java虚拟机规范中描述的各个运行时区域存储的内容;
  2. 希望以后在工作中遇到实际的内存溢出异常是,能根据异常的信息快速判断是那个区域的内存溢出,知道什么样的代码可能会导致这些区域内存溢出,以及出现异常后该如何处理。

首先,为了避免每次启动程序都需要手动敲入JVM参数,我们用Intellij IDEA来测试,并且在RUN/DEBUG Configurations---VM options中键入(参数说明请看这里:JVM-参数分析)

-verbose:gc -Xms20M -Xmx20M -Xmn10M
-XX:+PrintGCDetails -XX:SurvivorRatio=8

如下图。
在Intellij IDEA中设置虚拟机参数

Java堆溢出

Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免GC来清除这些对象,那么在对象数量达到最大堆的容量限制后就回产生内存溢出异常。

在下面的代码中,我们限制饿了Java堆的大小为20M,不可扩展(将堆的初始值-Xms与最大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoeyError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.archerda.oom;
import java.util.ArrayList;
import java.util.List;
/**
* VM options: -Xms20m -Xmx20m -XX:HeapDumpOnOutOfMemoryError
* Created by Archerda on 15/9/20.
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}

输出结果:

1
2
3
4
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid23366.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Heap dump file created [27701731 bytes in 0.202 secs]

Java堆内存的OOM异常是实际应用中常见的内存溢出异常情况。当出现Java堆内存溢出异常时,异常堆栈信息java.lang.OutOfMemoryError会跟着进一步提示Java heap space。

要解决这个区域的异常,一般的手段是先通过内存映像分析工具Jstat工具来分析下。首先找到堆转储文件java_pid23366.hprof,默认在工程的根目录下,也可以通过-XX:+HeapDumpOnOutOfMemoeyError=< file-path >来指定。然后运行jstat java_pid23366.hprof,jstat分析完成后会生成html文件并启动服务器,我们访问默认端口localhost:7000打开分析结果页面,默认会显示功能列表,我们主要关注的是有多少个实例存在导致了内存异常,所以我们点击链接Show instance counts for all classes (excluding platform)。如下图。

然后会调到下面这个页面,从这张图中我们可以很清晰的看见堆中竟然存在810326个OOMObject对象,由此问题的根源就显而易见了。

除了用jstat这个命令行工具外,我们还可以使用jvisualvm这个可视化工具来分析,只要把.hprof文件导入进去就可以了。如下图。
利用jvisualvm分析堆转储文件

如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致GC无法自动回收它们的。掌握了泄露对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄露代码的位置。

如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的参数(-Xms与-Xmx),与物理机内存对比看是否还可以调大,从代码检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行时期的内存消耗。

虚拟机栈和本地方法栈溢出

由于在HotSpot虚拟机中不区分虚拟机栈和本地方法栈,因此,对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但是毫无意义,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了2中异常:

  1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;
  2. 如果虚拟机在扩展时无法申请到足够的内存空间,将抛出OutOfMemoryError异常;

这里把异常分为2中情况,看似更加严谨,但却存在一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上都是对同一件事情的两种描述而已。

在实验中,将范围限制于单线程中的操作,尝试了下面2中方法均无法让虚拟机产生OutOfMemoryError异常,尝试的结果都是获得StackOverflowError异常,测试代码如下:

  1. 使用-Xss参数减少栈内存容量。结果:抛出StackOverflowError异常,异常出现时将输出栈的深度;
  2. 定义了大量的本地变量,增大此方法帧中本地变量表的长度,结果:抛出StackOverflowError异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.archerda.oom;
/**
* VM Options: -Xss160k
* Created by Archerda on 15/9/20.
*/
public class JavaVMStackOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
JavaVMStackOF oom = new JavaVMStackOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length: " + oom.stackLength);
throw e;
}
}
}

输出结果:

1
2
3
4
5
stack length: 751
Exception in thread "main" java.lang.StackOverflowError
at com.archerda.oom.JavaVMStackOF.stackLeak(JavaVMStackOF.java:11)
at com.archerda.oom.JavaVMStackOF.stackLeak(JavaVMStackOF.java:12)
...后面省略

结果表明:在单个线程的情况下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。

方法区和运行时常量池溢出

由于运行时常量池是方法区的一部分,因此这2个区域的溢出测试就放在一齐进行。之前说过JDK1.7开始逐步”去永久代”的事情,在此就以测试代码观察一下这件事对程序的实际影响。

String.intern()是一个native方法,它的作用是:如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK1.6及之前的版本中,由于常量池分配在永久代中,我们可以通过-XX:PermSIze和-XX:MaxPernSize限制方法区大小,从而间接限制其中常量池的容量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.archerda.oom;
import java.util.ArrayList;
import java.util.List;
/**
* JDK 1.6中有效,JDK1.7中无效。
* VM Options: -XX:PermSize=10m -XX:MaxPermSize=10m
* Created by Archerda on 15/9/20.
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>(); // 使用List保持常量池的引用,避免FullGC回收常量池行为.
int i = 0;
while (true) {
list.add(String.valueOf(i).intern()); // 10MB的PermSize在integer范围内足够产生OOM了.
}
}
}

运行结果:

1
2
3
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.archerda.oom.RuntimeConstantPoolOOM...

从运行结果中可以看出,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是”PermGen space“,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。

而使用JDK1.7运行这段程序就不会等到相同的结果,while循环将一直循环下去。关于这个字符串常量池的实现问题,还可以引申出一个有意思的影响,看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.archerda.oom;
/**
* Created by Archerda on 15/9/20.
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}

JDK1.6运行结果:

1
2
false
false

JDK1.7运行结果:

1
2
true
false

分析:

在1.6中,intern()方法会把首次遇到的字符串实例复制到常量池中,返回的也是常量池中的这个字符串实例的引用,而StringBuilder创建的字符串在堆上,所有必然不是同一个引用,所以2个都是false。

而在1.7中,intern()实现不会再复制,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串是同一个。对于str2比较返回是false是因为”java”这个字符串在执行StringBuilder.toString()之前已经出现过(???哪里出现了卧槽,虚拟机加载的关键字么,可是pack+age还是返回true啊),字符串常量池已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串是首次出现的。

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这些区域的测试,基本的思路是运行时生成大量的类去填满方法区,直到溢出。虽然直接使用Java SE API也可以动态产生类(如反射时的GeneratedConstructorAccessor和动态代理等),但我们这次使用CGLib直接操作字节码运行时生成大量的动态类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.archerda.oom;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* Created by Archerda on 15/9/20.
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}

运行结果:

1
2
Caused by: java.lang.OutOfMemoryError: PernGen space
at...

值得注意的是,我们在这个例子中的模拟场景并不是一个纯碎的实验,这样的应用经常会出现在实际的应用之中:当前很多主流的框架,如Spring、Hibernate,在对类进行增强的时候,都是使用CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以载入内存。

方法区溢出也是一种常见的内存溢出异常,一个类要被GC回收掉,判定条件是比较苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收情况。这类场景除了CGLib字节码增强之外,常见的还有:大量JSP或动态产生JSP文件的应用(JSP第一次运行需要被编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的类加载器加载会视为不同的类)等。


参考文档

  • 深入理解Java虚拟机(第二版) 周志明著

进程与线程是何关系

发表于 2015-09-16

进程和线程的相关内容,探究起来比较复杂,这里就先了解和记录下一些比较基础的概念和相关知识,等以后学习Linux内核相关的代码时再详细记录吧。

什么是进程

进程,是可并发执行的程序在一个数据集上的一次执行过程,它是系统资源分配的基本单位。从结构上看,进程实体是由 程序段、数据段和进程控制块(PCB)三部分组成,这三部分也被统称为”进程映像”或”进程上下文”。

进程状态

在一个进程的活动期间,它至少具备三种基本状态:就绪状态、执行状态、等待状态。

进程控制块

每一个进程有且只有一个进程控制块(Process Control Block, PCB),进程控制块是操作系统用于记录和描述进程状态及相关信息的数据结构,也是操作系统控制和管理进程的主要依据。PCB是进程存在的唯一标志。操作系统的并发执行也是根据PCB来进行控制和管理的。

进程队列

通常把处于相同状态的进程链接在一起,称为”进程队列”。比如若干个等待执行的进程(就绪进程)按一定的次序链接起来的队列称为”就绪队列”。把等待资源或等待某些时间的进程也排成队列,称为”等待队列”。

链接方式:单向链接和双向链接,如下图。

索引方式:

进程调度

在多道程序设计的系统中,往往有多个进程处于就绪状态,它们都要求占用CPU执行。但是,一个处理器每一时刻只能让一个进程占用。所以操作系统设计了一个“进程调度”来解决竞争CPU的问题。

进程调度的主要功能有:

  • 记录系统中所有进程的执行情况;
  • 选择占有CPU的进程;
  • 把CPU分配给进程,并进行进程上下文切换;
  • 收回CPU;

进程调度的时机:

  1. 正在执行的进程执行完毕;
  2. 执行中的进程被阻塞;
  3. 在分时系统中时间片用完;
  4. 在执行完系统调用等系统程序返回用户进程时;
  5. 执行中的进程被优先级更高的进程剥夺CPU;

进程调度算法:

  1. 先来先服务算法(FCFS)
  2. 优先数调度算法
  3. 时间片轮转调度算法
  4. 多级反馈队列调度算法

什么是线程

线程(Thread),是进程中的一个实体,是CPU调度的基本单位。一个进程可以有一个或多个线程,它们共享所属进程所拥有的资源。线程具有一下特性:

  1. 多个线程可以并发执行;
  2. 一个线程可以创建另一个线程;
  3. 线程具有动态性。一个线程被创建之后便开始了它的生命周期,期间可能处于不同的状态,直至死亡;
  4. 每个线程具有自己的线程控制块(Thread Controlling Block, TCB),其中记录了该线程的标识符、线程执行时的寄存器和栈等现场状态信息;
  5. 在同一个进程内,各线程共享同一地址空间(即所属进程的存储空间);
  6. 一个进程中的线程在另一个进程中是不可见的;
  7. 同一个进程内的线程间的通信主要是基于全局变量进行的;

线程的分类

多线程的实现分为三类:内核级线程(Kernel Level Thread, KLT)、用户级线程(User Level Thread, ULT)、混合式线程,即同时支持ULT和KLT两种线程。

进程与线程结构

引入线程后,一个进程可包括一个或多个线程。如果一个进程只包括一个线程,则该进程除了有自己的PCB、拥有的存储空间、栈(每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈)以外,还有对应的TCB。如下图所示。

而如果一个进程包含了多个线程,该进程也包括自己的PCB、存储空间、栈以及各个线程的TCB,但是每个线程将拥有自己的栈,这些栈都数据该进程的栈。如下图。


参考文档

  • 操作系统教程 谢旭升著 机械工业出版社
  • 阮一峰的网络日志:进程与线程的一个简单解释

详解HTTP超文本传送协议

发表于 2015-09-16

简介

HTTP协议定义了客户端怎么向服务器请求万维网文档,以及服务器怎么把文档传送到客户端。从层次的角度看,HTTP是面向事务(一系列的信息交换,而这一系列的信息交换是不可分割的,要么所有信息交换都完成,要么一次交换都不进行)的应用层协议,它是万维网上能够可靠地交换文件(包括文本、声音、图像等各种多媒体文件)的重要基础。

工作过程

万维网的工作过程如图:

每个万维网网点都有一个服务器进程,它不断地监听TCP的80端口,以便发现是否有客户端向他发出建立连接请求。一旦监听到连接请求并建立了TCP连接后,客户端就向服务器发出浏览某个页面的请求,服务器接着就返回所请求的页面作为响应。最后,TCP连接被释放了。

在客户端和服务器之间的请求和响应的交互,必须按照规定的格式和遵循一定的规则。这个格式和规则就是超文本传送协议HTTP。

HTTP规定在客户端和服务器之间的每次交互,都由一个ASCII码串构成的请求和一个类似的通用因特网邮件扩充,即”类MIME(MIME-like)”的响应组成。

实例分析

用户浏览页面的方式有2种:一种是在浏览器的地址窗口中键入所要找的页面的URL。另一种是在某一个页面中用鼠标点击一个可选部分,这时浏览器会自动跳转在因特网上找到所要链接的页面。

假定上图中的用户用鼠标点击了屏幕上的一个可选部分。它使用的链接指向了一个URLhttp:///www.tsinghua.edu.cn/chn/yxsz/index.html,下面我们用HTTTP/1.0更具体地说明在用户点击鼠标后所发生的几件事件:

  1. 浏览器分析链接指向页面的URL;
  2. 浏览器向DNS请求解析www.tsinghua.edu.cn的IP地址;
  3. 域名系统DNS解析出清华大学服务器的IP是166.111.4.100;
  4. 浏览器与服务器建立TCP连接(在服务器端IP是166.111.4.100,端口是80);
  5. 浏览器发出取文件命令:GET /chn/yxsz/index.html;
  6. 服务器www.tsinghua.edu.cn给出响应,把文件index.html发送给浏览器;
  7. 释放TCP连接;
  8. 浏览器显示文件index.html中所有的文本。

浏览器在下载文件时,可以设置为只下载其中的文本部分。这样可以使得下载的速度加快。在这种情况下,文件中原来嵌入的图像或者音频的地方只用一个小图标来显示。用户若要下载这些图像或音频,可用鼠标再分别点击这些图标。每点击一次鼠标,就重复执行一次类似与上面的8个步骤。也就是先建立TCP连接,再使用TCP连接传送命令和传送文件,最后释放TCP连接。

特性

HTTP通常使用TCP连接传送。HTTP使用了面向连接的TCP作为传输层协议,保证了数据的可靠传输。HTTP不必考虑数据在传输过程中被丢弃后又怎样被重传。但是,HTTP协议本身是无连接的。这就是说,虽然HTTP使用了TCP连接,但通信的双方在交换HTTP报文之前不需要先建立HTTP连接。

HTTP协议是无状态的(stateless)。也就是说,同一个客户第二次访问同一个服务器上的页面时,服务器的响应与第一次被访问时的相同(假定现在服务器还没有把页面更新),因为服务器并不记得曾经访问过的客户,也不记得为该客户服务过多少次。HTTP的无状态特性简化了服务器的设计,使服务器更容易支持大量并发的HTTP请求。

请求一个文档所需时间

下面我们粗略估算一下,从浏览器请求一个万维网文档到收到整个文档所需要的时间。如下图。用户在点击鼠标链接某个万维网文档时,HTTP协议首先要和服务器建立TCP连接。这需要使用三次握手。当三次握手的前两部分完成后(即经过一个RTT时间后),万维网客户就把HTTP请求报文作为第三次握手的第三个报文的数据发送到服务器(第三次握手可以携带数据,但需要消耗一个序号)。服务器收到请求报文后,就把所请求的文档作为响应报文返回给客户。

从上图可看出,请求一个万维网文档所需要的时间是该文档的传输时间(与文档大小成正比)加上两倍往返时间RTT(一个RTT用于TCP连接,另一个RTT用于请求和接收文档。这里TCP建立连接的三次握手的第三个报文段捎带了客户对万维网文档的请求)。

请求文档时间 = 2 * RTT + 文档传输时间

不足与解决方案

HTTP/1.0 的主要缺点,就是每请求一个文档就要有两倍RTT的开销。若一个主页上有很多链接的对象(如图片等),那么每一次链接下载都导致2RTT的开销。特别是万维网服务器往往要同时服务于大量的客户请求,所以这种 *非持续性连接 会使万维网服务器的负担很重。好在浏览器都提供了能够打开5~10个并行的TCP连接,而每一个TCP连接处理客户的一个请求。因此,使用并行的TCP连接可以缩短响应时间(然而服务端的负担没有任何减轻)。

HTTP/1.1 协议较好地解决了这个问题,它使用持续连接(persistent connection)。所谓持续连接就是万维网服务器在发送响应后仍然在一段时间内保持这条连接,使同一个客户(浏览器)和该服务器可以继续在这条连接上传送后续的HTTP请求报文和响应报文。这并不局限于传送同一个页面上链接的文档,而是只要这些文档在同一个服务器上就行了。目前大部分浏览器的默认设置就是使用HTTP/1.1.

HTTP/1.1协议的持续连接有2中工作方式:非流水线方式(without pipelining)和流水线方式(with pipelining)。

非流水线方式的特点,是客户在收到前一个响应后才能发出下一个请求。因此,在TCP连接已经建立后,客户每访问一次对象都要用去一个往返时间RTT。这比非持续性连接要用去2个RTT时间,节省了建立TCP连接的一个RTT时间。但非流水方式还是有缺点的,因为服务器在发送完一个对象后,其TCP连接就处于空闲状态,浪费了服务器资源。

流水线方式的特点,是客户在收到HTTP的响应报文之前就能够接着发送新的请求报文。于是一个接一个的请求报文到达服务器后,服务器就可连续发回响应报文。因此,使用流水线方式时,客户访问所有的对象只需要花费一个RTT时间。流水线工作方式使TCP连接中的空闲时间减少,提高了下载文档的效率。

HTTP请求报文的常用方法

方法 含义
GET 请求读取由URL所标识的信息
POST 给服务器发送信息
OPTION 请求一些选项信息
HEAD 请求读取由URL所标识的信息的首部
PUT 在指明的URL中存储一个文档
DELETE 删除指明URL所标识的资源
TRACE 用来进行环回测试的请求报文
CONNECT 用于代理服务器

状态码

  • 1xx消息——请求已被服务器接收,继续处理
  • 2xx成功——请求已成功被服务器接收、理解、并接受
  • 3xx重定向——需要后续操作才能完成这一请求
    • 301:重定向,资源永久移动,客户端不应该再继续通过该 url 访问该资源
    • 302:重定向,资源临时移动,以后可能仍然使用该 url
    • 304:not modified,浏览器可以使用本地缓存
  • 4xx请求错误——请求含有词法错误或者无法被执行
    • 400:bad request,客户发送的请求不能理解
    • 403:forbidden,无权限,服务器拒绝提供服务
    • 404:not found,没找到资源
  • 5xx服务器错误——服务器在处理某个正确请求时发生错误
    • 500:internal server error,服务器内部错误
    • 502 – 网关错误。常见的情况是反向代理后端的服务器(比如resin或tomcat)没有启动。
    • 503:server unavaliable,服务暂不可用
    • 504 – 网关超时。比如请求时长超出了服务器的响应时间限制

参考文档

  • 计算机网路(第六版) 谢希仁著 电子工业出版社
  • 维基百科:HTTP持久连接
  • 克鲁斯卡尔的博客:常见的HTTP状态码

TCP释放连接-四次挥手

发表于 2015-09-16

简介

TCP连接释放过程比较复杂,我们需要结合双方状态去理解。

四次挥手图

步骤

数据传输结束后,通信双方都可以释放连接。现在A和B都处于ESTABLISHED状态。步骤如下:

  • A的应用进程先向其TCP发出连接释放报文段,并停止发送数据,主动关闭TCP连接。A把连接释放报文段(FIN包)首部的终止控制位FIN = 1,其序号seq = u,它等于前面已传送过的数据的最后一个字节+1。这时A进入FIN-WAIT-1状态。注意,FIN报文段即使不携带数据,也要消耗一个序号。
  • B收到连接释放报文段后立即发出确认,确认号ack = u + 1,这个报文段自己的 序号seq = v,等于B前面已传送过的数据的最后一个字节的序号+1。然后B就进入CLOSE-WAIT(关闭等待)状态。TCP服务器进程这时应通知高层应用进程,因而从A到B这个方向的连接就释放了,这时的TCP连接处于半关闭(half-close)状态,也就是A已经没有数据要发送了,但是B若发送数据,A仍要接收。也就是说,从B到A这个方向的连接并未关闭,这个状态可能会持续一些时间。
  • A接收来自B的确认后,就进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文段。
  • 直到B已经没有要向A发送的数据,其应用进程就通知TCP释放连接。这时B发出的连接释放报文段必须使FIN = 1。假定B的序号seq = w(在半关闭状态B可能还发送了一些数据)。B还必须重复确认上次发送过的确认号ack = u + 1。这时B进入LAST-ACK(最后确认)状态,等待A的确认。
  • A收到来自B的连接释放报文后,必须对此发出确认。在确认报文段中设置ACK = 1,确认号ack = w + 1,而自己的序号为seq = u + 1(根据TCP标准,前面发送过的FIN报文段要消耗一个序号)。然后进入TIME-WAIT(时间等待)状态。请注意,现在TCP连接还没有释放掉,必须经过时间等待计数器设置的时间2MSL(MSL,Maximum Segment Lifetime,叫做最长报文段寿命,RFC793建议设置为2分钟.)后因此,从A进入到TIME-WAIT后,要经过4分钟才能进入CLOSE状态,才能开始建立下一个新的连接。
  • B接收到来自A的确认报文段,进入CLOSE状态。

疑问

  • 为什么A在TIME-WAIT状态必须等待2MSL的时间?
    1. 为了保证A发送的最后一个ACK报文段能够到达B。这个ACK报文段极有可能丢失,因而使得处在LAST-ACK状态的B收不到确认。B会超时重传FIN+ACK报文段,而A就能在2MSL时间内收到这个重传报文。接着A重传一次确认,重新启动2MSL计时器。最后A和B都能正常进入到CLOSED状态。相反滴,如果A在TIME-WAIT不等待一段时间,而是发送完ACK报文段后就立即释放连接进入CLOSED状态,那么就无法收到B重传的FIN+ACK报文段,因而也不会再发送一次确认报文段。这样,B就无法释放连接了。
    2. 防止”已失效的连接请求报文段”出现在本连接中。经过时间2MSL,就可以使本连接的时间所产生的所有报文段从网络中消失,这样就可以使下一个新的连接中不会出现旧的连接请求报文段。

参考文档

  • 计算机网络(第六版) 谢希仁著 电子工业出版社

TCP建立连接-Three-way Handshake

发表于 2015-09-16

简介

TCP 是面向连接的协议。运输连接是用来传送TCP报文的。TCP运输连接的建立和释放是每一次面向连接的通信中必不可少的过程。因此,运输连接就有3个阶段,即:建立连接、数据传送、释放连接。运输连接的管理就是使运输连接的建立和释放都能够正常进行。

在TCP建立连接过程中主要要解决3个问题:

  1. 要使每一方都能够确认对方的存在。
  2. 要允许双方协商一些参数(如最大窗口值、是否使用窗口扩大选项和时间戳选项以及服务质量等)。
  3. 能够对传输实体资源(如缓存大小等)进行分配。

TCP连接的建立采用客户服务器方式。主动发起连接建立的应用进程叫做客户(client),而被动等待连接建立的应用进程叫做服务器(server)。

三次握手图

步骤

上图画出了TCP的建立连接的过程。假定主机A运行的是TCP客户程序,而主机B运行的是TCP服务器程序。最初两端的TCP进程都处于CLOSED(关闭)状态。图中在主机下面的方框是TCP进程所处的状态。需要注意的是,A主动打开连接,而B被动打开连接。

连接过程如下:

  1. B的TCP服务器进程先创建传输控制块TCB(Transmission Control Block),准备接受客户进程的连接请求。然后服务器进程就处于LISTEN(监听)状态,等待客户的连接请求。如有,立即做出响应。
  2. A的TCP客户进程也是首先创建TCB,然后想B发出连接请求的报文段(SYN包,表明需要得到回应),这时SYN = 1,同时选择一个初始序列号seq = x。该报文不能携带数据但要消耗一个序号。这时,TCP客户进程进入SYN-SENT(同步已发送)状态。(ps.同步的意思就是想要收到回应。)
  3. B收到连接请求报文段后,如果同意连接,则向A发送确认。在确认报文段(SYN+ACK包,回应确认连接并需要得到回应)中应设置SYN = 1, ACK = 1,并设置确认号ack = x + 1,同时自己也选择一个初始序号seq = y。注意该报文段也不能携带数据并需要消耗一个序号。这时TCP服务器进程进入SYN-RCVD状态。
  4. TCP客户进程收到B确认后,还要回应B的请求。确认报文段(ACK包)中ACK = 1, ack = y + 1,而自己的序号为seq = x + 1。这个时候,TCP标准规定,ACK报文段可以携带数据,但如果不携带则不消耗序号(这时下一个报文段的序号仍是seq = x + 1)。这时,TCP连接已经建立,A进入ESTAB-LISHED状态。
  5. 当B收到A的确认后,也进入ESTAB-LISHEB状态。

疑问

  1. 为什么A还要发送一次确认呢(也就是第三次握手)?

简单来说,是为了”让两边的请求都能被识别到,所以逻辑上需要3次”。

详细的说, 主要是为了”防止已经失效的连接请求报文段突然又传送到B,因而发生错误”。考虑这么一种情况:A发出连接请求,但因连接请求报文丢失而未收到确认。于是A再重传一次连接请求,后来收到了确认,建立了连接。数据传输完毕后,就释放了连接。A共发送了2个连接请求报文段,其中一个丢失,第二个到达了B。没有“已失效的连接请求报文段”。 现在,我们假定出现了一种异常情况,即A发出的第一个连接请求报文段并没有丢失,而是在某个网络节点长时间滞留了,以致延误到连接释放以后的某个时间才到达B。本来这是一个早已失效的报文段。但是B收到此失效的连接报文段后,误以为是A又发出了一个新的连接请求。于是就向A发送了确认报文段,同意建立连接。假定不采用三次握手,那么只要B确认,新的连接就建立了。

由于现在A并没有发送建立连接的请求,因此不予理会B的确认,也不会向B发送数据。但是B却以为连接已经建立,并一直等待A发来数据,B的许多资源就这样浪费了。

采用第三次握手的办法可以防止上述现象的发生。例如在刚才的情况下,A不会向B的请求确认发出确认。B由于收不到确认,就知道A并没有要求建立连接。


参考文档

  • 计算机网络(第六版) 谢希仁著 电子工业出版社

7种经典排序算法总结和实现

发表于 2015-09-15

基于比较的7种排序算法

冒泡排序BubbleSort

介绍

冒泡排序的原理比较简单,主要是两两比较,每次把比较大的数据放在后面,这样一次下来,最大的数就放在数组最后了。然后依次类推。

主要特性如下:

排序算法 平均情况 最好情况 最坏情况 辅助空间 稳定性
冒泡排序 O(n^2) O(n) O(n^2) O(1) 稳定

步骤

  • 从索引为0的位置开始,往后依次两两比较,把大的数放在后面,小的数放在前面,这样一次下来最大的数就在数组末尾了(n);
  • 最后一个数完成排序,退出排序工作,也就是令n–;
  • 重复第一步,直到n=0;

源码

1
2
3
4
5
6
7
8
9
10
11
12
public static void bubbleSort(int[] a){
int temp = 0;
for(int i = 0; i < a.length - 1; i++){// 遍历第i次,每一次排好后面的1个
for(int j = 0; j < a.length - i - 1; j++){// 从a[0]开始,依次去后面尚未排序的元素两两比较,大的置后
if(a[j] > a[j + 1]){
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}

优化方案

  • 每次排序,如果没有发生任何一次交换位置,说明已经是有序的了。因此可以用一个标识位记录是否交换了位置,如果没有则直接结束排序工作。
阅读全文 »

Java 正则表达式详解

发表于 2015-09-11

概述

在实际开发过程中,总会遇到很多有关字符串的查找、匹配、替换、判断等操作(参加中兴的笔试时也遇到一道相关题目,泪~~),而有时候情况还比较复杂,如果直接用编程的方式来处理,代码量稍多且麻烦,往往效率低下。而这个时候,正则表达式(Regex)就是解决这类问题的利器。

正则表达式是一种模式匹配和替换的规范,一个正则就是由普通的字符(如a-zA-Z0-9)以及特殊字符(元字符)组成的文字模式,它用以描述文字主体的一个或者多个待匹配的字符。正则表达式作为表达式的一个模板,将某个字符模式与所给字符主题进行匹配。

需要说明的是,几乎每个语言都提供了正则表达式的功能,但不同语言之间的正则表达式可能略微有些差别。本文主要讲解Java中的正则表达式。

核心类

有关正则表达式的类在java.util.regex中,主要包括三个类:Pattern、Matcher、PatternSyntaxException。

Pattern(模式)

pattern对象是一个正则表达式的编译表示形式。指定为字符串的正则表达式必须先编译为此类的实例。然后,将得到的模式用于创建Matcher对象(匹配器)。依照正则表达式,该对象可以和任意的字符序列匹配。执行匹配所涉及的状态都驻留在匹配器中,所以多个匹配器可以共用一个模式。

  • 临时使用Pattern
1
public static boolean matches(String regex, CharSequence input)

matches()方法编译给定正则表达式并尝试将给定输出与其匹配。其中regex是要编译的表达式,input是要匹配的字符序列。调用此便捷方法的形式如下:

1
boolean b = Pattern.matches("a*b", "aaaaab");

  • 复用Pattern
1
2
public static Pattern compile(String regex)
public Matcher matcher(CharSequence input)

其中compile将给定的正则表达式编译到模式中,而matcher则根据给定的字符序列创建与此模式的匹配器。示例如下:

1
2
3
Pattern p = Pattern.compile("a*b");
Matcher m = p.matcher("aaaaab");
boolean b = m.matches();

Matcher(匹配器)

通过编译pattern对字符序列执行匹配操作的引擎。通过调用模式的matcher()方法创建匹配器。

RegexSyntaxException

抛出未检查的异常,说明正则表达式中存在语法错误。

捕获组

捕获组是把多个字符当一个独立单元进行处理的方法,它通过括号内的字符分组来创建。例如,正则表达式(dog)创建了一个单一的捕获组,组里包含’d’、’o’、’g’。

捕获组是通过从左到右计算其括号来编号。例如,在表达式( (A) ( B (C) ) )中,有4个捕获组:

  • ((A)(B(C)))
  • (A)
  • (B(C))
  • (C)

可以通过matcher对象的groupCount()方法来查看有多少个捕获组。groupCount()返回一个int值,表示matcher对象当前有多少个捕获组。

还有一个特殊的组(组0),它总是代表整个表达式。该组不包括在groupCount()的返回值中。

语法




示例

查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexMatches
{
public static void main( String args[] ){
// 按指定模式在字符串查找
String line = "This order was placed for QT3000! OK?";
String pattern = "(.*)(\\d+)(.*)";
// 创建 Pattern 对象
Pattern r = Pattern.compile(pattern);
// 现在创建 matcher 对象
Matcher m = r.matcher(line);
System.out.println("groupCount: " + m.groupCount());
if (m.find( )) {
System.out.println("Found value: " + m.group(0) );
System.out.println("Found value: " + m.group(1) );
System.out.println("Found value: " + m.group(2) );
} else {
System.out.println("NO MATCH");
}
}
}

以上实例编译运行结果如下:

1
2
3
4
groupCount: 3
Found value: This order was placed for QT3000! OK?
Found value: This order was placed for QT300
Found value: 0

最后

Java正则的功用还有很多,事实上只要是字符处理,就没有正则做不到的事情存在。(当然,正则解释时较耗时间就是了|||……)

参考文档

  • CSDN:Java正则表达式入门
  • RUNOOB.com:Java 正则表达式
1…456…8
archerda

archerda

71 日志
37 标签
GitHub Email
© 2015 - 2019 archerda
由 Hexo 强力驱动
主题 - NexT.Muse