使用JProfiler进行性能调优

Author:Zhang He ,Shao Sheldon 

简介:

如今web project越发庞大,一个项目中包含上百个jar包是经常碰到的情况。这也就导致项目启动越发缓慢。

针对此情况,可以考虑使用JProfiler对程序启动过程进行监听,找到影响性能的hot spots,对其进行分析,优化,藉此达到减少启动时间的目的。

本文旨在通过一个案例,介绍一种使用JProfiler进行性能优化的方法。

优化方法:

  1. 找到hot spots

所谓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操作可以想到下面几种思路:

  1. 绕过读取的过程
  2. 减少读取的内容
  3. 加快读取的速度

经过几次尝试,我们在加快启动速度方面,找到了突破口:

在对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分析源码,最后得出可行的优化方案。优化过程要注意以下几点:

  1. 要有针对性的选择需要跟踪的目标类包(package),尽量预判一些可疑的目标,减小跟踪范围。
  2. 分析热点时,可以根据项目特点进行分析。例如最近项目做过哪些改动,哪些改动可能导致对性能的影响。
  3. 设计优化方案时,尝试从不同角度着手,设计多个备用方案,共同工作。一方面可以提升优化结果,另一方面也保证在某方案被证明无效时有其它选择。

目前该Patch已被Apache接受,并将集成在下一个版本中。同时该优化方法,也可广泛适用于其他应用。

你可能感兴趣的:(JAVA)