• Java性能优化(详解)


    引起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等循环外面创建,在循环里面进行重用
    对于高频度使用的对象,需要进行单独优化处理给以重用,例如改为类的成员变量

  • 相关阅读:
    Mapbox-gl 关闭所有Popup,以及关闭按钮出现黑色边框bug
    MySQL主从复制原理剖析与应用实践
    Go基础语法:切片
    项目九、无线组网
    vue模板语法上集
    Linux之多线程
    马蹄集OJ赛第十三次
    计算机视觉(CV)技术
    spyder配置文件位置及使用说明
    【力扣 Hot100 | 第七天】4.22(找到字符串中所有字母异位词)
  • 原文地址:https://blog.csdn.net/apple_51426592/article/details/126636014