• Java程序性能优化Tip


    本博客是阅读<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());
            }
        }
    
    }
  • 相关阅读:
    Apollo与ROS
    QT windeployqt
    自定义QGraphicsItem
    ROS与C++
    aptitude与apt-get
    解决tcp粘包问题
    网络中两台主机通信
    I/O多路复用之select、poll、epoll
    Nginx命令行控制
    C++11
  • 原文地址:https://www.cnblogs.com/feichexia/p/3495554.html
Copyright © 2020-2023  润新知