
咱们知道JAVA语言与C语言的其中一个区别就是JVM中有垃圾回收器能够经过对运行中的对象进行判断是否存活而且将在内存中已经不在使用的对象进行回收释放其所占用的内存,而C语言须要进行手动的释放内存,1个对象的建立使用释放都须要程序进行显式的操做。固然不论是C仍是JAVA都有本身适合的开发领域。
对于代码性能优化,对于项目前期因为前期数据量并非太大可是随着时间的推移数据量的激增,若是没有良好的编码习惯后期会带来较大的性能开销,故项目初期编码过程应养成良好的习惯避免后期繁重的codereview等。
本篇博客将笔者在工做过程当中遇到的性能瓶颈以及在其中是如何进行优化进行记录,以便后续有相关需求的小伙伴进行采纳借鉴。
***java
对象篇
在JMM中(JAVA内存模型)中保存对象的开销实际上是至关大的,而且JMM要求对象必须按照8个字节对齐,虽然JMM会提供对字段的重排序来避免用户随意指定对象字段的顺序以此来尝试各个字段重排达到下降总体的对象开销。可是即便是这样1个空的String对象仍然得占用 对象头(8 字节)+ 引用 (4 字节 ) + char 数组(16 字节)+ 1个 int(4字节)+ 1个long(8字节)= 40 字节 若是在程序过程当中产生大量的对象无疑会加剧GC的工做。所以在实际编码过程当中应该避免建立大量的对象,在不影响代码可读性及程序并发问题时,应该尽量复用已有的对象,减轻GC的压力。正则表达式
- 使用StringBuild/StringBuffer代替循环体内进行字符串拼接,StringBuild与StringBuffer底层会默认建立一个16char数组进行字符串缓存,等到须要时在建立出String对象,而不是每一次都进行String对象建立。
- 使用StringTokenizer 代替Split 作字符串切割(若是不涉及到复杂正则只是简单字符串切割)
- JAVA编译时会对常量拼接进行从新定义做为一个完整的常量故无需纠结性能问题,可见以下程序块。
final String aa = "a"
final String bb = "b"
System.err.println((aa + bb) == (aa + bb));//true
- JDK1.6先后的subString方法实现
在JDK1.6 subString内部实现是子String仍然会保留父String的char数组引用,这在必定程度上会形成内存泄露,特别是当有特别长的字符串实际上只须要其特别短的字符串,可是因为引用依赖,GC没法回收,形成父String一直滞留在JVM内存中没法回收。
JDK1.6以后subString内部对其优化,调用拷贝父String的char数组中的子char数组造成一份新的副本,如此子String与父String之间不存在依赖关系,GC可以对不在使用的父String进行GC回收。
- 循环遍历中若是存在重复建立大量相同的字符串,建议建立缓存池进行对象缓存。
- 避免使用正则表达式,如必要使用至少要把Pattern进行缓存,避免反正建立Pattern编译。
- 当须要对一个基本数据类型进行字符串转换应该尽量使用toString或者String.valueOf(obj) 替换 obj+""。
在进行大文本字符串拼接时,应该为StringBuffer,StringBuilder设置初始化容量值
***数据库
JVM篇
- 将-Xms与-Xmx设置相同,避免每次垃圾回收完成后JVM从新分配内存。
- 原则上应该避免太大深度的递归,毕竟递归越深其中间栈帧所产生的数据引用仍然有效,没法被GC清除。
- 当虚拟机栈中须要存储基本数据或对象引用时须要调整-Xss来避免发生StackOverflowError异常
- 在虚拟机栈中随着栈帧的pop会对栈帧内的内存进行及时的清理,因此在局部方法内部中其中间变量应该尽量使用8大基本数据类型才可以随着栈帧结束而当即内存回收。
- 为了不频繁触发JVM对基本数据类型进行拆包与装包操做,其中间变量应该尽可能使用基本数据类型而不是其包装类型。
- JDK默认只缓存-128~+127的Integer和Long 若是超出该范围则会建立出新的对象,若是对计算数据敏感但是适当经过-XX:AutoBoxCacheMax增大缓存范围。
- G1与CMS收集器的选择,G1收集器会对堆内内存进行划分红Region,其堆越小则划分的Region数也会越少,在小堆的表现并不会比CMS突出。笔者认为8G如下采用CMS比较合适。目前比较期待Java 11 新加入的ZGC号称能够达到10ms 如下的 GC 停顿。
- 经过-XX:+AlwaysPreTouch提早初始化好真正的物理内存,而不是须要才进行内存申请初始化。默认未添加该参数时而-Xms、-Xmx只是告诉告诉操做系统须要多少内存,从而避免被其余进程使用,而只有当正直使用时才会进行内存逐渐申请。好比在堆中Eden区进行对象建立又或者Young区转Old区的内存空间。不过该参数也会影响启动时长,随着堆内存越大启动时间也会增大。
JDK监控工具 Jconsole,jProfile,VisualVM 能够经过可视化界面查看JAVA堆内存使用状况进行判断是否须要进行代码优化。
***数组
线程篇
- 如何为线程池选择合适线程数?
线程任务通常能够分为计算密集型和IO密集型。
计算密集型CPU处于忙碌中,此时须要作内存数据读写计算,没有任务的阻塞状态。而IO密集型任务,在执行IO时堵塞,CPU处于等待状态,等待过程当中系统会将时间片分给其余线程处理。
计算密集型任务线程数最好与系统核心数挂钩,毕竟4核单线程主机在某一时刻只能同时跑4个线程,若是过多的线程数反而会由于切换上下文而耗费更多的任务时间,能够经过调用JDK自带的方法Runtime.availableProcessors得到系统支持的能够核心数。N+1。
对于IO密集型,合适的线程数能够得到良好的性能支持,系统经过将IO阻塞的任务线程的时间片交给未被阻塞的任务线程,经过合适的调度发挥出比单线程更好的性能支持。在选择线程数应该考虑内存支持程度,避免过多的线程数致使内存激增产生OOM。2N+1。
线程等待时间所占比例越高,须要越多线程。线程CPU时间所占比例越高,须要越少线程。
一个完整的系统不该只有一个线程池,应该对线程任务进行梳理分类,划分出各自的任务类型以及工做负载来提供多个线程池。
- 若是须要加锁竟可能使用加锁代码块将锁范围控制在最小范围中,而不是在方法体中加锁。
- 尽可能避免嵌套取锁,容易形成死锁问题。
- 合理时候使用读写锁来替换synchronized独占锁。
- JDK1.8流处理对于大量须要计算的数据时可采用parallelStream进行并发数据处理,可提升处理速度。
- 多了解各种对象对并发支持性例如HashMap、SimpleDateFormat等。
- 合理使用ThreadLocal来实现数据在线程本地化。
若是对执行过程没有严格的串行顺序能够采用FutureTask对计算结果统一取值拼装。
***缓存
杂谈篇
- 为避免循环之间切换,尽可能采用小循环嵌套大循环。
- 避免在循环中大几率抛出异常的代码块进行TryCatch,尽量放在循环外层TryCatch。
- 代码块中应尽可能使用懒加载,即须要时才进行加载,不须要则不加载。
- 在JVM级别作适当的缓存级别,可使用EhCache、Guava Cache。Ehcache适合支持持久化功能,有集群解决方案,而Guava Cache只是一个支持LRU的concurrentHashMap,没有Ehcache那么多特性,只支持增删改查,刷新规则和时效规则设定等最基本的设定。
- 设置日志输出级别,避免大量可有可无的日志输出,影响业务系统性能。
- SQL调优能够经过索引分析,减小查询字段,限制表读数据等进行分析调优。
- 数据库瓶颈能够经过读写分离减小单服务器压力,以及经过垂直拆分或水平拆分将数据表数据合理治理。
引入缓存架构减轻数据库压力。性能优化
哪些是性能瓶颈的关键点
- 有些任务须要大量的计算,须要不停地占用CPU资源,致使其余任务抢占CPU的能力变弱而致使响应速度降低,而带来的性能问题。好比任务内过多的重复计算,无限的自循环计算,JVM频繁的FULL GC,多线程作大量的上下文切换等。
- 通常来讲说内存的读写速度很是快,通常不存在性能问题,可是因为内存成本比硬盘高即内存空间是有限的,应该注意内存的范围避免应内存耗尽而致使OOM等问题。
- 磁盘IO是引发系统性能的一大因素。涉及到数据落地等问题应尽可能使用硬盘顺序读写而不是随机读写,顺序读写的读写能力比随机读写能力强大太多。
- 数据库方面除了必要的SQL优化减小查询时间外若有必要须要引入缓存层减轻数据库压力
- 合理使用锁减小并发时形成性能损耗
防止瞬间时抛出大量的异常,抛出异常会不停从堆栈内拔取异常信息,很是耗性能。服务器
衡量性能的指标
- 系统响应时间
从发送请求到接收到数据时所须要的时间
- 系统吞吐量
单位时间内成功地传送数据的数量大小
负载承受能力
随着并发量的增长最终致使系统抛出大量的异常,整个系统处于不可用的时候。多线程