Java性能优化干货
在优化性能之前,首先要清楚木桶原理:
系统的最终性能取决于系统中性能表现最差的组件.
程序的性能一般为如下几个方面:
(1)执行速度: 程序的反映是否迅速,响应时间是否足够短.
(2)内存分配: 内存分配是否合理,是否过多地消耗内存或者存在泄漏
(3)启动时间: 程序从运行到可以正常处理业务需要花费多长时间
(4)负载承受能力: 当系统压力上升时,系统的执行速度,响应时间的上升曲线是否平缓
性能的参考指标:
执行时间: 一段代码从开始到结束所使用的时间
CPU时间: 函数或者线程占用CPU的时间
内存分配: 程序在运行时占用的内存时间
磁盘吞吐量: 描述I/O的使用情况
网络吞吐量: 描述网络的使用情况
响应时间 :系统对某用户行为或者事件做出响应的时间.
最有可能成为系统瓶颈的计算资源如下:
磁盘I/O: 磁盘读写的速度要比内存慢很多
网络操作: 网络操作的速度可能比本地IO更慢.
CPU: 科学计算,3D渲染等对CPU需求旺盛的应用.
异常: 对Java应用来说,异常的捕获和处理是非常消耗资源的.
数据库: 操作时等待数据库的响应速度.
锁竞争: 对高并发程序来说, 如果存在激烈的锁竞争,无疑是对性能极大的打击.
内存: 一般来说,内存在读写速度上不太可能成为性能瓶颈.
加速比 = 优化前系统耗时 / 优化后系统耗时
加速比越高, 表明优化效果越明显.
性能优化的层次:
代码优化
软件架构上
JVM虚拟机层
数据库
操作系统层面
数据库调优:
在应用层对SQL语句进行优化
对数据库进行优化
对数据库软件进行优化
这里举一个简单的优化方法 PreparedStatement来代替Statement 优点如下:
(1)代码的可读性和可维护性.
(2)PreparedStatement尽最大可能提高性能.
(3)最重要的一点是极大地提高了安全性.
善用设计模式:
首先了解一个概念: 延迟加载: 如果没有使用当前对象或是组件,则不需要真正的初始化它.
然后进入正题, 一般提到设计模式 ,大家首先想到的就是单例模式,它的好处如下:
(1)对于频繁使用的对象, 可以省略创建对象所花费的时间,尤其是对于重量级的对象来说,可以省掉非常可观的一笔系统开销.
(2)由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC缩短时间
但是要注意的地方就是: 序列化和反序列化可能会破坏单例.
代理模式, 使用代理对象完成用户请求, 屏蔽用户对真实对象的访问.
最常见的应用场景就是平时操作数据库的时候, jdbc等数据库连接对象都是已经创建好的, 调用时就省去了初始化这种连接数据库engine的时间
动态代理, 运行时动态生成代理类.
享元模式, 是设计模式中少数几个以提高系统性能为目的的模式之一, 如果系统中存在多个相同的对象,那么只需共享一份对象的拷贝, 而不必为每一次使用都创建新的对象.
它的核心是享元工厂, 需要确保系统可以共享相同的对象.
主要优点:
(1)可以节省重复创建对象的开销
(2)由于创建对象的数量减少, 所以对系统内存的需求也减少.
装饰者模式,可以动态添加功能. 代码重用使用的是委托机制而不是继承, 因为继承是一种紧密耦合,父类如果改动还要改动子类.
JDK中outputStream和InputStream类族的实现是装饰者模式的典型应用.
观察者模式, 当一个对象的行为依赖于另一个对象的状态时, 观察者模式就相当有用.
观察者模式可以用于事件监听, 通知发布等场合. 可以确保观察者在不使用监控的情况下, 及时收到相关消息和事件.
常用优化组件和方法:
(1)缓冲: 缓冲区是一块特定的内存区域,jdk中很多I/O组件都提供了缓冲功能,
(2)缓存: 缓存也是一块为提升系统性能而开辟的内存空间.
(3)并行替代串行,随着多核时代的到来,CPU的并行能力有了很大的提升,在这个背景下 单纯的串行已经不能满足.java中 提供了Thread对象和runnable接口用于创建进程内的线程.
(4)负载均衡: 并发数很多的情况下,单台计算机无法承受, 这时候一般都可以搭建服务器集群.
在使用tomcat集群时,有两种基本的session共享模式, 黏性session模式 (一台用户只能在一个机器上操作, 不能共享)和复制session模式(所有session在所有tomcat节点上,一般情况还是用这种合适).
字符串优化处理
string对象及其特点:
首先要了解string类型的3个基本特点:
(1)不变性.
(2)针对常量池的优化
(3)类的final定义 (不可能有任何子类, 这是对系统安全性的保护)
注意: 不变模式是一个可以提高多线程程序性能 , 降低多线程程序复杂度的设计模式.
StringBuffer 和 StringBuilder
(1)String常量的累加操作
String result = "aaa" + "bbb" + "ccc";
StringBuilder result = new StringBuilder();
result.append("aaa");
result.append("bbb");
result.append("ccc");
以上这两种方法我觉得大多数人 都会以为是 第二种效率更高,但实际上恰恰相反, 因为对于静态字符串的连接操作, Java在编译时会进行彻底的优化, 将多个连接操作的字符串在编译时合成一个单独的长字符串.
(2)String变量的累加操作
String str1= "aaa";
String str2 = "bbb";
String str3 = "ccc";
String result = str1 + str2 + str3;
这段代码其实是和 StringBuilder执行速度一样的, 因为对于变量字符串的累加,Java也做了相应的优化操作, 使用了StringBuilder对象来实现字符串的累加.
总结一下: 在无需考虑线程安全的情况下可以使用性能较好的StringBuiler, 但若系统有线程安全要求, 只能选择StringBuffer.
两者都可以设置一个容量参数, 在不指定容量参数时, 默认是16个字节.扩容策略是将原有的容量大小翻倍.
核心数据结构:
Set接口:
Set集合中的元素是不能重复的. 基于Set的重要实现有以下三种 : HashSet LinkedHashset TreeSet
这三种跟Map基本都是对应起来的 . HashSet的输出毫无规律可言 , LinkedHashMap的输出顺序跟输入顺序完全一致 ,TreeSet则将所有输出从小到大排序.
List接口:
这里我们只讨论3种最重要的List实现: ArrayList Vector 和 LinkedList .
这三种List均来自AbstratList的实现. 而AbstratList直接实现了List接口, 并扩展自AbstratCollection.
ArrayList Vector 均使用了数组实现, 使用了几乎相同的算法 ,唯一的区别可以认为是对多线程的支持. 没有实现线程同步的ArrayList要稍好于Vector ,但差别不是很明显.
LinkedList链表由一系列表项连接而成. 一个表项总是包含3个部分: 元素内容 , 前驱表项, 后驱表项.
ArrayList中的add() 性能取决于ensureCapacity()方法, 处理容量参数为10 如果容量不够的话 自增到原来的1.5倍 . 如果能确定集合的大小 可以直接指定容量参数的大小这样性能会提升很多.
LinkedList由于使用了链表的结构, 因此不需要维护容量的大小. 然而 每次元素增加都需要新建一个Entry对象, 并进行更多的赋值操作, 在频繁的系统调用中, 对性能会产生一定的影响.
但是 , 如果是在任意位置新增或者删除元素 ,而不是在队尾新增 , 则比ArrayList 效率高非常多.ArrayList 在任意位置新增或删除时都要重新将元素复制一遍, 打破原有的数组排列顺序.
常用的集合遍历方法有3种:
Foreach , 迭代器 和 for循环
总结: 对ArrayList这些底层用数组实现来说, 随机访问的速度是很快的. 可以优先考虑
Map接口:
围绕map接口, 最主要的实现类有HashTable (子类中还有properties类的实现) HashMap LinkedHashMap 和 TreeMap .
首先解决一下HashMap 和 HashTable的异同 (同步 / key,value的要求 / 算法):
HashTable大部分方法同步, 线程安全, key 和 value的值 不允许使用null值, 而HashMap可以.
内部索引的映射算法不同.
尽管存在以上的诸多问题 , 但是两者实现的性能相差无几.
因为HashMap被广泛应用, 这里将一下HashMap的实现原理, 主要是将key作为hash算法, 然后将hash值映射到内存地址, 直接取得所对应的数据. 底层使用的数据结构是数组, 所谓的内存地址即数组的下标索引.
HashMap的高性能需要保证以下几点:
hash算法必须是高效的; hash值到内存地址(数组索引)的算法是快速的 ; 根据内存地址(数组索引) 可以直接取得对应的值.
HashMap初始大小为16, 最大长度是2的30次方,load factor默认是0.75,扩充的临界值是16*0.75=12 负载因子 = 元素个数 / 内部数组总大小
LinkedHashMap --> 有序的HashMap , HashMap最大功能缺点是他的无序性.
LinkedHashMap提供两种类型的排序: 一是元素插入时的顺序, 二是最近访问的顺序.
可以通过以下构造参数指定排序行为 :
public LinkedHashMap(int initialCapacity, float loadFactor ,boolean accessOrder) , 其中accessOrder为true时按照元素最后访问时间排序;
当assessOrder为false时 ,按照插入顺序排序默认为false.
TreeMap
从功能上讲 ,TreeMap有着比HashMap更为强大的功能, 它实现了SortedMap接口, 可以对元素进行排序
这两种可以排序的Map实现的区别: LinkedHashMap是基于元素进入集合的顺序排序, 而TreeMap则是基于元素的固有顺序(由Comparator或者 Comparable确定)
使用NIO (New I/O)提升性能:
由于I/O的速度要比内存慢 , 因此 ,在很多情况下 I/O 都会成为系统的瓶颈. 特性如下:
为所有的原始类型提供(Buffer)缓存支持;
增加通道(Channel)对象 , 作为新的原始I/O 抽象;
支持锁和内存映射文件的文件访问接口;
提供了基于Selector的异步网络 I/O
跟JDK1.4之前的区别是 : 之前的I/O是 流式 ,NIO是基于块(Block)的, 它是块为基本单位处理数据.在NIO中, 最为重要的两个组件是缓冲Buffer 和 通道 Channel.
缓冲是一块连续的内存块 , 是NIO读写数据的中转地 通道表示缓冲数据的源头或者目的地. 它用于向缓冲读写或者写入数据. 是访问缓冲的接口.
Buffer的基本原理 :
Buffer中有3个重要的参数 : 位置 (position),容量(capactiy) 和 上限(limit) .
强引用:
可以直接访问目标对象 ;
强引用所指向的对象在任何时候都不会被系统回收. JVM宁愿抛出OOM异常, 也不回收强引用所指向的对象;
强引用可能导致内存泄漏 .
有助于改善性能的技巧:
(1) 慎用异常: try-catch 对系统性能会造成影响
(2) 使用局部变量 : 局部变量的访问速度远远高于类的成员变量.
(3) 位运算代替乘除法: 在所有的运算中, 位运算是最为高效的, 最典型的就是对于整数的乘除运算优化.
a *=2 ; 优化为 : a <<=1;
a /=2 ; 优化为: a >>=1;
(4) 一维数组代替二维数组
(5)提取表达式 : 很多通用的代码 只需要初始化一次就可以了.
(6)使用buffer代替 I/O操作
(7)使用clone() 代替new
(8)用静态方法代替实例方法 : 对于一些工具类, 应该使用static方法实现 ,这样不仅可以加快函数调用的速度, 同时, 调用static方法也不需要生成类的实例,
比调用实例方法更为方便, 易用.
JDK多任务执行框架
线程的数量必须得到控制, 盲目地大量创建线程对系统性能是有伤害的.
线程池: 基本功能就是进行线程的复用.
使用线程池后, 线程的创建和关闭通常由线程池维护, 线程通常不会因为执行完一次任务而关闭, 线程池中的线程会被多个任务重复使用.
线程池的大小对系统性能有一定的影响. 一般来说 只要避免掉极大和极小的两种情况就可以了.<<java并发>>
Ncpu = CPU的数量
Ucpu = 目标CPU的使用率 , 0<=Ucpu<=1
W/C = 等待时间与计算时间的比率
最优线程池的大小等于 : Ncpu * Ucpu * (1 + W/C)
java中可以通过 Runtime.getRuntime().availableProcessors(); 获取cpu的个数.
JDK并发数据结构
着重介绍一些用于多线程环境的数据结构, 如并发list 并发set 并发map等.
并发list:
ArrayList不是线程安全的. 应该尽量避免在多线程环境中使用ArrayList ,如果因为某些原因必须使用的,则需要使用:
Collections.synchronizedList(List list) 进行包装.
同步关键字 synchronized 是 Java语言中最为常用的同步方法之一. 虽然 synchronized可以保证对象或者代码段的线程安全.
为了实现多线程间的交互 ,还需要使用Object对象的wait() 和 notify() 方法.
"锁"的性能和优化
在高并发的环境下, 激烈的锁竞争会导致程序的性能下降, 这边简单的介绍常见的锁:
线程的开销 , 避免死锁 , 减小锁持有时间 , 减小锁粒度 ,读写分离锁来替换独占锁, 锁分离 ,锁粗化, 自旋锁, 锁消除 , 锁偏向
JVM调优
由于java字节码是运行在JVM虚拟机上的, 同样的字节码使用不同的JVM虚拟机参数运行 ,其性能表现可能就不一样.
垃圾回收基础
Java语言的一大特点是可以进行自动垃圾回收处理. 但是当内存释放不够完全时, 即存在分配但永不释放的内存块 ,就会引起内存泄漏.严重时,导致程序瘫痪.
垃圾处理器的基本问题是:
哪些对象需要回收?
何时回收这些对象?
如何回收这些对象?
垃圾回收算法与思想:
1. 引用计数法
2. 标记-清除算法
3.复制算法
4.标记-压缩算法