• 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.标记-压缩算法

  • 相关阅读:
    说说Java中的代理模式
    一个奇怪的异常
    JDBC第二次学习
    浅谈事务
    JDBC第一次学习
    Firebug & Chrome Console 控制台使用指南
    js 事件创建发布
    vue ui之 iview 事件拦截
    fetch获取json的正确姿势
    js对象通过属性路径获取属性值
  • 原文地址:https://www.cnblogs.com/pzyin/p/7977207.html
Copyright © 2020-2023  润新知