《Java程序性能优化》-笔记

时间:2023-02-02 05:56:05
第一章:性能调优概述
1.最有可能成为系统瓶颈的计算资源:
    网络操作、磁盘I/O、异常(异常的捕获和处理非常消耗资源)、数据库、锁竞争、CPU(计算性程序)、内存
2.根据“木桶原理”,必须对系统中表现最差的组件进行优化,而不是其它表现良好的。
3.提供系统性能应该尽力去降低串行化比重提高并行化比重,而不是一味的增加CPU数量。
4.调优层次:
    设计调优(宏观):开发前的优化,必须能评估出各种潜在的问题,良好的系统设计可以规避很多性能问题,必须多花时间在系统设计上,这是高性能程序的关键。
    代码调优(微观):开发过程中的优化,必须熟悉相关语言,找出最合适的API、算法、数据结构等。
    JVM调优:JVM的各项参数、垃圾回收策略等。
    数据库、网络、操作系统等:问题比较普遍,这个可以参考网络上提供的一些方案。
5.注意事项:
    调优必须有明确的目标,不能为了调优而调优或主观臆断需要调优,必须经过慎重的思考和测试,如果没有暴露出明显的性能问题而对其进行调整风险可能会大于收益。
第二章:设计优化
    1.单例模式:节省创建和GC的开销、对象更易于管理,使用静态内部类的方式实现单例是目前最好的,大型系统通常使用一些IOC框架完成此工作。
    2.代理模式:初始化代理组件要比初始化真实组件更加快速,在真正使用时候再由代理去初始化真实组件,代理模式多用于网络代理、安全代理、条件代理等。推荐使用:CGLIB。
    3.享元模式:享元模式为提升性能而生,主要作用是复用大对象(重量级对象),节省创建时间和内存空间。
    例如人事管理系统的SAAS软件中甲、乙、丙三个公司各为本SAAS的一个租户,每个公司(租户)下有100名员工可以登录查询,那么可以上每个租户下所有的员工共享一个数据库查询实例(因为同一个公司的数据在同一个数据库内),
    如此只需要三个享元实例就足以应付300个查询,注意:区别于对象池,对象池中的对象都是等价的可以相互替换,而享元池中的对象是不可相互替换的!
    4.装饰者模式:JDK中的I/O类族所使用此模式实现,不同的组合可以形成强大的对象。
    5.观察者模式:当一个对象依赖另一个对象的状态时很有用。可用于事件监听,通知发布等场合。它是如此之常用以至于被添加到了JDK中,可以直接使用。
    6.其它设计模式...
    7.善用缓冲:缓冲可以协调上层组件和下层组件的性能差,当上层组件性能优于下层组件时候,缓冲可以有效减少上层对下层的等待时间。
    典型应用如IO缓存,尽可能的在IO操作加缓冲组件以提高性能。
    8.善用缓存:将耗时的且不太会变化的数据暂存起来,节省大量的cpu使用或硬盘读取。举例:Ehcache、OSCache、JBoss Cache等。一般都是硬编码的方式,先从缓存找,找到即返回,否则计算得到并存储到缓存,
    优点:直白易操作,缺点:缓存组件与业务组件紧密耦合。基于动态代理的缓存方案,最大好处:业务层无需关注缓存的操作!它的原理和我们使用子类封装一样的,只不过没有直接写子类而是交给了动态代理框架来做,我们做的是对方法的复写过程,也就是aop
    9.池化对象:某对些象频繁的被使用而且可以复用,就可以考虑使用池,尤其在这些对象的创建开销较大时,性能会有明显提升,典型应用:数据库连接池、线程池。幸运的是我们不用去重复造*,apache有一套对象池组件:apache commons pool,
    注意只有重量级的对象使用对象池管理才可提高系统性能,对于轻量级的对象反而会有于池自身的调度降低系统性能。
    10.并行代替串行:随着电脑硬件的发展cpu的性能提高了很大,传统的串行程序已无法充分利用cpu资源导致浪费,java的多线程技术可以充分利用cpu运算资源以并行方式提高程序效率,java自身还提供了很多多线程的有力工具它们多位于java.util.concurrent下。
    11.负载均衡:当一台机器的处理依旧无法满足需求时可以考虑使用多台机器共同工作,每台机器承担较小的压力以提高系统性能,典型应用:Apache+Tomcat集群
    12.时间换空间:如果cpu资源很富足而内存资源很紧张,就需要这样做,改变算法结构减少了虽会增加cpu的开销但可减少内存的开销,这种方式可以缓解内存压力。
       空间换时间:相反地,如果cpu资源紧张而内存资源富足,就可以通过使用大量的内存存储中间变量等方式以节省cpu运算资源,典型应用:缓存,这种方式可以提供系统运行效率。
       这两种优化方式需要仔细考虑自己的算法结构以满足需求。
第三章:Java程序优化
    1.String类 内部组成:1个char数组,1个int的偏移量,1个int的长度,内存空间主要消耗在char数组上。
        其substring(int beginIndex, int endIndex)方法采用了空间换时间的做法,即使截取其中一个字符新生成的字符床依旧和原字符床持有相同的char数组,仅偏移量和长度不同而已,这无疑会造成内存的浪费,此方法的频繁使用会使永久区有可能出现内存泄漏(溢出)。
        使用String(String original)此构造方法进行包裹可避开此风险:new String(str.substring(begin, end));这个构造方法可以削减无用的内存开销。
        类似的还有:concat(String)/replace(char,char)/toLowerCase()/toUpperCase()/valueOf(char)等等,自行分析。
       split(String regex)方法非常强大,支持正则分割,然而对于简单的(分割符都一样)分割,它的效率不是最好的。使用简单高效的StringTokenizer代替。最高效的方法:自己利用indexOf('')和substring(int,int)两个方法查找到第一个,然后截取,对剩下的字符串依旧进行此操作即可。结论:封装的越厉害,效率就会越低。实际开放中一般使用StringTokenizer即可。
     字符串的charAt()也拥有超高的运行效率,如果要判断一个字符长如String str = "abcdefg"是否以"abc"开头,使用str.charAt(0)=='a' && str.charAt(0)=='b' && str.charAt(0)=='c' 要比str.startsWith("abc")更高效。
     由于String类设计的内部结构是一个不变的char数组,一旦赋值就不能再改变,所以在需要频繁拼接的场景下多使用StringBuilder(高效非同步)、StringBuffer(同步),java的编译器会对字符串的累加自动变成StringBuilder优化(反编译可看到),但在循环里面也会不断的创建StringBuilder,故不能依赖编译器的优化,尽量使用StringBuilder做累加操作。另外初始化时候如果可以指定合适的容量,效果会更好。
    2.ArrayList/Vector 基于数组 拥有很高的遍历效率 随机插入和删除效率低  LinkedList则相反 ,另外它们的三种遍历方法中for循环效率最高,forEach最低,迭代器居中。
    3.HashMap(双列,k/v) 通过对key计算hash直接映射内存地址,其检索速度超快,注意不要用糟糕的hashCode覆盖原有方法,最好给一个有意义(保证对象间不冲突)的实现
    4.HashSet(单列) 的内部是一个HashMap,它使用HashMap的key列存储数据,value列全部置空,因此它拥有HashMap同样高的检索性能。由于HashMap不允许key重复,故HashSet不允许插入重复值。
    5.LinkedHashMap 继承自HashMap,自然拥有了其优良特性,并且通过内部维护一个元素顺序(先进先出)链表弥补了HashMap的元素无顺序的缺陷,还可以设置按访问时间排序。
    6.LinkedHashSet 继承自HashSet。
    7.TreeMap也是一个有序的Map,它的顺序依赖value对象的排序规则,它有subMap(k2,k3):介于k2、k3之间,headMap(k2):大于k2,tailMap(k3):小于k3 等特色功能。
    8.TreeSet 对 TreeMap 包装
    9.NIO:提升I/O性能的两大组件:Channel、Buffer 对于大文件的复制性能较高
    10.强引用:所指对象在任何情况下都不会被回收,可能会引起内存泄漏。
       软引用(SoftReference):一个持有软引用的对象不会立即被回收,在临近阀值时被回收,可以使用一个引用队列,当对象被回收时会加入到这个队列中。适合做可有可无的数据缓存。
       弱引用(WeakReference):只要GC即被回收。适合做可有可无的生命周期更短的数据缓存。
       虚引用(PhantomReference):最弱的引用,和没有引用几乎是一样的,随时会被gc回收,它的作用主要是跟踪gc过程。
     11.慎用异常:try-catch 语句频繁使用会对性能影响很大,切勿通过try-catch异常捕捉来完成业务逻辑,如非必要请少用。
     13.静态变量、实例变量存放于堆中,远没有栈中变量访问效率高,所以如果一个变量若无被共享等需求应尽量在栈中初始化即在方法中定义和访问。
     14.在所有的运算中,位运算最高效。如果可以优先选择位运算如:乘以2  x <<= 1  除以2  x >>= 1
     15.switch 语句类似 if-else 性能虽然不差但也有提升空间,不可滥用,可以尝试将switch分支的结果存放于一个数组内,然后通过一个判断逻辑从数组内获取,这个判断逻辑自己写的可以超过switch内部的分支判断语句。如果遇到很多分支结构的判断可以去考虑下“状态模式”来设计程序。
     16.如果有多条计算语句它们的某些部分是相同,那么可以把相同的语句提取成公共的,计算一次后保存计算结构供每个表达式用,可以节省cpu资源,避免重复计算表达式,尤其在循环内部,这种优化是有必要的。
     17.减少循环次数:举例:for(int i=0;i<9999;i+=3){arr[i]=i;arr[i+1]=i+1;arr[i+2]=i+2;}
     18.arraycopy()是native方法效率很高,如果有可能我们应该尽量使用native的方法以最大限度提高性能。
     19.clone()方法可以绕过类的构造方法,对于构造耗时的对象可以考虑使用,但要注意默认的clone实现的是一个浅拷贝,如需要深拷贝需要重载clone方法。
      20.实例方法需要系统更多的开销维护以支持多态等特性,所以对于一些工具类、公共类、与实例变量无关等方法应尽量搞成静态方法以提高性能。
第四章:并行程序开发及优化(这部分的图和源码会独立安排)     1.Future模式:
    举例:我们在网上购物时,一般需要提交一个订单,订单提交完后就可以在家里等待送货上门,而通常商家处理订单送货上门的速度是比较慢的(会面对很多客户订单),这时候我们希望去做点别的事情而不是傻傻的一直在家里等。
    一般场景:某程序提交了一个请求希望的带回复,而服务器的回复时间却是不确定的,可能会很长(如互联网http、webservice请求等),传统的单线程是同步的,就是必须等待服务器返回数据才能继续后续工作。Future模式(在客户端使用)调用方式是异步的,可以不必等到结果。
    原理:服务器的长时间处理最终也是不可避免的,只不过客户端没有等待服务器处理完成就立即给自己设定一个代理数据假设完成(因为当前的工作并没有用到真实数据,相当于去提前准备数据去了)以从而可以进行后续工作,也就是把耗时长的数据提前请求去准备,进行后续工作在某个时刻用的时候就可以使用准备好的数据(如果用的时候还没有准备好就必需要阻塞等待,这个时候是不能异步的),可以节省等待时间。由于Future模式应用之广泛,JDK已经有了内置实现,功能很完善很强大。
    2.Master-Worker模式:是一种将串行任务并行化的模式,由Master和Worker两类进程,Master负责接收和分配任务,Worker负责处理任务,各个Worker处理完成后由Master归纳汇总。好处是将一个大任务分成小任务处理。
    3.生产-消费模式  经典模式,用于缓解服务器面对客户端请求的高并非压力,JDK提供的阻塞队列使其实现变得很简单。
    4.最优化线程池大小:NThreads=Ncpu*Ucpu*(1+W/C)  Ncpu:cpu数量(Runtime.getRuntime().availableProcessors());Ucpu:cpu使用率(0<1);W/C:等待时间/计算时间
    5.通过扩展线程池,可以使得开发者获取线程池内部的调度细节如当前执行的线程,执行前后进行日志等,对线程池故障排查有用。
    6.Vector是线程安全的List实现,其读写都会上锁,适合写多读少的场景。CopyOnWriteArrayList读操作不上锁,写操作进行拷贝,适合多线程下读操作远大于写操作的场景,写操作过多时性能影响大。写多的场景Vector性能好于CopyOnWriteArrayList。
    7.如果对已存在的List需要进行线程安全的操作,可以使用Collections.synchronizedList(list)包裹住,类似的也有Collections.synchronizedSet()等。
    8.CopyOnWriteArraySet完全依赖CopyOnWriteArrayList实现,也使用与读多写少的情况,如果Set需要在多线程想写多场景,可以使用Collections.synchronizedSet包裹普通的set
    9.Collections.synchronizedMap()返回的SynchronizedMap效率不行,多线程下的map建议使用专业的:ConcurrentHashMap
    10.高并发的队列:ConcurrentLinkedQueue。如果需要多线程共享数据需要使用BlockingQueue:LinkedBlockingQueue和ArrayBlockingQueue
    11.volatile:volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”,也就是说不保证线程执行的有序性(即你虽然读取到了正确的数据,但在接下来的使用中依然可能被其他线程所修改,也就是只是看到了最新数据,但不能锁住数据)。
    使用volatile声明的变量,线程会直接使用主存的变量数据,而不会使用线程内存的变量数据副本,这样就可以很快感知其它线程对数据的修改。
    注意:volatile不是线程安全的,如果希望其声明的变量线程安全,需要满足两个条件:
    a.对变量的写操作不依赖于当前值(即不能用于线程安全计数器等)。
    b.该变量没有包含在具有其他变量的不变式中(即)。
    实用场景:状态标志(状态修改后其他线程都可以立即感知并作出反应,如线程的启动停止控制)
    实际开发中要评估哪些类可能会被多线程访问,哪些属性需要只读一致性即可以声明为volatile的,如启动 停止 标志。
    12.jdk6以后的synchronized性能已经提升很高,因此在并发不是特别高的情况下绝对是上选,可以结合wait-notify机制实现线程间通信等特性synchronized用法需要注意的是,方法级别的synchronized虽然使用方便,但是这也就意味着整个方法是同步的,同步块范围很大,性能损伤很厉害,Collections.synchronized*就是这种场景,一般不能用于高并发下,降低synchronized的粒度可有效提升同步的效率,如将方法级别的synchronized改为方法内部某个需要同步的块上,因为方法内的所有逻辑不一定都需要同步尤其是运算型(耗时且无需同步)的,方法内部的某个逻辑上可能遇到某数据的同步,这种小粒度的加锁可以很快释放,这样及可以大幅度提升效率。总而言之,降低冲突的可能性可以提高效率。
    
    13.在高并发下的synchronized性能已不理想,替代方案有:重入锁ReentrantLock等。具体方案选用需要根据实际情况测试。ReentrantLock在高并发下性能明显优于synchronized,且有可中断、定时、支持公平(线程公平排队先进先出获取锁)等特性,阻塞队列BlockingQueue很多地方都用到了重入锁。可以简单理解为ReentrantLock是synchronized的改良版,但依然是一个粗粒度的锁即无读写分离。
    14.读写锁ReadWriteLock:实现了读写分离锁。读写锁实现了真正的并行读,并且在有写锁的时候,读也会阻塞
    15.使用锁Lock的newCondition方法得到一个与该锁绑定的Condition实例,类似于wait-notify机制完成多线程间的协调工作。
    16.Semaphore信号量:信号量的作用在于可以允许指定数量的线程同时访问一个对象(听上去像是支持并发访问),它是对锁的扩展,一般锁是只允许一个线程访问,无法做到同时允许几个线程加锁访问,信号量对锁做了扩展可以进行限定某资源的最大访问数,比如指定3个线程可同时并发访问一个资源,大与3个后需要等待获取锁。使用信号量实现数目的控制是上选,如线程池控制,当一个线程被使用时信号量就减少1,信号量减少到0时就不再处理请求,请求将被阻塞,一个线程处理完后信号量加1,等待的请求可以重新申请处理。
    17.锁分离思想:锁分离思想是通过降低锁的竞争率提高效率的一种思想如对于一个线程不安全的链表,从头部去掉一个和从尾部添加一个,虽然都是写操作,但基于链表的原理是不会互相影响的,只有同时头部取或者同时在尾部添加会不安全,同时头部取和尾部添加不会不安全,因此可以在头部和尾部各加一把锁,而不是对整个链表加锁,这种分离锁可以降低锁的竞争从而提高效率。
    18.锁粗化:这种思想似乎与锁细化、锁分离相背离,其实不然,比如在一个方法内部的n多逻辑出,出项了n多的对象加锁块,由于获取货也需要开销,不断的获取反而会效率不佳,这种情况反而不如在方法级加一把独占锁来的利落,不过还是要注意,除频繁的请求同一把锁需要加一个大锁外还是应该坚持锁细化的思想。
    19.自旋锁:如果请求独占资源时候没有成功则转而去执行一个空循环饭后再去检查条件是否符合,不符合则继续空循环,即自旋转,这种自旋可以防止程序被挂起,如果尝试几次依然无法获取锁则需要挂起以避免浪费过多的cpu资源。JVM中通过设置-XX:UseSpinning参数开启自旋锁,使用-XX:PreBlockSpin设置自旋等待次数。
    20.JVM锁消除:jvm编译时候会通过对运行上下文扫描,去除不可能存在共享资源的竞争锁,即消除无用的加锁,可以节省毫无意义的请求锁时间。
    21.无锁的并行计算:如果加锁无论什么时候无论怎么优化都难免有阻塞和等待,都难免会有性能的损失,那么可以采用无锁的并行计算!最简单的一种非阻塞同步以ThreadLocal为代表,每个线程都会有独立的数据副本以此不会有等待和阻塞的情况。
    无锁的并行计算是非常复杂且有难度的,但是他天生的高并发无需等待免疫死锁等特性很吸引人。在jdk中的java.util.concurrent.atomic包下有很多无锁的实现类,如无锁的原子AtomicInteger,无锁的原子 AtomicBoolean 等,一般他们是通过无限空循环(for(;;))和比较并交换的方式等待多线程间的处理获取合适的条件以避免线程不安全。原子类的性能要明显超过加锁处理,如果可以应该优先考虑使用。
第五章:JVM调优
    1.JVM 调优的主要过程有:
    1).确定堆内存大小(-Xmx -Xms)
    2).合理分配新生代和老年代(-XX:NewRatio -Xmn -XX:SurvivorRatio)
    3).确定永久区大小(-XX:Permsize -XX:MaxPermsize)
    4).选择合适的垃圾回收器 并 对垃圾回收器进行合理的配置
    5).禁用显示GC(-XX:+DisableExplicitGC)
    6).禁用类元数据回收(-Xnoclassgc)
    7).禁用类验证(-Xveriry:none)
    8).其它
    2.Apache性能测试工具:JMeter
第六章:JAVA性能调优工具
    1.JDK命令行工具:
    1)jps 可以列出系统中所有java应用程序,可方便地查看java进程的启动类、传入参数和jvm参数等信息。
    2)jstat 可方便的查看java应用程序的运行时信息如堆使用情况,gc情况等等。
    3)jinfo 可以查看运行时jvm某参数的实际取值,甚至可以运行时修改某些参数并立即生效,但可修改的参数有限。
    4)jmap 可以生成java的堆快照和对象的统计信息
    5)jhat 可以分析java的堆快照内容,可以通过浏览器查看分析内容
    6)jstack 可以生成java的线程堆栈
    2.可视化工具
    1)jconsole jdk自带,提供: jvm信息  应用程序概括 内存 线程 MBean等功能,小巧 简单 功能有限
    2)jvisualvm jdk自带 提供:多合一工具,可以代替jps、jstat、jmap、jhat、jstack 甚至jconsole,其线程页面可以自动检测死锁,包含两个抽样器,cpu和内存,cpu抽样可以找到占用cpu时间最长的方法(ps:不能提供该方法的调用堆栈,即无法知道被谁调用了多少次),可以通过设置要检测的包,如排除java.*,javax.*等。内存抽象可以实时检测系统中实例的分布情况和各个Class所占内存大小。其快照功能可以对堆信息和线程堆栈做实时快照另存,使用自带的堆快照分析工具可以分析保存的和其它第三方的堆快照。另外可以通过添加插件的方式添加更所实用功能如MBean管理、TDA、BTrace等。
    3.TDA : Threa Dump Analyzer
    线程快照分析工具,使用jstack命令或jvisualvm工具导出的线程堆栈的文本分析是很艰难的,TDA可以将文本的分析结果以图形形式展现出来。
    4.BTrace
    BTrace工具可以在不停机的情况下通过字节码注入动态检测系统运行情况,跟踪方法调用、系统内存等信息。在生产环境中可能经常遇到各种问题,定位问题需要获取程序运行时的数据信息,如方法参数、返回值、全局变量、堆栈信息等。为了获取这些数据信息,我们可以通过改写代码,增加日志信息的打印,再发布到生产环境。通过这种方式,一方面将增大定位问题的成本和周期,对于紧急问题无法做到及时响应;另一方面重新部署后环境可能已被破坏,很难重新问题的场景。 BTrace天生就为解决这类问题而来,BTrace在使用上有很多限制条件,如不能创建对象、数组、抛出和捕获异常、循环等。
    5.MAT  
    eclipse内存分析工具。可以分析jmap、jvisualvm等工具产生的堆快照文件,可以方便的查看对象引用和被引用等情况,以图形形式显示对象内存分布情况,检测可能存在的内存泄漏。mat还支持线程的分析等。
    6.JProfile
    前面介绍的都是免费工具,JProfile是一个商业软件,提供了一些免费软件无法达到的功能,它的主要功能包括:内存分析、快照分析、cpu分析、线程分析、jvm性能信息收集等待。