Java性能优化(详解)

来源:互联网 发布:中国网络为什么多喷子 编辑:程序博客网 时间:2024/06/15 14:54
引起Java性能问题的常见原因之一是过多地创建临时对象

1、有效使用内存
精简业务流程,减少不必要的环节
按需创建对象,不要提前创建
重复的运算,考虑将结果转过变量(即方法的封装)
高频度使用的对象,单独处理使重复使用


2、高效使用循环
循环体外创建对象,使对象重复使用

两层循环的时候,注意优先顺序

最有可能的判断放前面(概率问题)

避免循环中重复运算



3、合理操作字符串
字符串的拼接使用StringBuilder或者StringBuffer
避免循环中重复运算
private int getKeyIndex(String key)
    {
        int index = -1;//循环前创建对象
        int count = 0;
        index = keys.indexOf(key);//避免循环体内的重复计算
        for (int i = 0; i <= index; i++)
        {
            if(keys.charAt(i) == ',')
            {
                count += 1;
            }
        }
        return count;
}

在java中,进行字符串查找匹配时一般有三种实现方式:一种是调用String对象的indexOf(String str)方法;第二种是调用String对象的matches(String regex)方法;第三种是直接使用正则表达式类(包括Pattern类、Matcher类)实现匹配
indexOf(String str)方法运行速度最快,效率最高,但不支持正则表达式
matches(String regex)方法性能最差,支持正则表达式,使用起来最简单。该方法性能差的原因是每调用一次时,就重新对正则表达式编译了一次,新建了一个Pattern对象出来,而不是重复利用同一个Pattern对象。
直接使用正则表达式类来实现匹配,可以支持正则表达式,在频繁操作下性能比matches(String regex)方法要好。
对于频繁的字符串匹配操作,应避免使用String. matches()方法。需要使用正则表达式匹配时,使用Pattern和Matcher类;

对于常量字符串,不要通过new方式来创建;
对于字符串常量之间的拼接,请使用“+”;对于字符串变量(不能在编译期间确定其具体值的字符串对象)之间的拼接,请使用StringBuilder;
在使用StringBuilder进行字符串操作时,请尽量设定初始容量大小;
当查找字符串,不需要支持正则表达式时,请使用indexOf(…)实现查找;当需要支持正则表达式时,如果需要多次匹配,请直接使用正则表达式类实现查找;
对于简单的字符串分割,请尽量使用自己定义的公用方法或StringTokenizer;

当需要对报文等文本字符串进行分析处理时,请加强检视,注意算法实现的优化。

不要频繁调用String.split()方法,对于需要分割字符串的热点函数,用indexOf和substring由于法实现字符串分割。



4、磁盘I/O

繁读取文件会导致进程CPU超高,同时系统WA值(等待IO读写CPU)也会飙升

避免频繁文件读写,因为频繁读写文件会导致CPU高,导致整个系统IO操作缓慢。对于配置文件,建议在进程启动时读入配置信息;读写文件时使用缓存。

在进程启动时读取该配置文件到内存,以后直接从内存中读取信息;如果该信息会变化,可以通过通知机制或定时读取配置文件刷新内存中的值
不使用缓冲的I/O操作会频繁的访问磁盘和调用操作系统底层函数,使用缓冲机制能带来显著的性能提升。
I/O优化(括号内为优化方案)
内存访问比硬盘I/O访问快万倍(考虑降低硬盘I/O访问次数,如硬盘数据库访问)
内存访问比网络I/O访问快百倍(降低进程间通信I/O次数,尤其是远程进程间通信I/O次数,如JDBC数据库访问)
网络I/O访问比硬盘I/O访问快百倍(降低CPU和内存等资源的占用)


5、网络I/O
没有特殊需要,尽量使用异步通信
基于socket开发的,尽量使用非阻塞I/O,比阻塞I/O一般快两倍(netty,mina)
远程通信可考虑基于二进制,性能往往比基于XML传输数据好
如果是基于XML的消息包,请使用StAX,不要使用DOM
如果使用SOAP,请考虑使用开源库Xfire/CXF,一般来说其性能是Apache Axis的3倍以上,比Axis2的性能也好一些
尽量降低远程进程间通信次数
在降低远程通信次数的同时,降低消息包的大小


6、数据库性能
连接池:物理连接的建立对性能影响很大,对于并发很高的应用,可适当考虑调高连接池的大小
访问频率:尽量降低对数据库的访问次数,避免频繁删除、更新数据库表,可以通过批量删除、更新,减少频率。
预编译:使用prepared statement,避免重复解析与编译
查询:查询大数据量时使用Prefetch
写数据:尽量使用批量写的方式(batch),但每个事务中的SQL不要超过500

简单语句:SQL不要太复杂,尤其是连表查询的表不要超过3个

对于同一张表,如果需要频繁执行insert语句,优先采用copy语句批量写入数据库,其次采用事务批量insert,批量越大,性能提升越明显。



7、多线程
在进行多线程调用时,获取单实例的方法上通过synchronized标记,这种方式,对于访问非常平凡的类,是极其消耗性能的。我们可以假设一种情形,有10个线程同时需要通过获取该类的实例,当第一个获取这个实例时,其他9个线程都必须等待第一个调用的结束,然后由第二个线程调用该实例方法,而剩下的8个得继续等待,如此依次执行获取实例,这样10个线程执行下来,对这个简单的获取实例方法变为串行操作,可这并不是必要的!同时调用该方法的线程越多,性能问题则越严重突出。
在编写多线程代码时,一方面需要注意代码的可充入性,另一方面尽量少用或者合理的使用同步等方法,提高程序的性能。

对于多线程应用场景,线程同步是不可避免的。同步会造成线程的等待或阻塞,带来额外的性能开销。从性能的角度出发,应当最低限度的使用同步。避免不必要的同步,避免多次同步


禁止频繁调用子进程,对于可以通过API完成的功能,不要通过执行系统命令完成。


线程同步的常见问题
需要同步的地方没有同步
单线程中运行的代码,增加不必要的同步
已经同步方法的代码,增加了不必要的二次同步
对可以使用线程局部变量规避同步的地方进行了同步
只需要对小段代码同步的地方,对整个方法进行了同步
采用多线程机制提高业务处理速度时,其前提是要可以将业务处理划分为几个相对独立的处理逻辑,然后要么并发执行这些独立的处理逻辑,要么将一部分处理逻辑提前到系统空闲时间中执行。另外一个问题是要控制好线程间的同步问题(可以通过调用wait方法或join方法来实现这一点)和相关资源的释放问题。

多线程与多进程比较,有什么相同和不同点?
是不是线程越多越好?


8、数据机构及算法
容器性能对比:
ArrayList 与 LinkedList都是实现了 List 接口的类,是有序集。 List 接口支持通过索引的方法来访问元素,对于这一点, ArrayList 没有任何问题;但是对于 LinkedList 则有很大的问题,链表本身不应该支持随机存储,但是作为 List 的一个实现,链表也提供了对随机访问的支持,但是效率很低。每次通过索引的方法都是进行一次遍历。我认为,其实就不应该让链表支持随机访问;而 Java 这样实现我想是因为整个集合框架的体系,使得链表与数组可以使用同样的方法使用。综上所述,对于 LinkedList 最好不使用随机访问,而使用迭代器。
(对能够确认大小的容器,初始化时设定具体大小,避免动态计算增加容量耗费性能)

List 从 Collection 接口中分立出来是因为 List 的特点——有序的集合。这里指的有序并不是按照大小排好序的( Sorted ),而是指集合是可以以确定的顺序访问的序列。针对 List 的这个特点,它比 Collection 接口增加了通过索引进行操作的方法。例如, add 、 remove 、 get 、 set 等方法的参数表中都可以加入索引的数值,从而操作处在索引位置处的元素。 

根据应用场景选择容器
容器因底层数据结构的不同而具有不同的特性,根据应用场景合理的选择容器,有助于提升代码性能。可能的应用场景包括: 随机读取优先、频繁修改优先、线程安全等。
ArrayList:适合随机读取
LinkedList:适合频繁插入/删除
HashSet:通用
TreeSet:按对象比较器顺序排序,性能较低
LinkedHashSet:按插入顺序排序
CopyOnWriteArraySet:适合频繁读取,线程安全
HashMap:通用
TreeMap:按对象比较器顺序排序,性能较低
LinkedHashMap:按插入顺序排序
ConCurrentHashMap:线程安全

案例:
在某产品的故障查询功能中,流水号占用内存巨大,一般数据量达到300万左右就会导致内存溢出
分析优化点:在java的链表结构中,以Object的方式保存内容,因此在保存流水号的时候必须用Integer对象,而不是基本整数类型int
在JAVA中一个Integer对象占用的内存大约为32个字节,而int类型占用4个字节

优化思路是自定义一个IntArrayList链表类,该类有两个特点:
保持与原接口兼容。它实现List接口,那么对外的接口几乎和原来链表类一致,这样对现有系统的改动是非常小的,只需要用新写的类替换掉程序中原有的类。
用基本数据类型替换对象。在新链表里面用一个int型数组来保存数据而不是用Object对象数组,这样就可以减少内存占用。


9、日志性能
大消息的正常处理流程不允许打印debug级别的日志。
messages是一个超大对象,不能对messages进行字符串拼接,因为不管系统设置哪种日志级别,拼接操作总会被执行。
日志打印过于频繁,会导致系统WA值(等待IO读写CPU)太高,进而使系统运行减慢
如果日志级别设为DEBUG以上,日志不会打印,也不会执行字符拼接操作。
日志必须按合适级别分级打印,并且可以通过开关控制打印级别,这样既能保证在正常情况下输出信息少,又能保证在需要日志的时候信息尽可能全。
调用日志打印方法时,尽量不要进行字符串拼接,因为不管是否打印日志,字符串拼接操作总会被执行,而字符串拼接是一个相当消耗CPU的操作,特别是和大对象拼接;可以通过传参数方式避免没有必要的字符串拼接。


10、JVM参数调优
申请与释放资源都是比较影响性能的,所以在需要的时候才申请,不用时立即释放。(有些以空间换时间的场景例外)
Java进程内存需合理设置
内存过小,应用不可用;
内存过大造成浪费而且导致应用性能降低;
JVM内存参数设置不合理,会导致GC频繁,影响应用的性能和可靠性。

其他注意事项:
使用缓冲IO进行读写操作。IO读写操作包括网络IO操作和磁盘IO操作。
使用System.copyArray()进行数组拷贝。
检查并消除内存泄漏。常见的内存泄漏原因有:
往ArrayList等集合对象中只添加而不删除元素,并保持对集合对象的引用。
在一个对象中创建了一个线程,当对象不再使用时,又没有关闭该线程,该对象不会被回收。
对于IO操作,没有在finally中作对应的关闭动作。

在重载的finallize()方法中,没有调用super.finallize()。

对于异步消息,如果消息量可能会很大,需要增加缓冲队列和流控机制


性能优化基本方法总结:
1、以空间换时间
在内存成本日益降低情况下,可以采用占用空间以换取执行效率。
使用数组、数据库做缓冲
2、最低限度的使用同步
3、改变判断顺序
根据概率,将最可能为真的放在前面判断
修改逻辑判断顺序
4、减少临时对象的创建
在实现业务处理流程的过程中,需要考虑临时对象引起的性能问题,精简业务处理流程,减少不必要的中间环节
对象的创建应尽量按需创建,而不是提前创建
对象的创建应尽量在for、while等循环外面创建,在循环里面进行重用
对于高频度使用的对象,需要进行单独优化处理给以重用,例如改为类的成员变量


0 0
原创粉丝点击