Author:Zhang He ,Shao Sheldon
简介:
如今web project越发庞大,一个项目中包含上百个jar包是经常碰到的情况。这也就导致项目启动越发缓慢。
针对此情况,可以考虑使用JProfiler对程序启动过程进行监听,找到影响性能的hot spots,对其进行分析,优化,藉此达到减少启动时间的目的。
本文旨在通过一个案例,介绍一种使用JProfiler进行性能优化的方法。
优化方法:
所谓hot spots是指潜在的导致性能降低的热点。单纯通过人工找到这些热点费时费力,并不十分可行。鉴于这些热点通常是一些函数的大量调用,故可以通过JProfiler监听web project的启动过程,找到大量占用cpu时间的函数调用,这些调用即为可能的热点。
1.1JProfiler设置
JProfiler,选择”New Server Integration”à选择”Generic application server”à选择”On this computer”à选择相应的jdk.
之后采用默认选项,直到此步骤(如图1):
图1
根据说明,选中配置信息,并复制到Tomcat中server设置项的Arguments中(如图2):
图2
之后对刚刚建立的session设置Filter(如图3),以过滤不需要跟踪的内容:
图3
启动web project后Console会提示运行JProfiler,此时运行之前配置过的session即可监听web project启动时的状态。注意,启动过程中请勾选上 ”Record CPU data on startup ” .
1.2分析JProfiler运行结果:
我们可以重点关注函数的调用,在CPU Views中我们可以看到函数调用树(如图4),找到cpu占用比高的部分进行分析,从中挑出潜在的hot spots。
图4
本文所涉web project中的hot spots包括对Annotation的处理、对TLD的处理等,我们选择Annotation处理部分作为例子,进行后面的介绍。
2.代码优化
2.1分析原因
首先需要知道的是,这里cost相对较高的原因。
通过函数命名可以得知,这一部分的开销主要发生在Tomcat容器启动时对Annotation的处理。启动阶段的Annotation处理,我们很容易会想到Servlet 3.0新特性——注解。所以重点就聚焦在这部分的内容上。
2.2分析源码
既然知道性能瓶颈在于Tomcat对于Annotation的处理,接下来即可针对这部分的内容进行优化。首先把Tomcat源码下载下来,导入eclipse中。
通过函数调用树可以看出,很大一部分开销都发生在了ClassParser类中。所以首先弄清ClassParser类中做了什么。
ClassParse类的构造函数如下:
public ClassParser(InputStream file) {
this.file = new DataInputStream(new BufferedInputStream(file, BUFSIZE));
}
它接受一个InputStream,并用它构造出一个DataInputStream,通过向上查找代码调用,可以分析出该Stream是jar文件的某个ClassEntry,即jar包中的class文件。
该类的parse()方法如下:
public JavaClass parse() throws IOException, ClassFormatException {
readID();
readVersion();
readConstantPool();
readClassInfo();
readInterfaces();
readFields();
readMethods();
readAttributes();
return new JavaClass(class_name, superclass_name, access_flags, constant_pool, interface_names, runtimeVisibleAnnotations);
}
其中调用了一系列read方法,该方法是将Stream中的class文件读取并封装成一个JavaClass类,可见该方法会涉及到大量的IO操作,成为了性能的瓶颈。
加快IO操作可以想到下面几种思路:
经过几次尝试,我们在加快启动速度方面,找到了突破口:
在对class文件的解析过程中,DataInputStream的一些read方法被反复调用,包括readInt(),readUnsignedShort () 等,以readInt() 为例:
public final int readInt() throws IOException {
int ch1 = in.read();
int ch2 = in.read();
int ch3 = in.read();
int ch4 = in.read();
if ((ch1 | ch2 | ch3 | ch4) < 0)
throw new EOFException();
return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
}
在对class文件的解析过程中,DataInputStream的一些read操作,该操作由BufferedInputStream完成。
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
BufferedInputStream实际上使用一个缓存数组buf[] 去提高文件读取效率,但是其read方法却因此需要判断多次是否越界。考虑readInt() 方法,通过四次read() 方法调用读取出一个Int,总共需要8次越界判断。但理论上完全可以直接从buf中取出4个字节的长度,仅通过两次判断即可得到所需要的内容。这在大量的文件读写中,对性能的影响不可忽视。针对该方案,进行优化。
2.3代码优化
设计一个FastDataInputStream类继承自BufferedInputStream,实现DataInput接口。该类直接需要实现DataInput接口的方法,即对Int, Long, UnsignedShort等内容的读取。其中,对读取“固定长度”内容的方法需要进行优化,跳过read()方法,直接从buf[]中取出需要的内容。仍以readInt()为例:
public final int readInt() throws IOException {
if(pos + 3 >= count){
fillNew();
if(pos + 3 >= count) throw new EOFException();
}
int ch1 = this.buf[pos++] & 0xff;
int ch2 = this.buf[pos++] & 0xff;
int ch3 = this.buf[pos++] & 0xff;
int ch4 = this.buf[pos++] & 0xff;
return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
}
同时由于我们在DataInput接口中增加了一个buffer,这样会导致需要考虑边界问题。例如数据充满16长度的buf[],buf中前15个字节已被读取,此时读取int类型的时候,需要考虑对残余字节的处理。由于残余数据量不会太大,所以为了简化逻辑,可以考虑在数组读取越界的时候,直接将残余内容复制到buf的开头,然后重新充满数组:
private void fillNew() throws IOException {
int remain = 0;
if(pos < count){
remain = count - pos;
System.arraycopy(buf, pos, buf, 0, remain);
}
pos = 0;
int n = this.in.read(buf, remain, buf.length - remain);
count = pos + n + remain;
}
修改Tomcat容器中的ClassParser类的构造方法,使后续程序中直接使用FastDataInputStream类:
public ClassParser(InputStream file) {
if(isFDIS){
this.file = new FastDataInputStream(file, BUFSIZE);
} else {
this.file = new DataInputStream(new BufferedInputStream(file, BUFSIZE));
}
}
注:isFDIS 为自己定义的开关标识。
性能对比:
下表对FastDataInputStream的性能进行了对比。针对classpath下所有文件进行parse的时候,性能提升约18%;若只针对若干jar包进行parse,性能提升约为15%。
若所运行项目包含更大量的jar文件,该优化将得到更可观的效率提升。
表1:性能对比
|
All “jar” in Classpath |
10 “jar” files only |
DataInputStream |
593ms |
93ms |
FastDataInputStream |
488ms |
79ms |
总结:
本文主要介绍了使用JProfiler对项目性能进行优化的一个方法:通过性能分析软件找到hot sports,针对hot sports分析源码,最后得出可行的优化方案。优化过程要注意以下几点:
目前该Patch已被Apache接受,并将集成在下一个版本中。同时该优化方法,也可广泛适用于其他应用。