目 录Table of Contents html
1 前言 7 java
2 编码性能规范 7 正则表达式
2.1 线程同步规则 7 算法
2.1.1 规则描述 7 编程
2.1.2 案例研究 8 windows
2.2 字符串使用规则 10 api
2.2.1 规则描述 10 数组
2.2.2 案例研究 10 缓存
2.3 临时对象建立规则 17 性能优化
2.3.1 规则描述 17
2.3.2 案例研究 17
2.4 集合类使用规则 22
2.4.1 规则描述 23
2.4.2 案例研究 23
2.5 IO读写规则 26
2.5.1 规则描述 26
2.5.2 案例研究 26
2.6 数组、集合操做规则 28
2.6.1 规则描述 29
2.6.2 案例研究 29
2.7 内存泄漏防范规则 30
2.7.1 规则描述 30
2.7.2 案例研究 31
3 设计性能规范 32
3.1 事件派发线程使用规则 32
3.1.1 规则描述 32
3.1.2 案例研究 32
3.2 界面组件设计规则 35
3.2.1 规则描述 38
3.2.2 案例研究 38
3.3 业务流程设计规则 41
3.3.1 规则描述 42
3.3.2 案例研究 42
3.4 界面响应设计规则 45
3.4.1 规则描述 45
3.4.2 案例研究 46
3.5 系统抗负载能力设计规则 46
3.5.1 规则描述 47
3.5.2 案例研究 47
3.6 多线程设计规则 48
3.6.1 规则描述 49
3.6.2 案例研究 49
4 附录A:安装盘压缩 52
4.1 背景介绍 52
4.2 Pack200压缩格式介绍 52
4.3 7z压缩格式介绍 52
5 附录B:性能测试专题 53
5.1 背景介绍 53
5.2 经常使用的java性能测试工具 54
5.2.1 Profiler工具介绍 54
5.2.2 Visualgc工具介绍 55
5.2.3 GC日志分析工具介绍 55
5.2.4 Windows性能检视器介绍 56
5.3 如何分析性能测试数据 57
5.3.1 检查测试数据的真实性 57
5.3.2 经过Excel对测试数据进行处理 58
5.3.3 分析测试数据的关注点 58
5.3.4 对测试项进行理论评估和公式推导 58
6 参考文献 59
表目录 List of Tables
表2 JTextArea组件和UltralEdit程序打开文件时的内存增量状况 39
表3 本身开发的文本组件和JTextArea组件的性能对比数据 40
图目录 List of Figures
Java编程性能规范
范 围Scope:
本规范规定了在基于程序性能考虑的状况下,java语言的系统编码和系统设计规则。
本规范适用于使用Java语言编程的部门和产品。
简 介Brief introduction:
本规范从如何提升java系统性能的角度,给出了系统设计和编码时的重要关注点。在本规范中,针对每个关注点,都从概述、规则描述和案例研究三个方面来展开描述。本规范中的规则描述都是从实际的优化案例和技术研究中提炼出来的,能够供java编程人员在系统设计和编码时做为意见参考。值得一提的是,本文中列举了大量的实际优化案例(基于JDK1.4环境),这些案例对于java系统的性能优化有着比较高的借鉴价值,能够供开发人员在性能优化时参考。
关键词Key words:java,性能优化
引用文件:
下列文件中的条款经过本规范的引用而成为本规范的条款。凡是注日期的引用文件,其随后全部的修改单(不包括勘误的内容)或修订版均不适用于本规范,然而,鼓励根据本规范达成协议的各方研究是否可以使用这些文件的最新版本。凡是不注日期的引用文件,其最新版本适用于本规范。
序号No. |
文件编号Doc No. |
文件名称 Doc Title |
1 |
||
2 |
||
术语和定义Term&Definition:<对本文所用术语进行说明,要求提供每一个术语的英文全名和中文解释。List all Terms in this document, full spelling of the abbreviation and Chinese explanation should be provided.>
缩略语Abbreviations |
英文全名 Full spelling |
中文解释 Chinese explanation |
性能是软件产品开发中须要关注的一个重要质量属性。在咱们产品实现的各个环节,均可能引入不一样程度的性能问题,包括从最初的软件构架选择、到软件的详细设计,再到软件的编码实现等等。但在本规范中,主要内容是针对java系统,给出一些在软件详细设计和编码实现过程当中须要注意的规则和建议。
即便咱们在产品开发的各个环节都考虑了性能质量属性,可能仍是不够的。随着系统业务功能的不断增多,用户要求的不断提升,或者为了提升产品的可用性和竞争力,咱们不少状况下仍是须要对已开发的产品进行一次集中式的专门的性能优化工做。本规范中所提供的大量实际优化案例,可供这种专门的性能优化工做进行参考。
可是在性能优化工做中,咱们也需警戒"过早优化"的问题。咱们的基本指导策略仍是首先让系统运行起来,再考虑怎么让它变得更快。通常只有在咱们证明某部分代码的确存在一个性能瓶颈的时候,才应进行优化。除非用专门的工具分析瓶颈,不然颇有多是在浪费本身的时间。另外,性能优化的隐含代价会使咱们的代码变得难于理解和维护,这一点也是须要权衡和关注的。
关于线程同步的处理,通常在编码过程当中存在如下几个问题:
从性能角度来说,并非全部的同步方法都影响性能。若是同步方法调用的频率足够少,则加同步和不加同步,对性能影响不是很大,甚至能够忽略。咱们在编写代码时,须要对一些重点的地方关注同步问题。
建议1.1.1.1:对于可能被其它代码频繁调用的方法,须要关注同步问题
建议1.1.1.2:对于经常使用工具类的方法,须要关注同步问题
建议1.1.1.3:对于不能确认被其它代码如何调用的方法,须要关注同步问题
在代码编写过程当中,很容易犯同步问题的错误,不恰当的使用同步机制。咱们在编写代码(重点是:可能被频繁调用的方法、做为经常使用工具类的方法、不能确认被其它代码如何调用的方法)时,须要重点关注如下同步问题:
总的来讲,咱们不必对全部代码都关注同步问题,在处理同步问题的性能优化时还须要保证代码的可读性和可维护性。因此对于进入维护阶段的代码,咱们不要盲目的进行同步问题的优化,以避免引入一些新的问题。
在JDK的Integer类的public static String toString(int i)方法中,使用了线程局部变量perThreadBuffer来保存char数组,供构建String对象时使用。其代码以下:
...... // Per-thread buffer for string/stringbuffer conversion private static ThreadLocal perThreadBuffer = new ThreadLocal() { protected synchronized Object initialValue() { return new char[12]; } }; public static String toString(int i) { switch(i) { case Integer.MIN_VALUE: return "-2147483648"; case -3: return "-3"; case -2: return "-2"; case -1: return "-1"; case 0: return "0"; case 1: return "1"; case 2: return "2"; case 3: return "3"; case 4: return "4"; case 5: return "5"; case 6: return "6"; case 7: return "7"; case 8: return "8"; case 9: return "9"; case 10: return "10"; } char[] buf = (char[])(perThreadBuffer.get()); int charPos = getChars(i, buf); return new String(buf, charPos, 12 - charPos); } ...... |
关于以上代码,若是不使用线程局部变量的话,通常作法会是首先在Integer类中定义一个char数组的静态成员变量,而后直接将toString(…)方法加上synchronized关键字来进行同步处理。但这种作法咱们能够想象,当Integer的toString(…)方法被频繁调用的话,对性能影响是很是大的。
改用线程局部变量后,能够为每一个调用的线程单独维护一个char数组,而且保证每一个线程只使用本身的char数组变量,从而也就不存在须要同步的问题了,所以性能就比直接使用synchronized来进行同步要好不少。其实在JDK的源代码中,Integer.java、Long.java、Charset.java、StringCoding.java等类中都用到了线程局部变量。
在JDK的源代码中,不少地方都是使用代码块同步,而不是直接对整个方法进行同步的。直接对方法进行同步的一个主要问题在于:当一个线程对对象的一个同步方法进行调用时,会阻止其它线程对对象其它同步方法的调用,而无论对象的这些同步方法之间是否具备相关性。如下是一个java.util.Timer.java中同步代码块的例子:
…… private TaskQueue queue = new TaskQueue(); …… public void cancel() { synchronized(queue) { thread.newTasksMayBeScheduled = false; queue.clear(); queue.notify(); // In case queue was already empty. } } …… |
在咱们实际开发的代码中,有很大一部分代码都是在作各类字符串操做。常常用到的字符串操做包括:字符串链接、字符串比较、字符串大小写转换、字符串切分、字符串查找匹配等。咱们在编码过程当中存在的问题是:每每对于一种字符串操做,有多种处理方式可供选择,而对于程序中的那些热点方法,若是选择了不恰当的字符串处理方式,将会对程序性能产生较大的影响。
规则1.2.1.1:对于常量字符串,不要经过new方式来建立
规则1.2.1.2:对于常量字符串之间的拼接,请使用"+";对于字符串变量(不能在编译期间肯定其具体值的字符串对象)之间的拼接,请使用StringBuffer;在JDK1.5或更新的版本中,若字符串拼接发生在单线程环境,可使用StringBuilder
建议1.2.1.3:在使用StringBuffer进行字符串操做时,请尽可能设定初始容量大小;也尽可能避免经过String/CharSequence对象来构建StringBuffer对象
规则1.2.1.4:当查找字符串时,若是不须要支持正则表达式请使用indexOf(…)实现查找;当须要支持正则表达式时,若是须要频繁的进行查找匹配,请直接使用正则表达式工具类实现查找
建议1.2.1.5:对于简单的字符串分割,请尽可能使用本身定义的公用方法或StringTokenizer
建议1.2.1.6:当须要对报文等文本字符串进行分析处理时,请增强检视,注意算法实现的优化
在java语言中,对于字符串常量,虚拟机会经过常量池机制确保其只有一个实例。常量池中既包括了字符串常量,也包括关于类、方法、接口等中的常量。当应用程序要建立一个字符串常量的实例时,虚拟机首先会在常量池中查找,看是否该字符串实例已经存在,若是存在则直接返回该字符串实例,不然新建一个实例返回。咱们说常量是能够在编译期就能被肯定的,因此经过new方法建立的字符串不属于常量。关于字符串常量的特性,能够经过如下代码作一个测试:
String s0="abcd"; String s1="abcd"; String s2=new String("abcd"); String s3=new String("abcd"); System.out.println( s0==s1 ); //true System.out.println( s2==s3 ); //false System.out.println( s0==s2 ); //false System.out.println( "============================" );
s2=s2.intern(); //把常量池中"abcd"的引用赋给s2 System.out.println( s0==s2 ); //true
输出结果为: true false false ============================ true |
经过上面测试代码的输出结果能够了解两点:
在java中,字符串拼接方式经常使用的有两种,一是经过"+"号进行拼接,另一种是经过StringBuffer进行拼接。这两种拼接方式都有本身特定的适用场合。通常规则是对于字符串常量之间的拼接,请使用"+";对于字符串变量(不能在编译期间肯定其具体值的字符串对象),请使用StringBuffer。另外,当使用StringBuffer进行字符串拼接时,请尽可能指定合适的初始容量大小。如下代码是对字符串常量拼接的一个测试代码,经过"+"号来实现字符串常量拼接,能够达到共享实例的目的:
…… private static String constStr1="abcde"; private static String constStr2="fghi";
private final static String constStr3="abcde"; private final static String constStr4="fghi";
public static void main(String[] args) {
String str0="abcdefghi"; String str1="abcde"; String str2="fghi"; final String str_1="abcde"; final String str_2="fghi";
String str3="abcde"+"fghi"; String str4=str1+str2; String str_4=str_1+str_2; String str5=constStr1+constStr2; String str6=constStr3+constStr4;
System.out.println(str0==str3); //true,直接经过常量相加,能够共享实例 System.out.println(str0==str4); //false,经过引用对常量进行相加,将会获得一个新的字符串变量 System.out.println(str0==str_4); //true,经过final引用对常量相加,能够共享实例 System.out.println(str0==str5); //false,没有加final,仍是会获得一个新的字符串变量 System.out.println(str0==str6); //true,成员变量加了final后,才能够看成常量来使用 } ……
以上代码输出结果为: true false true false true |
最可怕的不恰当的字符串拼接方式,是在for循环中使用"+"来进行字符串对象拼接,相似以下代码:
for(int i = 0 ; i < 1024*1024; i++ ) { str += "XXX" ; } |
运行上面代码的结果是:将致使整个操做系统CPU占有率长时间达到100%,操做系统长时间(应该在5分钟以上)几乎处于不响应状态。具体缘由很简单,每次"+"号操做后,都会生成一个新的临时字符串对象,随着循环的深刻,建立的临时字符串对象愈来愈大,执行起来就会愈来愈困难。若是将上述代码改成StringBuffer来实现拼接,能够看到程序可以正常运行。
在java中,进行字符串查找匹配时通常有三种实现方式:第一种是调用String对象的indexOf(String str)方法;第二种是调用String对象的matches(String regex)方法;第三种是直接使用正则表达式工具类(包括Pattern类、Matcher类)来实现匹配。这三种实现方式的各自特色以下:
如下是对三种查找匹配实现方式的性能测试代码:
String s0="abcdefghjkilmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"; String s1="890ABCDE"; Pattern p = Pattern.compile(s1); Matcher m = p.matcher(s0); int loop=1000000; long start=System.currentTimeMillis(); for(int i=0;i<loop;i++) { s0.indexOf(s1); //经过String对象的indexOf(String str)方法实现查找匹配 } long end=System.currentTimeMillis(); long time1=end-start;
start=System.currentTimeMillis(); for(int i=0;i<loop;i++) { s0.matches(s1); //经过String对象的matches(String str)方法实现查找匹配 } end=System.currentTimeMillis(); long time2=end-start;
start=System.currentTimeMillis(); for(int i=0;i<loop;i++) { m.matches(); //经过正则表达式工具类实现查找匹配 } end=System.currentTimeMillis(); long time3=end-start;
System.out.println("time1:"+time1); System.out.println("time2:"+time2); System.out.println("time3:"+time3);
上述代码某次执行时的输出结果: time1:187 time2:1844 time3:219
|
在咱们实际开发中,经常须要对字符串进行分割,如将"abc def ghi"按空格分割成三个字符串而后放到一个String数组中。在java中,通常有三种方法能够实现字符串的分割:
对于以上三种分割方法,其运行效率差异是很大的。采用split(String regex)方法性能最差,但因为使用了正则表达式,功能性最强;采用StringTokenizer对象分割字符串,性能能够接受,功能也较强;采用本身开发代码实现字符串分割,速度最快,但不具有通用性。下面是对三种方法的一个测试代码:
…… //经过String对象的split(String regex)方法实现字符串分割 public static String[] testSplit1(String str) { return str.split(","); }
//经过StringTokenizer对象实现字符串分割 public static String[] testSplit2(String str) { ArrayList list=new ArrayList(); StringTokenizer st = new StringTokenizer(str,","); while(st.hasMoreTokens()) { list.add(st.nextToken()); } String[] obj=(String[])list.toArray(new String[0]); return obj; }
//经过本身开发代码实现字符串分割 public static String[] testSplit3(String str) { int fromIndex=0; int index0=0; int signLen=",".length(); int strLen=str.length();
index0=str.indexOf(",",fromIndex); if(index0==-1) { return new String[]{str}; } ArrayList list=new ArrayList(); String subStr=str.substring(fromIndex,index0); if(!subStr.equals("")) { list.add(subStr); }
fromIndex=index0+1;
while(fromIndex<strLen-1) { index0=str.indexOf(",",fromIndex); if(index0==-1) { list.add(str.substring(fromIndex)); break; }
String subStr1=str.substring(fromIndex,index0); if(!subStr1.equals("")) { list.add(subStr1); } fromIndex=index0+signLen; } return (String[])list.toArray(new String[0]); } …… |
通过测试,假设要分割的字符串是"aaaa,bbbb,cccc,dddd,eeee,ffff",对上述三种算法各调用1000次后,所花的时间大概以下表:
实现算法 |
所花时间(单位:毫秒) |
采用split(String regex)方法进行分割 |
141 |
采用StringTokenizer进行分割 |
46 |
采用自定义方法进行分割 |
16 |
根据上表的性能测试结果可知,采用split(String regex)方法进行分割字符串性能是很低的。因此在实际代码开发中,若是须要频繁对字符串进行分割的话,最好不要采用String对象的split(…)方法进行字符串分割,一个比较好的选择是本身写一个公用的字符串分割方法。
在实际开发中,会遇到须要对报文等文本字符串进行分析处理的问题,对于不一样的开发人员,实现文本字符串分析处理的算法会存在较大差别,有的实现会比较高效,有的实现看起来比较简单但性能却不好。如下是一个字符串处理算法优化案例:
//优化前的方法实现 private String extractPingReportBody(String strPingReport) { String intialMessage = strPingReport; int index = intialMessage.toUpperCase().indexOf(PING_MESSAGE_FORMAT); if(0 <= index) { intialMessage = intialMessage.substring(index);//建立了一个新的子串 index = intialMessage.indexOf(DOUBLE_LINE_SEPARATOR); if(0 <= index) { //又建立了一个新的子串 intialMessage = intialMessage.substring(index + DOUBLE_LINE_SEPARATOR.length()); index = intialMessage.indexOf(DOUBLE_LINE_SEPARATOR); if(0 <= index) { intialMessage = intialMessage.substring(0, index); //又建立了一个新的子串 return intialMessage; } else { ...... } } else { ...... } } else { ...... } return strPingReport; }
//优化后的方法实现 private String extractPingReportBody(String strPingReport) { int index0 = strPingReport.toUpperCase().indexOf(PING_MESSAGE_FORMAT); if(index0<0) { ...... } int index1= strPingReport.indexOf(DOUBLE_LINE_SEPARATOR,index0);//再也不建立子串 if(index1<0) { ...... } else { //再也不建立子串 int dex2=strPingReport.indexOf(DOUBLE_LINE_SEPARATOR,index1 + DOUBLE_LINE_SEPARATOR.length()); if(index2>=0) { return strPingReport.substring(index1,index2); } else { ...... } } return strPingReport; } |
总结字符串处理的优化案例,通常不高效的字符串处理算法表现为:
错误的作法: intialMessage = intialMessage.substring(index); index = intialMessage.indexOf(DOUBLE_LINE_SEPARATOR);
好的作法: index= intialMessage.indexOf(DOUBLE_LINE_SEPARATOR,index); |
在java语言中,或者说在面向对象语言中,对象的建立是既耗时间,又占用内存。若是在系统运行过程当中建立了大量没必要要的临时对象,对性能的影响是比较大的。所以如何避免建立没必要要的临时对象,对系统性能的提高有着重要做用。在实际代码开发中,建立了没必要要的临时对象的缘由通常可分为如下几种:
建议1.3.1.1:在实现业务处理流程的过程当中,须要考虑临时对象引发的性能问题,精简业务处理流程,减小没必要要的中间环节
建议1.3.1.2:对象的建立应尽可能按需建立,而不是提早建立
建议1.3.1.3:对象的建立应尽可能在for、while等循环外面建立,在循环里面进行重用
建议1.3.1.4:对于高频度使用的对象,须要进行单独优化处理给以重用
在某OM系统中,有一个对跟踪消息体码流进行保存的处理流程。在该处理流程中,主机会频繁的向OM系统上报跟踪消息,OM系统须要对全部这些上报消息实时进行保存,以便用户后续根据消息分析和定位问题。在优化前,跟踪消息体的保存处理流程以下图所示:
从上面的处理流程能够看出,每保存一条消息体码流,就新建了一个ByteBuffer对象和一个byte数组对象,这是没必要要的浪费,当跟踪消息上报速度很大时,这种ByteBuffer和byte数组临时对象的建立对性能影响是很大的。通过对该处理流程改进优化后,新的处理流程以下图:
优化前和优化后的处理流程的差异在于,优化前每保存一条消息码流,都要建立一个ByteBuffer对象,而优化后把这一过程给省略了,直接经过IO写入数据,减小了中间码流的转换过程。
在某OM系统的MML报文隐藏密码方法中,优化前存在三个问题:第一个问题是StringBuffer没有按需建立;第二个问题是存在多余处理逻辑;第三个问题是StringBuffer没有指定初始容量大小。如下是隐藏密码方法优化前的代码:
public static String hidePassword(final String msg) { if (null == msg || "".equals(msg)) { return msg; } Matcher matcher = getPattern().matcher(msg); //StringBuffer没有按需建立,由于若是一开始就没有发现匹配的话,StringBuffer对象就是多余的; //StringBuffer没有指定初始大小,实际上StringBuffer的容量能够用msg.length()来指定; StringBuffer sbf = new StringBuffer(); int iLastEnd = 0; int iEnd = 0; int iValueIdx = 0; while (matcher.find()) { iEnd = matcher.end(); sbf.append(msg.substring(iLastEnd, iEnd)); iValueIdx = msg.indexOf('"', iEnd); if (0 <= iValueIdx) { sbf.append("*****"); iLastEnd = iValueIdx; } else { iLastEnd = iEnd; } } if (iLastEnd < msg.length()) { //若是一开始就没有发现匹配的话,即iLastEnd==0,这时只须要直接返回msg就好了, //因此在这种状况下,下面的语句就是多余的。 sbf.append(msg.substring(iLastEnd)); } return sbf.toString(); } |
通过优化后, 报文隐藏密码方法的代码以下:
public static String hidePassword(final String msg) { if (null == msg || msg.length()==0) { return msg; } Matcher matcher = getPattern().matcher(msg); StringBuffer sbf = null; int iLastEnd = 0; int iEnd = 0; int iValueIdx = 0; while (matcher.find()) { //将sbf的new操做放到下面来进行。 if(null == sbf) { //最后StringBuffer的长度确定与msg的长度差很少,因此能够给其指定初始大小 sbf = new StringBuffer(msg.length()); } iEnd = matcher.end(); sbf.append(msg.substring(iLastEnd, iEnd)); iValueIdx = msg.indexOf('"', iEnd); if (0 <= iValueIdx) { sbf.append("*****"); iLastEnd = iValueIdx; } else { iLastEnd = iEnd; } } //若是一开始就没有发现匹配的话,则直接返回msg就好了。 if(0 == iLastEnd) { return msg; } else if (iLastEnd < msg.length()) { sbf.append(msg.substring(iLastEnd)); } return sbf.toString(); } |
在实际代码开发中,没有按需建立对象是一个常常容易犯的错误。不少开发人员每每图一时编码方便或时间紧张来不及细想,将一些对象提早建立出来,而在后面某些条件分支中又彻底用不到这些对象。
在某OM系统的跟踪过滤功能中,有一段热点代码以下图:
…… public void executeColumnFilter(String[] selectedValues) { …… for (int tableIndex = 0; tableIndex < size; tableIndex++) { Vector rowData = (Vector) tableDataV.get(tableIndex); String detailMsgStr = getDetailMsgStr(tableIndex); …… } …… } ……
private String getDetailMsgStr(int tableIndex) { String detailMsgStr = "";//消息详细解释码流字符串 HashMap detailParameter = new HashMap(); //该对象没有重用 …… } …… |
在上面显示的代码中,for循环里面每次都调用了getDetailMsgStr()方法,而该方法每次都会建立一个临时HashMap对象,方法调用结束后,HashMap就再也不使用了。通过测试(512内存、CPU2.4G),在for循环里面建立50000个HashMap对象(不指定初始容量大小)的开销是:须要时间约16毫秒、须要内存约320K。因此,在性能比较紧张的状况下,如何重用该Map对象仍是有意义的。
通过优化后,代码结构以下图:
…… public void executeColumnFilter(String[] selectedValues) { …… Map map=new HashMap(3); //在for循环外面建立对象,在使用时进行重用。 for (int tableIndex = 0; tableIndex < size; tableIndex++) { Vector rowData = (Vector) tableDataV.get(tableIndex); map.clear(); String detailMsgStr = getDetailMsgStr(tableIndex,map); …… } …… } ……
private String getDetailMsgStr(int tableIndex,Map map) { String detailMsgStr = "";//消息详细解释码流字符串 …… } …… |
虽然优化后的代码比优化前更加高效,但可读性要比优化前差些,须要多加一些注释进行说明。从这个优化方案,能够总结出如下结论:
在java API的集合框架(Java Collections Framework)中,提供了丰富的集合处理类,包括无序集合(Set集合)、有序集合(List集合)和映射关系集合(Map集合)。在一般状况下,集合框架提供了足够的功能供咱们使用,咱们关注的重点是如何选择这些集合类。但须要指出的是,java API的集合框架也并无想象的那么完善,它不可能解决全部应用场景下的问题。咱们在使用集合类进行编程时,经常面临如下问题:
建议1.4.1.1:在代码开发中,须要根据应用场景合理选择集合框架中的集合类,应用场景可按单线程和多线程来划分,也可按频繁插入、随机提取等具体操做场景来划分
建议1.4.1.2:对于热点代码,能够采用特定的集合类来提供系统性能,特定集合类能够是本身开发,也能够直接采用Trove这样的第三方开源类库
建议1.4.1.3:当须要在方法之间传递多个属性值时,从性能角度考虑,应优先采用结构体,而非ArrayList或Vector等集合类
在某网管系统中,故障查询数据中的流水号占用了大量内存。当数据量达到必定数值(通常达到300万)时,容易形成前台内存溢出。其缘由主要是流水号的存取是采用集合框架的ArrayList来存取的。因为ArrayList必须以Object的方式保存内容,所以在保存流水号的时候必须用Integer对象,而不是基本整数类型。在java中一个Integer对象占用的内存大约为32个字节,而int类型只占用4个字节,因此在大数据量状况下,采用Integer对象比int类型耗费的内存要多得多。针对上面的问题,优化思路是开发一个特定的集合类IntArrayList,该类有两个特色:
IntArrayList类的参考实现以下:
public class IntArrayList implements List { int size = 0; //保存Integer数值的数祖 int[] elementData = null; public IntArrayList(int initialCapacity) { super(); if (initialCapacity < 0){ throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } this.elementData = new int[initialCapacity]; } /** * 增长数据 */ public boolean add(Object o) { try { ensureCapacity(size + 1); // Increments modCount!! elementData[size++] = ( (Integer)o).intValue(); return true; } catch (Exception ex){} return false; } /** * 获取数据 */ public Object get(int index) { RangeCheck(index); return new Integer(elementData[index]); } /** * 删除指定位置的数据 */ public Object remove(int index) { Object oldValue = null; try { RangeCheck(index); oldValue = new Integer(elementData[index]); int numMoved = size - index - 1; if (numMoved > 0){ System.arraycopy(elementData, index + 1, elementData, index,numMoved); } sizee--; } catch (Exception ex){ return oldValue; } } } |
Trove集合类是一种开放源代码的java集合包,听从LGPL协议(能够商用),提供了java集合类的高效替代品。Trove能够从http://trove4j.sourceforge.net/处获取。相对于java集合框架,Trove具备如下特色:
如下是对Map的put和get方法的性能对比测试:
TObjectIntHashMap intmap = new TObjectIntHashMap(); HashMap map=new HashMap(); int loop=300000; long start=System.currentTimeMillis(); for(int i=0;i<loop;i++) { intmap.put("N"+i, 2); int numInStock = intmap.get("N"+i); } long end=System.currentTimeMillis();
long time=end-start;
Integer intObj=new Integer(2); start=System.currentTimeMillis(); for(int i=0;i<loop;i++) { map.put("N"+i, intObj); Integer numInStockObj=(Integer)map.get("N"+i); } end=System.currentTimeMillis(); long time1=end-start;
System.out.println("time:"+time); System.out.println("time1:"+time1);
以上代码某次执行时的输出结果以下:
time:1375 time1:1593 |
IO读写是咱们在实际开发中常常要遇到的功能实现。Java API为咱们提供了庞大的IO读写库,从实现上来看可分为早期的基于流的IO库和新的基于块的NIO库,从功能上来看可分为基于字节操做的IO库和基于字符操做的IO库。在这么庞大的IO库下,对于同一个IO功能,能够编写出多种实现代码,但须要强调的是,一个设计拙劣的IO代码可能要比通过精心调整的IO代码慢上几倍。为了使咱们开发的系统能高效运行,咱们就必然面临一个问题:怎样编码才能使IO功能实现能够性能最优?
规则1.5.1.1:进行IO读写操做时,必须使用缓冲机制
建议1.5.1.2:从性能角度考虑,应尽可能优先使用字节IO进行读写,而避免用字符IO进行读写
对于文件读写操做,java API中提供了多种类库可供咱们选择,不一样的选择和实现方式会产生不一样的性能结果。如下是六种读写文件实现方式的测试代码:
//经过NIO实现文件读写(方式1) FileInputStream fin1 = new FileInputStream("d:/test1.rar"); FileOutputStream fout1 = new FileOutputStream("d:/e/test1.rar"); FileChannel fcin = fin1.getChannel(); FileChannel fcout = fout1.getChannel(); int fileLength = (int)fcin.size(); long start=System.currentTimeMillis(); fcin.transferTo(0, fileLength, fcout); fin1.close(); fout1.close(); long end = System.currentTimeMillis(); long time1 = end - start; System.out.println("NIO_time1:"+time1);
//经过NIO实现文件读写(方式2) FileInputStream fin11 = new FileInputStream( "d:/test11.rar" ); FileOutputStream fout11 = new FileOutputStream( "d:/e/test11.rar" ); FileChannel fcin11 = fin11.getChannel(); FileChannel fcout11 = fout11.getChannel(); ByteBuffer buffer = ByteBuffer.allocate( 512 ); start=System.currentTimeMillis(); while (fcin11.read(buffer)!=-1) { buffer.flip(); fcout11.write( buffer ); buffer.clear(); } fin11.close(); fout11.close(); end = System.currentTimeMillis(); long time11 = end - start; System.out.println("NIO_time2:"+time11);
//经过IO进行批量读写 byte[] arr = new byte[512]; FileInputStream fin3 = new FileInputStream("d:/test3.rar"); FileOutputStream fout3 = new FileOutputStream("d:/e/test3.rar"); start = System.currentTimeMillis(); while (fin3.read(arr) != -1) { fout3.write(arr); } fin3.close(); fout3.close(); end = System.currentTimeMillis(); long time3 = end - start; System.out.println("IO_byteArray:" + time3);
//经过buffer IO进行读写 FileInputStream fin4 = new FileInputStream("d:/test4.rar"); FileOutputStream fout4 = new FileOutputStream("d:/e/test4.rar"); BufferedInputStream bufferInput=new BufferedInputStream(fin4); BufferedOutputStream bufferOutput=new BufferedOutputStream(fout4); int c=-1; start = System.currentTimeMillis(); while ((c = bufferInput.read()) != -1) { bufferOutput.write(c); } bufferInput.close(); bufferOutput.close(); end = System.currentTimeMillis(); long time4 = end - start; System.out.println("IO_Buffer:"+time4);
//经过字符IO进行读写 FileReader reader=new FileReader("d:/test5.rar"); FileWriter writer=new FileWriter("d:/e/test5.rar"); char[] charArr = new char[512]; start = System.currentTimeMillis(); while (reader.read(charArr) != -1) { writer.write(charArr); } reader.close(); writer.close(); end = System.currentTimeMillis(); long time5 = end - start; System.out.println("IO_char:" + time5);
//直接经过IO进行读写(不使用缓冲) c = -1; FileInputStream fin2 = new FileInputStream("d:/test2.rar"); FileOutputStream fout2 = new FileOutputStream("d:/e/test2.rar"); start = System.currentTimeMillis(); while ((c = fin2.read()) != -1) { fout2.write(c); } fin2.close(); fout2.close(); end = System.currentTimeMillis(); long time2 = end - start; System.out.println("IO_noBuffer:"+time2);
以上代码某次执行的输出结果以下(读写的文件大小为3M): NIO_time1:171 NIO_time2:250 IO_byteArray:235 IO_Buffer:344 IO_char:515 IO_noBuffer:10002 |
经过以上代码测试,可得出以下结论:
在java API中,针对数组和集合的操做,专门封装了两个类:java.util.Arrays和java.util.Collections。在这两个工具类中,包含了数组及集合的经常使用操做方法,如拷贝、查找、排序等,这些方法通常来讲算法实现上都是很是高效的。可是对工具类中的排序等方法,存在的问题是,这些方法因为不能从业务数据集合里面获取排序数据,所以在实现上就多了不少数据拷贝、克隆等操做,形成的结果是容易生成大量的临时对象。因此当咱们须要对数组或集合进行拷贝、查找、排序等操做时,对于通常的应用应优先使用Arrays和Collections中提供的方法,可是对于热点代码,最好是参考java API中的方法实现,本身开发特定的排序等方法。
建议1.6.1.1:对于数组、集合的拷贝、查找、排序等操做,若是是通常应用,能够优先采用java.util.Arrays和java.util.Collections中提供的工具方法;可是对于热点代码,最好是参考java API中的方法实现,本身开发特定的排序等方法,以减小临时对象的建立。
规则1.6.1.2:对于数组的拷贝,请使用System.arraycopy(…)方法
一些对java API不太熟悉的开发人员,每每会犯数组拷贝的错误,没有用Arrays类中提供的工具方法而是本身写一个很是低效的数组复制方法。如下是相关的代码示例:
char[] sourceArr=new char[1000]; char[] destineArr=new char[1000];
//错误的数组拷贝方法 for(int i=0;i<sourceArr.length;i++) { destineArr[i]=sourceArr[i]; }
//正确的数组拷贝方法 System.arraycopy(sourceArr, 0, destineArr, 0, sourceArr.length); |
在Collections类中实现了排序工具方法,该方法的实现代码以下:
//Collections类中的sort方法 public static void sort(List list) { Object a[] = list.toArray(); //新建立了一个数组 Arrays.sort(a); ListIterator i = list.listIterator(); for (int j=0; j<a.length; j++) { i.next(); i.set(a[j]); } }
//ArrayList类中的toArray()方法 public Object[] toArray() { Object[] result = new Object[size]; System.arraycopy(elementData, 0, result, 0, size); return result; }
//Arrays.sort方法 public static void sort(Object[] a) { Object aux[] = (Object[])a.clone(); //为了排序,又复制了一个数组 mergeSort(aux, a, 0, a.length, 0); } |
分析上面的代码实现,能够看出,在一次排序过程当中,该排序算法产生了2个临时的数组对象,这对于那些动态排序功能(须要根据上报的数据频繁的进行排序)的实现,性能的影响是不能忽略的。
和C++同样,内存泄漏问题也是java程序开发中须要重点关注的性能问题。一些常见的内存泄漏缘由有:
另外值得一提的是,将一些大的对象定义成静态的,也会形成相似于内存泄漏的问题。其缘由是若是静态变量所属的类是被系统类装载的,则即便该类再也不使用时也不会被卸载掉,这将致使静态对象的生存时间可能和系统同样长久,而无论该对象是否被使用。
规则1.7.1.1:若是往框架类或者系统类对象中添加了某个对象,那么当该对象再也不使用时,必须及时清除(这里的框架类、系统类指的是在系统整个运行过程当中始终存在的对象类,如iView主框架的相关类)
规则1.7.1.2:当使用本身定义的类装载器去装载类时,在被装载的类再也不使用后,须要保证该类装载器能够被垃圾回收
建议1.7.1.3:尽可能不要将一些大的对象(对象自己比较大或其引用的对象比较多)定义成静态的
规则1.7.1.4:若是在一个对象中建立了一个线程,当对象再也不使用时,必须关闭该线程
建议1.7.1.5:在JFrame、JDialog等窗口对象中,尽可能处理窗口关闭事件并释放资源
规则1.7.1.6:在IO操做中,必须定义finally代码段,并在该代码段中执行IO关闭操做
在某OM系统中,整个系统代码分为平台部分和适配部分。对于适配部分的代码,平台框架会经过一个自定义的类装载器实例DynClassLoader进行装载。可是当用户注销系统回到登陆界面后,因为系统仍然保持对DynClassLoader实例的引用,致使全部经过DynClassLoader实例装载的适配类都不能卸载掉。这样产生的结果是,当用户从新登陆到第2个、第3个适配版本时,因为所装载的适配类所有都不能卸载,使得JVM代码区的增加超越了设定的上限值,发生内存溢出。关于保持ClassLoader引用会致使全部被该ClassLoader加载的类都不能卸载的缘由,咱们能够分析一下jdk的ClassLoader代码,如下是部分代码片段:
public abstract class ClassLoader {
private static native void registerNatives(); static { registerNatives(); }
// If initialization succeed this is set to true and security checks will // succeed. Otherwise the object is not initialized and the object is // useless. private boolean initialized = false;
// The parent class loader for delegation private ClassLoader parent;
// Hashtable that maps packages to certs private Hashtable package2certs = new Hashtable(11);
// Shared among all packages with unsigned classes java.security.cert.Certificate[] nocerts;
// The classes loaded by this class loader. The only purpose of this table // is to keep the classes from being GC'ed until the loader is GC'ed. private Vector classes = new Vector();
// The initiating protection domains for all classes loaded by this loader private Set domains = new HashSet();
// Invoked by the VM to record every loaded class with this loader. void addClass(Class c) { classes.addElement(c); } …… |
从上面的代码说明咱们能够知道,只要类装载器不被垃圾回收掉,则被该类装载器装载的全部类都不会被卸载掉。
Java的设计目标是灵活、易用和平台一致性。出于这一目的,在UI设计方面,java将界面的绘制和事件处理统一放在了一个独立的线程中进行,这个线程就是事件派发线程。因为事件派发线程只有一个,而且负责了关键的界面绘制和界面事件处理,因此若是该线程被阻塞或者处理的业务逻辑太重的话,会致使整个系统响应很慢、甚至发生灰屏现象。这对用户来讲,就是严重的性能问题。因此在系统的设计开发中,对派发线程的使用必须格外谨慎。
规则2.1.1.1:对于非界面的业务逻辑,应放在事件派发线程以外处理,保证事件派发线程处理的逻辑尽量的少;避免在派发线程中执行时间较长、或执行时间具备较大不肯定性(如访问远程服务器)的业务逻辑
建议2.1.1.2:对于高频度的界面更新事件,最好采用批量定时更新方式代替实时更新方式
在某OM系统中,出于性能优化,对跟踪上报消息显示功能采用了定时刷新机制来批量更新表格数据。当上报消息解析好后,直接将其添加到表格模型中,但不触发模型更新事件(也即在添加数据时,不调用fireTableRowsInserted、fireTableRowsDeleted等方法)。模型更新事件统一放在一个javax.swing.Timer里面定时进行触发(每300毫秒触发一次)。跟踪上报消息表格定时更新的处理流程以下图:
在某OM系统中,有一个上报消息处理业务功能。在优化前,该功能实现将大量业务放到了派发线程中处理(主要有从缓冲区取消息、保存消息到文件、解析消息码流、将解析结果添加到表格、定时刷新表格界面),其流程实现以下图:
基于让派发线程处理尽量少的业务的原则,优化后,经过新增一个业务处理线程,并在该业务处理线程和派发线程之间添加一个表格模型缓冲区的方式,较好的实现了将大部分业务移到派发线程以外处理,最后的结果是派发线程只须要定时刷新表格界面就能够了。优化后的上报消息处理流程以下图:
咱们在实际开发中,常常会遇到这样一个问题:在一个连续的业务处理过程当中,如何将非界面处理的业务逻辑隔离到派发线程以外?根据设计经验,基本能够得出这样一个结论:要想使两个线程协调工做,必须有一个可操做的共享数据区或对象。在上面的优化案例中,咱们定义了一个表格模型缓冲区来使业务处理线程和派发线程协调工做。咱们还能够调用javax.swing.SwingUtilities类的invokeAndWait和invokeLater方法,在业务处理线程环境下将一些界面处理逻辑添加到派发线程中进行处理(在这种状况下,事件派发队列就是业务处理线程和派发线程之间的共享数据区)。
在Swing中,全部轻型(lightweight)组件都是经过java的绘图工具绘制出来的。每一个轻型组件都有本身的paint()方法(默认的paint方法是从JComponent类中继承过来的)。当显示组件时,由派发线程调用顶层容器的paint()方法将容器及容器里面的全部子组件绘制出来。从性能角度讲,Swing的界面绘制机制存在如下问题:
Swing轻型组件的绘制流程以下图:
从上面的流程图能够看出,Swing组件的绘制是一个层层往下的过程,组件首先绘制本身,若是有border则再绘制出border,而后绘制本身的子组件;对于子组件来讲,首先绘制本身,而后再又绘制本身的子组件;每一个组件在绘制本身时,须要进行相关的绘图区域范围计算。另外,须要指出的是,在java体系中,字符串的显示也是经过java本身的绘制机制绘制出来的。因此,在字符串的显示过程当中,也会建立不少临时对象。
经过上面的分析,从性能角度上讲,Swing的实现并无想象的那么好。所以在特殊的应用场景下,为了提升咱们系统的性能,咱们须要,也有必要根据业务处理特色,定义本身的界面绘制机制,甚至开发特定的界面组件。对于像JTable,JTree这样的界面对象,经过定制和优化,能够极大的提升其界面绘制的性能。
建议2.2.1.1:为了提升系统性能,能够根据业务处理特色,定义本身的界面绘制机制
建议2.2.1.2:为了提升系统性能,能够根据业务处理特色,开发本身的界面组件
在某OM系统中,对实时跟踪模块,早期的跟踪消息表格绘制方式采用的是JDK默认绘制方式,经过BasicTableUI对象实现表中单元格的绘制。JDK的这种方式具备共用性,提供的功能也很是多,但存在的问题是每绘制1个单元格,都要至少拷贝1个Graphics2D临时对象,同时要调用表格中的swing组件的paint()进行组件的绘制,在单元格组件的绘制过程当中,又建立了Graphics2D临时对象。
在后期的性能优化工做中,已经将实时跟踪上报消息采用优化表格进行显示。具体优化方法及步骤以下:
采用优化表格后,表格界面的绘制流程以下:
在实际开发中,通常有以下两种方法能够定义本身的界面绘制机制:
在这里须要指出的是,只有在充分研究分析了应用场景后,才能尝试采用定义本身的界面绘制机制来提升系统性能。通常状况下不建议改变Swing组件已有的绘制机制,一来出于工做量的考虑,二来出于通用性的考虑,再者也避免引入一些未知问题。
在某OM系统中,采用JDK的JTextArea类实现批处理文件的打开、编辑和保存等功能。在使用过程当中发现,当打开比较大的批处理文件(通常是几兆大小的文本文件)时,常常会致使系统灰屏、CPU占用率100%,甚至内存溢出现象。通过对JTextArea组件作性能测试,发现当打开比较大的文件时,JTextArea存在临时对象建立过多和内存占用过大的问题。如下是JTextArea组件和UltraEdit程序打开一样大小文件(文件大小为3.95M,共376792行)时的内存增量数据:
打开方式 |
物理内存增量(M) |
虚拟内存增量(M) |
JTextArea组件 |
52.4 |
53.3 |
UltraEdit程序 |
3.44 |
1.59 |
经过上面的内存增量数据能够看出,JDK的JTextArea组件打开4M左右的文件须要占用50M左右的内存,这在实际应用中是很难知足要求的,和UltralEdit程序比起来,性能要差一个数量级以上。
JTextArea组件打开文件时内存占用过大的主要缘由在于其文档模型(JTextArea使用PlainDocument对象来存储文本数据和文本结构信息)。总的来看,JTextArea的文档模型PlainDocument(其实Swing的全部Document对象都存在该问题)具备如下性能问题:
幸运的是,在java开源项目Jedit(注意,Jedit听从GPL协议,不能商用)中,提供了比JTextArea性能好得多的文本编辑组件。为了解决性能问题,咱们借鉴Jedit文本组件的设计思路,开发了本身的文本编辑组件。该组件相比JTextArea具备如下优势:
如下是咱们本身开发的文本编辑组件和JTextArea组件的性能对比数据(将组件都放在一个JFrame中,而后读取一个1.91M的文本文件,共49431行):
打开方式 |
GC后的OLD区内存占用状况(M) |
代码区内存占用状况(M) |
使用JTextArea |
12.653 |
4.686 |
使用本身开发的组件 |
4.464 |
4.160 |
Java语言不像C++语言同样,能够在栈上建立对象,随着函数调用完后对象可以自动被释放;另外也不能对new出来的对象进行delete。Java语言的这些限制致使了临时对象问题的存在。咱们在业务流程的设计实现中,从性能上考虑应尽可能保证流程处理的精简和高效,不然很容易产生临时对象的问题。
建议2.3.1.1:对于一些关键的业务处理流程,须要尽可能减小中间处理环节,从而避免建立没必要要的临时对象
建议2.3.1.2:在一些关键的业务处理流程中,对于必需要用到的对象,能够采起重用对象机制,重复利用,避免每次都建立
建议2.3.1.3:对于大多数业务处理来讲,临时对象都不是问题。只有对那些高频度或大数据量业务处理操做来讲,而且经过性能测试证实的确是临时对象引发了性能问题,才须要进行临时对象的优化。
在某OM系统的跟踪模块中,须要对实时上报的跟踪消息码流进行解析,而后将解析结果显示到界面表格中。跟踪消息码流的解析过程为:首先从handleMessage方法中接受要解析的消息码流包(一个大的byte数组,约6K),而后将该消息码流包分解成具体的跟踪消息帧;对每个跟踪消息帧,先解析出消息头,并根据消息头里面的信息校验消息长度的正确性,而后根据消息头信息获取消息体的码流,最后对消息体的码流进行解析,根据系统定义显示相关结果字符串到界面表格中。
在早期的跟踪消息码流解析处理流程中,会产生大量的临时byte数组对象:把一个消息包分解成消息帧,须要建立许多消息帧byte数组;对消息帧的消息头进行解析,又须要建立一个消息头的byte数组;再对消息帧中的消息体进行解析前,又须要建立一个消息体的byte数组。具体流程以下图所示:
在后期的跟踪模块性能优化中,针对跟踪消息码流的解析流程进行了优化。优化的主要思路是:在消息码流的整个解析过程当中,再也不建立新的byte数组,而是重用消息码流包的byte数组;在解析时,经过消息的偏移量定义,从byte数组中取出要解析的数据。具体流程以下图所示:
在告警浏览上报消息处理中,采用相似对象池的方式来重用对象,以减小临时对象的建立。告警浏览上报消息的解析流程以下:
从上面的流程图能够看出,告警浏览模块主要经过ObjPool来重用AlarmRecord对象。每次当须要一个AlarmRecord对象时,都从ObjPool里面取,若是没有才新建一个AlarmRecord对象;当AlarmRecord对象再也不使用时,将其从新放到ObjPool中。
对于桌面客户端系统来讲,界面性能是用户所关注的一项重要内容。通常界面性能应该包括:界面响应速度(这里特指界面的建立和显示时间)、界面刷新速度、界面友好性(这里特指进度条、鼠标置忙等响应措施)。提升系统界面性能的主要方法包括:
建议2.4.1.1:对于用户频繁进行开启、关闭的窗口组件,须要尽可能采起重用机制,用界面隐藏代替界面关闭
建议2.4.1.2:若是一个操做须要很长时间(如大于60秒),则要在执行操做以前就弹出提示选择界面,让用户选择是否真要执行该操做
建议2.4.1.3:若是一个操做须要较长时间(如大于3秒),则最好弹出明确的进度条提示界面
建议2.4.1.4:若是一个操做比通常操做耗时较长(如大于1秒),那么能够给出非显要的界面提示(如在窗口的状态栏给出相关提示)
在某OM系统中,消息详细解释功能是用户使用很是频繁的一个功能。在早期版本的消息详细解释子模块中,每次用户双击一条跟踪消息时,都会建立整个窗口界面,而后显示给用户。这种实现方式使得每次弹出窗口都须要3秒左右,响应速度比较慢。在后期版本改进中,对整个消息详细解释模块进行了重构和优化。优化后,只有第1次弹窗口时,才会建立界面组件,后面再次弹窗口时,只是更新一下数据模型就能够了。优化后的窗口弹出速度只须要1秒左右,响应速度明显提升。优化后的消息详细解释窗口显示流程以下图:
系统负载能力指的是在应用许可的任务负载下,系统的性能是否知足客户要求。这里的系统性能主要包括:在可能的极限负载下,系统是否能够保持正常运行,不发生崩溃或内存溢出等现象,而且界面能保持响应。从性能角度讲,通常随着执行任务的增长,系统响应时间应该是逐渐增长缓慢,而不是成指数增长的。下图左图是成指数增长的响应时间,右图是平缓增长的响应时间:
成指数增长的响应时间 平缓增长的响应时间
规则2.5.1.1:若是系统须要运行动态变化的负载,那么须要保证在可能的极限负载下,系统能够正常运行
在某OM系统中,存在一个跟踪消息上报的处理流程。当网元的跟踪消息上报流量太大时,容易致使系统性能严重降低,甚至发生灰屏、界面不响应的状况。为此,在跟踪消息处理流程中增长了一个流控机制,该流控机制能够根据系统资源可用状况,分级采起不一样的流控措施,当系统资源充足时,已发生的流控又可自行回复。流控机制的具体实施方案以下:
流控方案采用三级流控方式:1)显示流控:下降界面的刷新频率,2)存盘流控:消息只存盘不解析码流,也不显示在界面,3)过载流控:将当前流控的跟踪任务的消息缓存清空。具体表现为:当网元大量上报消息致使CPU使用率持续超过系统指定处理能力上限阈值(如95%)的时候,开始进入显示流控;进入显示流控后若是CPU使用率仍然持续超过系统指定处理能力上限阈值而且上报消息的速率持续大于系统指定的基线值(如40条/秒)的时候,开始进入存盘流控;进入存盘流控后若是CPU使用率仍然持续超过系统指定处理能力上限阈值而且客户端进程的CPU使用率大于45%的时候, 开始进入过载流控。流控的顺序是按照:正常情况->显示流控->存盘流控->过载流控方向进行流控;恢复的时候则是按照流控的逆过程: 恢复过载流控->恢复存盘流控->恢复显示流控->正常情况方向进行恢复。整个流控机制的流程图以下:
对于须要较长时间执行的业务处理,能够考虑分为多个相对独立的并发处理业务,采用多线程机制以缩短总的业务执行时间。如当用户双击一个很大的tmf文件时,能够在显示系统界面的过程当中,同时用另外一个线程去解析tmf文件,准备好要显示的数据。这样从双击文件到显示出最终数据的时间就会缩短。这种优化方法简单的说就是:将串行工做变为并行工做,以缩短整体工做时间;或者是利用系统空闲时间,作一些前期辅助工做,以缩短界面响应时间。
采用多线程机制提升业务处理速度时,其前提是要能够将业务处理划分为几个相对独立的处理逻辑,而后要么并发执行这些独立的处理逻辑,要么将一部分处理逻辑提早到系统空闲时间中执行。另一个问题是要控制好线程间的同步问题(能够经过调用wait方法或join方法来实现这一点)和相关资源的释放问题。
建议2.6.1.1:对于须要较长时间执行的业务处理,能够考虑采用多线程机制将业务处理划分为几个相对独立的处理逻辑并发执行,或者将一部分处理逻辑提早或延后到系统空闲时间中执行,以缩短总的业务执行时间
在某OM系统中,有一个跟踪消息体码流过滤功能,其要求的业务处理流程以下:
根据要求的业务处理流程,优化前代码处理逻辑以下图:
采用上面优化前的处理逻辑,当跟踪回顾界面中有50000条消息时,执行过滤所花的时间通常须要10秒左右。为了解决执行过滤所花时间过长的问题,对跟踪消息体过滤代码进行了优化,主要的优化思路是采用一个单独线程来收集码流数据,优化后的代码处理逻辑以下图:
采用上面优化后的处理逻辑,当跟踪回顾界面中有50000条消息时,执行过滤所花的时间能够由原来的10秒左右降为3秒左右。
在Java应用系统开发完毕后,须要对全部程序文件进行打包,制做成安装盘供用户安装使用。安装盘的大小和系统的安装时间也是用户比较关注的性能问题。安装盘越小,用户从网络上下载安装盘所需时间就越短;一样,安装时间越短,用户就能够在安装过程当中不用长时间的等待。所以,咱们在制做安装盘的时候,通常都要采用相关的压缩算法来对要发布的程序文件进行压缩,而不是简单的打包。对基于java开发的OM系统来讲,其程序文件中通常既包括了大量的java class文件、资源文件、也包括大量的DLL文件,为了使压缩后的安装盘尽量的小,咱们须要针对不一样的文件类型,采用不一样的压缩格式。针对java程序文件(以jar格式存在),一种最优的压缩格式是pack200压缩格式,而针对其它文件,一种很是高效的压缩格式是7z压缩格式。
对基于java开发的OM系统来讲,在实际应用中,能够先对每一个jar文件采用pack200方式进行压缩,而后对全部文件进行7z格式的压缩,实践证实,这种混合压缩方式制做的安装盘压缩比是很是优的。
Pack压缩格式最初是SUN公司为了减少JRE(J2SE v1.4.1 and J2SE1.4.2)安装盘大小而设计开发的。Pack压缩格式是JSR200项目,在JDK1.5中已提供实现。
当前咱们广泛使用的JAR压缩方式,是在字节层面对class文件进行的压缩。Pack压缩格式是在JAR压缩方式之上的二次压缩,它将对JAR里面的class文件和资源文件进行统一组织,同时去掉那些重复的共享数据结构。Pack压缩格式对jar文件的压缩很是高效,通常它能够将jar文件压缩到原来的1/7到1/9大小。
Pack压缩格式的java实如今jdk1.5中已提供,能够经过java.util.jar.pack200工具类进行使用。关于Pack压缩格式的详细信息能够从如下地址获取:
http://jcp.org/en/jsr/detail?id=200;
jdk1.5 API:java.util.jar.pack200;
jdk1.5的bin目录下有pack200.exe和unpack200.exe工具程序,能够经过命令行实现对jar文件的打包和解包。
7z是一种新的压缩格式(听从LGPL协议,能够商用),它拥有目前最高的压缩比。7z格式的主要特征有:
LZMA 算法是 7z 格式的默认标准算法。LZMA 算法的主要特征有:
目前支持7z格式的压缩软件有:7-Zip、WinRAR、PowerArchiver、TUGZip、IZArc。关于LZMA压缩算法的实现,当前已经有多个语言版本的软件开发工具包及源代码可供下载,包括:C,C++,C#,Java。关于7z和LZMA的详细资料能够从如下网址获取:
http://www.7-zip.org/zh-cn/7z.html
http://www.7-zip.org/zh-cn/sdk.html
在对系统进行性能优化的过程当中,性能测试工做起着相当重要的做用。一方面,在性能优化前,咱们须要经过性能测试找出系统的性能瓶颈所在,作到有目的的优化;另外一方面,咱们在对系统作了一个性能优化方案后,仍然须要经过性能测试来验证方案的优化效果,对于没有明显性能优化效果的方案,咱们通常是不建议给予实施的。
性能测试是一个比较具体化的工做,须要具体问题具体分析。总的来说,在性能测试过程当中都将面临两个问题:一个是性能测试工具的选择和使用,另一个是性能测试方法即如何设计测试用例和分析测试数据的问题。对于java应用系统来讲,目前有多种性能测试工具可供使用,下面将对这些工具一一作一个简要的介绍。不一样的性能测试工具所关注的性能测试点是不同的,因此咱们在性能测试过程当中,须要综合利用这些工具,从不一样的关注点来对一个系统性能作出全面的评估。另外,下面也将对一些性能测试方法作一个简要的说明和介绍。
目前,网络上有各类各样的profiler工具,通常最经常使用的是Borland公司的Optimizeit套件。经过Borland公司的profiler工具主要能够作如下事情:
Profiler工具的运行界面以下图:
Visualgc工具是sun公司开发的一个免费的性能测试工具,能够从sun公司网站上下载。经过visualgc工具主要能够作如下事情:
Visualgc工具的运行界面以下图:
打印和分析GC日志,是对java系统进行性能测试的一个重要手段。对sun公司的hotspot虚拟机来讲,能够添加相似以下的JVM参数来打印GC日志信息:
"-verbose:gc –XX +PrintGCDetails –Xloggc:c:\gclog\log.txt"
打印出GC日志后,能够经过GC日志分析工具来进行分析,如今网络上有诸如GCViewer之类的免费工具可供使用,固然也可直接查看和分析GC日志数据。经过分析GC日志,能够作以下事情:
Windows性能检视器是windows操做系统自带的一个性能测试工具。经过windows性能检视器,能够记录和检测进程或整个操做系统的资源使用状况。在windows性能检视器中,包含大量的性能计数器,下表列出了经常使用的性能计数器名:
计数器名 |
类别 |
说明 |
等价的任务管理器功能 |
Working Set |
Process |
驻留集,当前在实际内存中有多少页面 |
Mem Usage |
Private Bytes |
Process |
分配的私有虚拟内存总数,即提交的内存 |
VM Size |
Virtual Bytes |
Process |
虚拟地址空间的整体大小,包括共享页面。由于包含保留的内存,可能比前两个值大不少 |
无 |
Page Faults / sec(每秒钟内的页面错误数) |
Process |
每秒中出现的平均页面错误数 |
连接到 Page Faults(页面错误),显示页面错误总数 |
Committed Bytes(提交的字节数) |
Memory |
"提交"状态的虚拟内存总字节数 |
Commit Charge:total |
Processor Time |
Processor |
进程的CPU占用率 |
CPU Usage |
性能检视器的配置界面以下:
在对测试数据进行正式分析前,首先须要检查测试数据是否真实可靠。测试时获得了不可靠的测试数据的主要缘由有:
致使测试数据失真的缘由是很是多的,因此在分析测试数据以前须要检查测试数据的真实性和可靠性。检查的方法通常有:
在数据量不大的状况下,经过Excel工具进行数据处理是一个比较好的方法(Excel最多支持65536行,256列)。经过Excel工具处理数据的经常使用方法有:
性能测试的一个难点就是如何对测试数据进行分析,并找出各类测试结果产生的缘由。对测试数据进行分析时通常关注如下几个方面:
经过设计和执行测试用例来检测系统性能是最直接,也是最可靠的方式。但在实际操做中,若是对全部的状况都进行性能测试,每每工做量是巨大的,而且是得不偿失的。好比测试实时跟踪的一个性能优化方案的效果,首先在上报速度为200条/秒的状况下进行测试,发现优化效果明显。可是立刻你们会存在疑惑:那么在上报速度为100条/秒、50条/秒、10条/秒等状况下,这种优化效果是否依然存在呢?换句话说,若是效果不是很明显,那么咱们是否还有优化的必要呢?若是咱们为了回答这些疑问,针对全部这些状况都进行测试的话,其工做量将是很是大的。
实际上,只要改变测试用例中的任何一个测试条件,都将产生一个新的测试用例。所以咱们不可能对全部延伸出来的测试用例都进行测试。解决这个问题的办法应该采起理论和实践相结合的方式,首先经过基本测试用例获得几组测试数据,而后根据这些测试数据进行理论评估和公式推导,最后根据推导的公式给出当测试条件变化时的预期结果。
进行理论公式推导的方法是,首先根据业务代码创建起数据模型,而后将现有的测试数据代入数据模型中,得出可求解的公式。
序号No. |
文献编号或出处 Doc No. |
文献名称 Doc Title |
1 |
机械工业出版社,2003 |
Effective Java 中文版 |
3 |
http://java.sun.com/ |
Java Platform Performance: Strategies and Tactics |
4 |
O'reilly & Associates, 2001 |
Java Performance Tuning |
5 |
http://trove4j.sourceforge.net/ |
Trove集合类 |
6 |
IBM公司的developerworks 中国网站 |
性能观察: Trove 集合类 |
7 |
http://www.jedit.org/ |
Jedit源代码 |
8 |
Pack200资料 |
|
9 |
7z压缩格式资料 |