本博客是阅读<java time and space performance tips>这本小书后整理的读书笔记性质博客,增加了几个测试代码,代码可以在此下载:java时空间性能优化测试代码 ,文件StopWatch是一个秒表计时工具类,它的代码在文末。

1. 时间优化

1.1 标准代码优化

a. 将循环不变量的计算移出循环

我写了一个测试例子如下:

import util.StopWatch;

/**
* 循环优化:
* 除了本例中将循环不变量移出循环外,还有将忙循环放在外层
* @author jxqlovejava
*
*/
public class LoopOptimization { public int size() {
try {
Thread.sleep(200); // 模拟耗时操作
}
catch(InterruptedException ie) { } return 10;
} public void slowLoop() {
StopWatch sw = new StopWatch("slowLoop");
sw.start(); for(int i = 0; i < size(); i++); sw.end();
sw.printEclapseDetail();
} public void optimizeLoop() {
StopWatch sw = new StopWatch("optimizeLoop");
sw.start(); // 将循环不变量移出循环
for(int i = 0, stop = size(); i < stop; i++); sw.end();
sw.printEclapseDetail();
} public static void main(String[] args) {
LoopOptimization loopOptimization = new LoopOptimization();
loopOptimization.slowLoop();
loopOptimization.optimizeLoop();
} }

测试结果如下:

slowLoop任务耗时(毫秒):2204
optimizeLoop任务耗时(毫秒):211

可以很清楚地看到不提出循环不变量比提出循环不变量要慢10倍,在循环次数越大并且循环不变量的计算越耗时的情况下,这种优化会越明显。

b. 避免重复计算

这条太常见,不举例了

c. 尽量减少数组索引访问次数,数组索引访问比一般的变量访问要慢得多

数组索引访问比如int i = array[0];需要进行一次数组索引访问(和数组索引访问需要检查索引是否越界有关系吧)。这条Tip经过我的测试发现效果不是很明显(但的确有一些时间性能提升),可能在数组是大数组、循环次数比较多的情况下更明显。测试代码如下:

import util.StopWatch;

/**
* 数组索引访问优化,尤其针对多维数组
* 这条优化技巧对时间性能提升不太明显,而且可能降低代码可读性
* @author jxqlovejava
*
*/
public class ArrayIndexAccessOptimization { private static final int m = 9; // 9行
private static final int n = 9; // 9列
private static final int[][] array = {
{ 1, 2, 3, 4, 5, 6, 7, 8, 9 },
{ 11, 12, 13, 14, 15, 16, 17, 18, 19 },
{ 21, 22, 23, 24, 25, 26, 27, 28, 29 },
{ 31, 32, 33, 34, 35, 36, 37, 38, 39 },
{ 41, 42, 43, 44, 45, 46, 47, 48, 49 },
{ 51, 52, 53, 54, 55, 56, 57, 58, 59 },
{ 61, 62, 63, 64, 65, 66, 67, 68, 69 },
{ 71, 72, 73, 74, 75, 76, 77, 78, 79 },
{ 81, 82, 83, 84, 85, 86, 87, 88, 89 },
{ 91, 92, 93, 94, 95, 96, 97, 98, 99 }
}; // 二维数组 public void slowArrayAccess() {
StopWatch sw = new StopWatch("slowArrayAccess");
sw.start(); for(int k = 0; k < 10000000; k++) {
int[] rowSum = new int[m];
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
rowSum[i] += array[i][j];
}
}
} sw.end();
sw.printEclapseDetail();
} public void optimizeArrayAccess() {
StopWatch sw = new StopWatch("optimizeArrayAccess");
sw.start(); for(int k = 0; k < 10000000; k++) {
int[] rowSum = new int[n];
for(int i = 0; i < m; i++) {
int[] arrI = array[i];
int sum = 0;
for(int j = 0; j < n; j++) {
sum += arrI[j];
}
rowSum[i] = sum;
}
} sw.end();
sw.printEclapseDetail();
} public static void main(String[] args) {
ArrayIndexAccessOptimization arrayIndexAccessOpt = new ArrayIndexAccessOptimization();
arrayIndexAccessOpt.slowArrayAccess();
arrayIndexAccessOpt.optimizeArrayAccess();
} }

d. 将常量声明为final static或者final,这样编译器就可以将它们内联并且在编译时就预先计算好它们的值

e. 用switch-case替代冗长的if-else-if

测试代码如下,但优化效果不明显:

import util.StopWatch;

/**
* 优化效果不明显
* @author jxqlovejava
*
*/
public class IfElseOptimization { public void slowIfElse() {
StopWatch sw = new StopWatch("slowIfElse");
sw.start(); for(int k = 0; k < 2000000000; k++) {
int i = 9;
if(i == 0) { }
else if(i == 1) { }
else if(i == 2) { }
else if(i == 3) { }
else if(i == 4) { }
else if(i == 5) { }
else if(i == 6) { }
else if(i == 7) { }
else if(i == 8) { }
else if(i == 9) { }
} sw.end();
sw.printEclapseDetail();
} public void optimizeIfElse() {
StopWatch sw = new StopWatch("optimizeIfElse");
sw.start(); for(int k = 0; k < 2000000000; k++) {
int i = 9;
switch(i) {
case 0:
break;
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 5:
break;
case 6:
break;
case 7:
break;
case 8:
break;
case 9:
break;
default:
}
} sw.end();
sw.printEclapseDetail();
} public static void main(String[] args) {
IfElseOptimization ifElseOpt = new IfElseOptimization();
ifElseOpt.slowIfElse();
ifElseOpt.optimizeIfElse();
} }

f. 如果冗长的if-else-if无法被switch-case替换,那么可以使用查表法优化

1.2 域和变量优化

a. 访问局部变量和方法参数比访问实例变量和类变量要快得多

b. 在嵌套的语句块内部或者循环内部生命变量并没有什么运行时开销,所以应该尽量将变量声明得越本地化(local)越好,这甚至会有助于编译器优化你的程序,也提高了代码可读性

1.3 字符串操作优化

a. 避免频繁地通过+运算符进行字符串拼接(老生常谈),因为它会不断地生成新字符串对象,而生成字符串对象不仅耗时而且耗内存(一些OOM错误是由这种场景导致的)。而要使用StringBuilder的append方法

b. 但对于这种String s = "hello" + " world"; 编译器会帮我们优化成String s = "hello world";实际上只生成了一个字符串对象"hello world",所以这种没关系
c. 避免频繁地对字符串对象调用substring和indexOf方法

1.4 常量数组优化

a. 避免在方法内部声明一个只包含常量的数组,应该把数组提为全局常量数组,这样可以避免每次方法调用都生成数组对象的时间开销

b. 对于一些耗时的运算比如除法运算、MOD运算、Log运算,可以采用预先计算值来优化

1.5 方法优化

a. 被private final static修饰的方法运行更快
b. 如果确定一个类的方法不需要被子类重写,那么将方法用final修饰,这样更快
c. 尽量使用接口作为方法参数或者其他地方,而不是接口的具体实现,这样也更快

1.6 排序和查找优化

a. 除非数组或者链表元素很少,否则不要使用选择排序、冒泡排序和插入排序。使用堆排序、归并排序和快速排序。

b. 更推荐的做法是使用JDK标准API内置的排序方法,时间复杂度为O(nlog(n))
    对数组排序用Arrays.sort(它的实现代码使用改良的快速排序算法,不会占用额外内存空间,但是不稳定)
    对链表排序用Collections.sort(稳定算法,但会使用额外内存空间)
c. 避免对数组和链表进行线性查找,除非你明确知道要查找的次数很少或者数组和链表长度很短
    对于数组使用Arrays.binarySearch,但前提是数组已经有序,并且数组如包含多个要查找的元素,不能保证返回哪一个的index
    对于链表使用Collections.binarySearch,前提也是链表已经有序
    使用哈希查找:HashSet<T>、HashMap<K, V>等
    使用二叉查找树:TreeSet<T>和TreeMap<K, V>,一般要提供一个Comparator作为构造函数参数,如果不提供则按照自然顺序排序

1.7 Exception优化

a. new Exception(...)会构建一个异常堆栈路径,非常耗费时间和空间,尤其是在递归调用的时候。创建异常对象一般比创建普通对象要慢30-100倍。自定义异常类时,层级不要太多。

b. 可以通过重写Exception类的fillInStackTrace方法而避免过长堆栈路径的生成

class MyException extends Exception {

    /**
*
*/
private static final long serialVersionUID = -1515205444433997458L; public Throwable fillInStackTrace() {
return this;
} }

c. 所以有节制地使用异常,不要将异常用于控制流程、终止循环等。只将异常用于意外和错误场景(文件找不到、非法输入格式等)。尽量复用之前创建的异常对象。

1.8 集合类优化

a. 如果使用HashSet或者HashMap,确保key对象有一个快速合理的hashCode实现,并且要遵守hashCode和equals实现规约
b. 如果使用TreeSet<T>或者TreeMap<K, V>,确保key对象有一个快速合理的compareTo实现;或者在创建TreeSet<T>或者TreeMap<K, V>时显式提供一个Comparator<T>
c. 对链表遍历优先使用迭代器遍历或者for(T x: lst),for(T x: lst)隐式地使用了迭代器来遍历链表。而对于数组遍历优先使用索引访问:for(int i = 0; i < array.length; i++)

d. 避免频繁调用LinkedList<T>或ArrayList<T>的remove(Object o)方法,它们会进行线性查找
e. 避免频繁调用LinkedList<T>的add(int i, T x)和remove(int i)方法,它们会执行线性查找来确定索引为i的元素

f. 最好避免遗留的集合类如Vector、Hashtable和Stack,因为它们的所有方法都用synchronized修饰,每个方法调用都必须先获得对象内置锁,增加了运行时开销。如果确实需要一个同步的集合,使用synchronziedCollection以及其他类似方法,或者使用ConcurrentHashMap

1.9 IO优化

a. 使用缓冲输入和输出(BufferedReader、BufferedWriter、BufferedInputStream和BufferedOutputStream)可以提升IO速度20倍的样子,我以前写过一个读取大文件(9M多,64位Mac系统,8G内存)的代码测试例子,如下:

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader; import util.StopWatch; public class ReadFileDemos {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\jxqlovejava\\workspace\\PerformanceOptimization\\test.txt";
InputStream in = null;
BufferedInputStream bis = null;
File file = null;
StopWatch sw = new StopWatch(); sw.clear();
sw.setTaskName("一次性读取到字节数组+BufferedReader");
sw.start();
file = new File(filePath);
in = new FileInputStream(filePath);
BufferedReader br = new BufferedReader(new InputStreamReader(in));
char[] charBuf = new char[(int) file.length()];
br.read(charBuf);
br.close();
in.close();
sw.end();
sw.printEclapseDetail(); sw.clear();
sw.setTaskName("一次性读取到字节数组");
sw.start();
in = new FileInputStream(filePath);
byte[] buf = new byte[in.available()];
in.read(buf);// read(byte[] buf)方法重载
in.close();
for (byte c : buf) {
}
sw.end();
sw.printEclapseDetail(); sw.clear();
sw.setTaskName("BufferedInputStream逐字节读取");
sw.start();
in = new FileInputStream(filePath);
bis = new BufferedInputStream(in);
int b;
while ((b = bis.read()) != -1);
in.close();
bis.close();
sw.end();
sw.printEclapseDetail(); sw.clear();
sw.setTaskName("BufferedInputStream+DataInputStream分批读取到字节数组");
sw.start();
in = new FileInputStream(filePath);
bis = new BufferedInputStream(in);
DataInputStream dis = new DataInputStream(bis);
byte[] buf2 = new byte[1024*4]; // 4k per buffer
int len = -1;
StringBuffer sb = new StringBuffer();
while((len=dis.read(buf2)) != -1 ) {
// response.getOutputStream().write(b, 0, len);
sb.append(new String(buf2));
}
dis.close();
bis.close();
in.close();
sw.end();
sw.printEclapseDetail(); sw.clear();
sw.setTaskName("FileInputStream逐字节读取");
sw.start();
in = new FileInputStream(filePath);
int c;
while ((c = in.read()) != -1);
in.close();
sw.end();
sw.printEclapseDetail();
}
}

结果如下:

一次性读取到字节数组+BufferedReader任务耗时(毫秒):121
一次性读取到字节数组任务耗时(毫秒):23
BufferedInputStream逐字节读取任务耗时(毫秒):408
BufferedInputStream+DataInputStream分批读取到字节数组任务耗时(毫秒):147
FileInputStream逐字节读取任务耗时(毫秒):38122

b. 将文件压缩后存到磁盘,这样读取时更快,虽然会耗费额外的CPU来进行解压缩。网络传输时也尽量压缩后传输。Java中压缩有关的类:ZipInputStream、ZipOutputStream、GZIPInputStream和GZIPOutputStream

1.10 对象创建优化

a. 如果程序使用很多空间(内存),它一般也将耗费更多的时间:对象分配和垃圾回收需要耗费时间、使用过多内存可能导致不能很好利用CPU缓存甚至可能需要使用虚存(访问磁盘而不是RAM)。而且根据JVM的垃圾回收器的不同,使用太多内存可能导致长时间的回收停顿,这对于交互式系统和实时应用是不能忍受的。

b. 对象创建需要耗费时间(分配内存、初始化、垃圾回收等),所以避免不必要的对象创建。但是记住不要轻易引入对象池除非确实有必要。大部分情况,使用对象池仅仅会导致代码量增加和维护代价增大,并且对象池可能引入一些微妙的问题

c. 不要创建一些不会被使用到的对象

1.11 数组批量操作优化

数组批量操作比对数组进行for循环要快得多,部分原因在于数组批量操作只需进行一次边界检查,而对数组进行for循环,每一次循环都必须检查边界。

a. System.arrayCopy(src, si, dst, di, n) 从源数组src拷贝片段[si...si+n-1]到目标数组dst[di...di+n-1]

b. boolean Arrays.equals(arr1, arr2) 返回true,当且仅当arr1和arr2的长度相等并且元素一一对象相等(equals)

c. void Arrays.fill(arr, x) 将数组arr的所有元素设置为x

d. void Arrays.fill(arr, i, j x) 将数组arr的[i..j-1]索引处的元素设置为x

e. int Arrays.hashCode(arr) 基于数组的元素计算数组的hashcode

1.12 科学计算优化

Colt(http://acs.lbl.gov/software/colt/)是一个科学计算开源库,可以用于线性代数、稀疏和紧凑矩阵、数据分析统计,随机数生成,数组算法,代数函数和复数等。

1.13 反射优化

a. 通过反射创建对象、访问属性、调用方法比一般的创建对象、访问属性和调用方法要慢得多

b. 访问权限检查(反射调用private方法或者反射访问private属性时会进行访问权限检查,需要通过setAccessible(true)来达到目的)可能会让反射调用方法更慢,可以通过将方法声明为public来比避免一些开销。这样做之后可以提高8倍。

1.14 编译器和JVM平台优化

a. Sun公司的HotSpot Client JVM会进行一些代码优化,但一般将快速启动放在主动优化之前进行考虑

b. Sun公司的HotSpot Server JVM(-server选项,Windows平台无效)会进行一些主动优化,但可能带来更长的启动延迟

c. IBM的JVM也会进行一些主动优化

d. J2ME和一些手持设备(如PDA)不包含JIT编译,很可能不会进行任何优化

1.15 Profile

2. 空间优化

2.1 堆(对象)和栈(方法参数、局部变量等)。堆被所有线程共享,但栈被每个线程独享

2.2 空间消耗的三个重要方面是:Allocation Rate(分配频率)、Retention(保留率)和Fragmentation(内存碎片)
      Allocation Rate是程序创建新对象的频率,频率越高耗费的时间和空间越多。
      Retention是存活的堆数据数量。这个值越高需要耗费越多的空间和时间(垃圾回收器执行分配和去分配工作时需要进行更多的管理工作)
      Fragmentation:内存碎片是指小块无法使用的内存。如果一直持续创建大对象,可能会引起过多的内存碎片。从而需要更多的时间分配内存(因为要查找一个足够大的连续可用内存块),并且会浪费更多的空间因为内存碎片无法被利用。当然某些GC算法可以避免过多内存碎片的产生,但相应的算法代价也较高。

2.3 内存泄露

2.4 垃圾回收器的种类(分代收集、标记清除、引用计数、增量收集、压缩...)对Allocation Rate、Retention和Fragmentation的时间空间消耗影响很大

2.5 对象延迟创建

附上StopWatch计时工具类:

/**
* 秒表类,用于计算执行时间
* 注意该类是非线程安全的
* @author jxqlovejava
*
*/
public class StopWatch { private static final String DEFAULT_TASK_NAME = "defaultTask";
private String taskName;
private long start, end;
private boolean hasStarted, hasEnded; // 时间单位枚举:毫秒、秒和分钟
public enum TimeUnit { MILLI, SECOND, MINUTE } public StopWatch() {
this(DEFAULT_TASK_NAME);
} public StopWatch(String taskName) {
this.taskName = StringUtil.isEmpty(taskName) ? DEFAULT_TASK_NAME : taskName;
} public void start() {
start = System.currentTimeMillis();
hasStarted = true;
} public void end() {
if(!hasStarted) {
throw new IllegalOperationException("调用StopWatch的end()方法之前请先调用start()方法");
}
end = System.currentTimeMillis();
hasEnded = true;
} public void clear() {
this.start = 0;
this.end = 0; this.hasStarted = false;
this.hasEnded = false;
} /**
* 获取总耗时,单位为毫秒
* @return 消耗的时间,单位为毫秒
*/
public long getEclapsedMillis() {
if(!hasEnded) {
throw new IllegalOperationException("请先调用end()方法");
} return (end-start);
} /**
* 获取总耗时,单位为秒
* @return 消耗的时间,单位为秒
*/
public long getElapsedSeconds() {
return this.getEclapsedMillis() / 1000;
} /**
* 获取总耗时,单位为分钟
* @return 消耗的时间,单位为分钟
*/
public long getElapsedMinutes() {
return this.getEclapsedMillis() / (1000*60);
} public void setTaskName(String taskName) {
this.taskName = StringUtil.isEmpty(taskName) ? DEFAULT_TASK_NAME : taskName;
} public String getTaskName() {
return this.taskName;
} /**
* 输出任务耗时情况,单位默认为毫秒
*/
public void printEclapseDetail() {
this.printEclapseDetail(TimeUnit.MILLI);
} /**
* 输出任务耗时情况,可以指定毫秒、秒和分钟三种时间单位
* @param timeUnit 时间单位
*/
public void printEclapseDetail(TimeUnit timeUnit) {
switch(timeUnit) {
case MILLI:
System.out.println(this.getTaskName() + "任务耗时(毫秒):" + this.getEclapsedMillis());
break;
case SECOND:
System.out.println(this.getTaskName() + "任务耗时(秒):" + this.getElapsedSeconds());
break;
case MINUTE:
System.out.println(this.getTaskName() + "任务耗时(分钟):" + this.getElapsedMinutes());
break;
default:
System.out.println(this.getTaskName() + "任务耗时(毫秒):" + this.getEclapsedMillis());
}
} }

Java程序性能优化Tip的更多相关文章

  1. Java程序性能优化技巧

    Java程序性能优化技巧 多线程.集合.网络编程.内存优化.缓冲..spring.设计模式.软件工程.编程思想 1.生成对象时,合理分配空间和大小new ArrayList(100); 2.优化for ...

  2. 《Java程序性能优化:让你的Java程序更快、更稳定》

    Java程序性能优化:让你的Java程序更快.更稳定, 卓越网更便宜,不错的书吧

  3. [JAVA] java程序性能优化

    一.避免在循环条件中使用复杂表达式 在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快. 例子: import java.util ...

  4. Java程序性能优化之性能概述

    性能的基本概念 一).什么叫程序的性能? 程序运行所需的内存和时间. 二).性能的表现形式: 1).执行速度: 程序的反应是否迅速,响应时间是否足够短. 2).启动时间:程序从运行到可以处理正常业务所 ...

  5. java程序性能优化

    一.避免在循环条件中使用复杂表达式 在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快. 例子: import java.util ...

  6. Java程序性能优化——让你的java程序更快、更稳定

    1.Java性能调优概述 1.1.Web服务器,响应时间.吞吐量是两个重要的性能参数. 1.2.程序性能的几个表现: 执行速度:程序的反映是否迅速,响应时间是否足够短 内存分配:分配是否合理,是否过多 ...

  7. 《Java程序性能优化》学习笔记 设计优化

    豆瓣读书:http://book.douban.com/subject/19969386/ 第一章 Java性能调优概述 1.性能的参考指标 执行时间: CPU时间: 内存分配: 磁盘吞吐量: 网络吞 ...

  8. Java程序性能优化读书笔记(一):Java性能调优概述

    程序性能的主要表现点: 执行速度:程序的反映是否迅速,响应时间是否足够短 内存分配:内存分配是否合理,是否过多地消耗内存或者存在内存泄漏 启动时间:程序从运行到可以正常处理业务需要花费多少时间 负载承 ...

  9. 《Java程序性能优化》之设计优化

    豆瓣读书:http://book.douban.com/subject/19969386/ 第一章 Java性能调优概述 1.性能的参考指标 执行时间: CPU时间: 内存分配: 磁盘吞吐量: 网络吞 ...

随机推荐

  1. html5之canvas画图

    导航 前言 基本知识 绘制矩形 清除矩形区域 圆弧 路径 绘制线段 绘制贝塞尔曲线 线性渐变 径向渐变(发散) 图形变形(平移.旋转.缩放) 矩阵变换(图形变形的机制) 图形组合 给图形绘制阴影 绘制 ...

  2. hasOwnProperty()&&isPrototypeOf()

    1.hasOwnProperty() hasOwnProperty()函数用于指示一个对象自身(不包括原型链)是否具有指定名称的属性.如果有,返回true,否则返回false. 该方法属于Object ...

  3. 关于解决keil4和mdk共存后51不能使用go to definition Of 'XXXXXX'问题

    自己安装keil4和mdk共存后,(我是先安装的keil 后安装的 MDK),在51单片机工程里不能使用go to definition Of 'XXXXXX'问题, 类似的如图 已经困扰了好长时间, ...

  4. paip java.net.SocketException No buffer space available的解决办法及总结

    java.net.SocketException No buffer space available的解决办法及总结 作者Attilax  艾龙,  EMAIL:1466519819@qq.com 来 ...

  5. iOS开发之静态库(一)—— 基本概念

    在项目开发过程中,经常出现优秀代码重用现象,又或者提供给第三方功能模块却又不想让其看到源代码,这些时候,通常的做法是将代码封装成库或者框架,这些在Windows编程或Linux编程中非常容易实现的过程 ...

  6. javaweb学习总结(二十)——JavaBean总结

    一.什么是JavaBean JavaBean是一个遵循特定写法的Java类,它通常具有如下特点: 这个Java类必须具有一个无参的构造函数 属性必须私有化. 私有化的属性必须通过public类型的方法 ...

  7. 理解与模拟一个简单servlet容器

    servlet接口 使用servlet编程需要实现或者继承实现了javax.servlet.Servlet接口的类,其中定义了5个签名方法: public void init(ServletConfi ...

  8. OpenGL学习进程(11)第八课:颜色绘制的详解

        本节是OpenGL学习的第八个课时,下面将详细介绍OpenGL的颜色模式,颜色混合以及抗锯齿.     (1)颜色模式: OpenGL支持两种颜色模式:一种是RGBA,一种是颜色索引模式. R ...

  9. linux之cp/scp命令+scp命令详解(转)

    名称:cp 使用权限:所有使用者 使用方式: cp [options] source dest cp [options] source... directory 说明:将一个档案拷贝至另一档案,或将数 ...

  10. SCN试验之二 checkpoin scn 与数据库scn的关系

    oracle11g 观察数据库scn: SQL> select dbms_flashback.get_system_change_number from dual; GET_SYSTEM_CHA ...