笔记2010/06/24-持续更新
算法视角
设计合理的程序算法
- 时间复杂度对性能的影响
- 空间复杂度对性能的影响
-
选择合适的数据结构
日常以数据结构的需求居多
- 数据结构初始容量对性能影响
- 数据结构的扩容对性能的影响
- 数据结构的收缩对性能的影响
- 数据结构随机访问对性能影响
- 数据结构顺序访问对性能影响
- 数据结构移动时对性能的影响
- 数据结构复制时对性能的影响
- 数据结构负载因子对性能影响
- 哈希函数的设计对性能的影响
- 数据结构在并发时性能的影响
- 数据结构内存连续性对性能的影响
语言视角
原生优化
- 优先使用System.arraycopy
- 优先使用Java的 native 方法
循环优化
- 优先使用支持随机访问index的循环遍历
- foreach反编译后的代码可能等价Iterator
- Iterator反编译后可能会转化成while风格
编译优化
- 字符串字面常量相加的优化
- 编译器可能会对字符串优化
- 运行时的对象逃逸分析优化
- StringBuffer的同步消除化
字符串类
- 定位字符串-indexOf
- 截断字符串-substring
- 为可变字符串设置初始容量
- 优先使用StringTokenizer分割字符串
编程经验
- 避免用抛出异常来控制流程
- 合理的使用位运算进行计算
- 合理调整if的分支的优选级
- 合理的利用if条件短路效应
- 合理的调整嵌套循环的顺序
- 合理的栈分配来代替堆分配
- 合理的合并或重用计算逻辑
- 合理使用静方法代替实例方法
- 避免在一个长循环中创建对象
- 极端情况下合理的展开循环
- 极端情况下手动做逃逸分析
- 极端情况下手动的内联函数
对象管理
对象池优化
- 对象池是为了避免创建和销毁重对象的开销
- 对象池往往伴随着池对象的竞争访问与归还
- 轻量级对象的创建速度往往都会非常有效率
- 对象池对于轻量级对象的获取未必性能良好
管理重对象
I/O的源和目标
- 文件-File
- 管道-Pipes
- 网络-Socket
- 内存-Memory
- 系统输入/输出/错误流
I/O的处理过程
- I/O读写
- I/O过滤
- I/O缓冲
- I/O解析
I/O的数据类型
- 原始类型读写
- 文本类型读写
- 对象类型读写
I/O技术的选择
- 面向流还是面向缓冲区
- 面向同步还是面向异步
- 面向阻塞还是面向回调
缓冲区性能优化
- 缓冲区可以减少用户态调用内核态本地或网络I/O的读写次数
- 缓冲区可用于限制访问流量或合并批量的发送缓冲区中的请求
- 缓冲区可以用于协调上游高速程序和下游低速程序之间的性能差
- 缓冲区过小对I/O性能提升不明显
- 缓冲区过大占用内存和GC的负载
- 可重用缓冲区对象避免对象分配
-
直接内存的特性
直接内存的创建成本高于堆内内存的创建
- 直接内存的读写性能高于堆内内存的读写
- 直接内存对垃圾收集的影响小于堆内内存
BIO
- 基于字节的输入&输出流
- 基于字符的输入&输出流
- 合理的使用缓冲区装饰器
NIO
- NIO核心组件
- Channels
- Buffers
- Selector
- 直接内存分配
- ByteBuffer.allocateDirect分配堆外的内存
- 直接内存映射
- MappedByteBuffer有利于对大文件的读写
- 合理设计结构化数据
- Gather&Scatter
其他
单例模式
静态代理模式
- 基于静态代理模式的对象延迟创建与访问
- 基于静态代理模式常常用于对象池的实现
动态代理模式
- 动态代理性能损耗
- 动态代理类实例的创建性能
- 动态代理类的函数调用性能
- 动态代理常用框架
- JDK Proxy
- CGLIB
- Javassist
- Byte Buddy
享元模式
- 基于享元模式避免重复创建可重用的对象
- 享元模式与对象池的差异
- 对象池中的对象是等价的
- 享元对象则不能相互替代
- 享元模式与对象池的举例
- 例如享元的字体类
- 例如数据库连接池
原型模式
- 基于原型模式避免较慢的构造函数创建对象
缓存视角
缓冲区的作用
- 缓存区常用高速易失性存储的访问代替低速非易失性存储的访问
- 缓存区常常用来缓存已经获得或者已经计算好的可以重用的数据
缓存淘汰算法
- 基于队列机制的FIFO/FILO
- 基于访问时间的LRU/MRU
- 基于访问频度的LFU
- 基于访问时间与频度的LFRU=LFU+LRU
- 基于随机替换策略的RR
基于缓存的位置
- 客户端的缓存
- 内容分发网络
- 服务器端缓存
- 堆内部的缓存
- 堆外部的缓存
缓存相关组件
- 缓存设计规范JSR107
易失性缓存
理解数据库原理
- 存储管理
- 索引结构
- 数据结构
- 查询执行
- 查询编译器
- 系统日志
- 并发控制
- 事务管理
- 分布式数据库
数据库的设计阶段
- 合理的字符集的类型设置
- 合理的数据业务模型设计
- 合理设计数据库的表名字
- 合理设计数据表字段名字
- 合理的数据字段类型选择
- 合理的数据字段长度选择
- 固定长度字符选定长类型
- 避免设计较宽的数据库表
- 避免用数据库内置关键字
- 约定每张表都应存在主键
- 用自增主键或无符号整数
- 合理的数据库的索引设计
- 避免分离率低的字段做索引
- 避免更新频繁的字段做索引
- 组合索引应遵循左前缀法则
- 组合索引等值比较优先靠左
- 组合索引区分度高的字段靠左
- 组合索引最好能做到索引覆盖
- 排序字段和索引字段顺序一致
- 数据库表的关联字段添加索引
- 应避免创建冗余的数据库索引
- 索引失效的一些常见场景
- 对索引列使用函数会导致索引失效
- 对索引列进行计算会导致索引失效
- 对索引列类型转换会导致索引失效
- 对索引列左模糊匹配导致索引失效
- 组合索引违反左前缀法则导致索引失效
- 组合索引存在or操作会退化成单键索引
- 对索引列!=,<>操作会导致全索引扫描
- order by不走索引列时导致文件排序
- group by不走索引列时导致文件排序
- 当数据量较少时可能会采用全表扫描
数据库的开发阶段
- 使用数据库连接池的驱动
- 使用预编译的Statement
- 显示的指定返回的字段集
- 显示指定插入的字段名称
- 合理的减少连接表的数量
- 合理的进行批量插入数据
- 合理的使用文本装载数据
- 避免字段出现NULL悬空
- 避免使用GUID作为主键
- 避免重复查询相同的数据
- 避免隐式的数据类型转换
- 避免返回大量数据的查询
- 避免不带条件的 Where
- 使用Union all代替Union
- 维护好清晰的数据库文档
- 使用乐观锁来代替悲观锁
- 使用连接查询替代子查询
- 子查询会产生临时表
- 优先优化调用频繁的查询
- 基于业务的数据库微服务
- 通常应该要避免全表扫描
- 全表扫描有时候未必不好
- 连续数据的读取全表扫描避免了随机读
- 索引访问有时候未必就好
- 大量的离散数据的回表操作造成随机读
执行计划分析阶段
- Type
- const
- eq_ref
- ref
- range
- Full Index Scan
- Full Table Scan
- Extra
- Using index 索引覆盖
- Using where, Using index
- 索引过滤数据
- 索引返回数据
- Using index condition 索引并回表
- Using temporary 临时表
- Using where 全表扫描
- Using filesort 文件排序
数据库的运维阶段
- 尽可能大的内存缓存索引
- 数据库的实时监控和预警
- CPU使用率
- 计算函数的压力
- IOPS
- 物理磁盘的压力
- QPS/TPS
- 业务访问量的压力
- 会话数
- 数据库连接池的压力
- CPU使用率
- DLL数据库操作导致锁表
- 添加索引
- 删除索引
- 改变索引
- 数据库死锁与数据库阻塞
- 数据库阻塞
- 数据库阻塞不一定是死锁
- 最后进入死锁的事务会自动回滚
- 数据库死锁
- 数据库的死锁会造成阻塞
- 发生阻塞的事务则需要等待超时
- 数据库阻塞
- 避免大或长的数据库事务
- 问题
- 导致事务发生死锁
- 导致从库延迟回放
- Undo列表会增长
- 解决
- 基于对复杂业务的拆分
- 基于对复杂DDL的拆分
- 问题
- 如何删除表中的大量数据
- 问题
- 大量数据的删除会导致长事务
- 会导致Undo&Binlog的增长
- Optimize Table回收数据碎片
- 解决
- 基于Truncate做全表删除
- 大量删除数据先删除索引
- 分批次的逐步删除大数据
- 问题
- 如何定位数据库相关问题
- 命令
- explain sql
- show variables like ‘optimizer_switch’
- 系统表
- sys_config
- performance_schema
- information_schema
- Information_schema.innodb_trx - 查看事务
- information_schema.innodb_locks - 查看事务死锁
- information_schema.innodb_lock_waits - 查看事务等待
- information_schema.processlist - 查看运行中的SQL语句
- 参数
- 避免采用两趟扫描
- max_length_for_sort_data
- 避免产生文件排序
- sort_buffer_size
- 避免采用两趟扫描
- 命令
数据集的外部优化
- 基于缓存对数据库的减压
- 缓存对热点数据的预加载
数据库反范式设计
- 合理放弃数据库存储过程
- 数据库功能上浮到应用层
- 合理的添加数据冗余字段
- 合理的舍弃数据库的约束
- 合理的减少数据库的计算
- 合理的舍弃数据库的外键
- 合理的使用多态关联设计
- 合理的纵向拆分不常用列
- 合理的纵向拆分较大的列
- 合理的水平拆分大的表格
- 合理使用整数存储浮点数
- 合理创建小的热点数据表
- 合理的拆分大的复杂查询
- 合理的数据翻页的设计
- 基于limite翻页
- 基于nextToken
- 基于id回表机制
- 合理的树形结构的设计
- 父子关系结构
- 路径前缀结构
- 闭包关系结构
- 合理的选择表继承方案
- 单表继承
- 实体表继承
- 类表继承
- 半结构化数据
非关系数据库选择
- 合理的引入键值型数据库弥补关系型数据库的不足
- 合理的引入文档型数据库弥补关系型数据库的不足
- 合理的引入列族型数据库弥补关系型数据库的不足
- 合理的引入图结构数据库弥补关系型数据库的不足
- 合理的引入全文搜索系统弥补关系型数据库的不足
并发视角
基于阿姆达定律分析
- 提升 CPU 核数
- 降低串行化比重
多线程的程序的开销
- 创建线程需要更多内存空间
- 更多的线程导致上下文切换
- 共享资源的共享互斥的开销
- 竞争激烈时CAS的CUP开销
- 多线编程可能带来的复杂性
多线程造成死锁问题
- 死锁的解决
- 合并多个共享资源的访问
- 约束共享资源的访问顺序
- 死锁的发现
- 基于超时的的死锁检测
- 基于等待图的死锁检测
并发设计模式的优化
- 基于Master-Worker模式的优化
- 基于生产者和消费者模式的优化
- 基于ThreadLocal简化共享互斥
- 基于Future模式优化
- 同步阻塞等待
- 异步回调通知
- 基于对象状态不可变模式的优化
- java.lang.String
- java.lang.Boolean等
线程管理的性能优化
- 基于可控制线程数量的性能优化
- 如线程池
并发算法的性能优化
- 基于非阻塞算法的性能优化技术
- 例如CAS
- 基于非阻塞容器的性能优化技术
- 例如JCU
多线程锁的性能优化
- 基于缩小同步代码块范围的优化
- 例如synchronized 的访问
- 基于对共享资源分片的性能优化
- 例如ConcurrentHashMap
- 基于对资源读写竞争的性能优化
- 例如读写锁
- 基于对资源读写并发的性能优化
- 例如COW
- 基于对竞争条件分离的性能优化
- 例如Condition
- 基于对资源首尾访问的性能优化
- 如双端队列
- 基于对资源范围分级的性能优化
- 例如意向锁
- 基于乐观的竞争冲突的性能优化
- 例如乐观锁
- 基于对锁的逐步升级的性能优化
- 例如偏向锁,自旋锁,重量级锁
- 基于JVM逃逸分析的锁消除优化
基于单个进程的优化
- 权衡上下文切换开销是否高于请求执行本身
- 基于多核CPU的特性创建多个单进程的优化
基于线程框架的优化
- JCU
- Amino
- disruptor
- Kilim协程框架
基于协程的性能优化
JVM视角
JVM内存设计的目的
- 分代(新生代和年老代)是为了对不同生命周期的对象分而治之的优化
- 分区(Eden区和幸存区)是为对位于不同区域的对象分而治之的优化
- region 将内存分为更小的颗粒度进行管理
JVM调优的基本思路
1. 确定吞吐量优先还是响应优先
2. 选择合适的堆大小
堆内存
- 新生代-Eden
- 小的新生代有利于提高响应性能,但以GC频繁震荡和牺牲吞吐量为代价
- 大的新生代有利于最大化吞吐量,但以占用空间和牺牲暂停时间为代价
- 新生代-Survivor0
- 新生代中的幸存区太小,对象容易直接溢出到老年代
- 新生代-Survivor1
- 新生代中的幸存区太大,相当于浪费了另一个幸存区
- 年老代
- 堆的大小是固定的,增加年轻代会导致减少老年代,将增加Full GC的频率
- 保持老年代足够大,以容纳应用程序在任何给定时间内需要使用的实时数据
堆边界
线程栈
- 永生区或元数据区(常量池,方法信息,类型信息)
- 元数据
- 直接缓冲区
- 代码缓存区
3. 选择合适的GC类型
串行收集器
- 如果应用程序的数据集很小(最多大约 100 MB),则选择串行收集器-XX:+UseSerialGC。
- 如果应用程序将在单个处理器上运行并且没有暂停时间要求,则选择串行收集器-XX:+UseSerialGC
并行收集器
- 如果应用程序吞吐量是第一优先级并且没有暂停时间要求或可以接受一秒或更长的暂停,则选择并行收集器-XX:+UseParallelGC
- 基于收集线程的调优
- 基于硬件线程数调整并行的GC线程数量 XX:ParallelGCThreads
- 基于并发目标行为的性能期待
- 最大暂停时间-XX:MaxGCPauseMillis
- 吞吐量的大小-XX:GCTimeRatio
- 开启自动驾驶-XX:+UseAdaptiveSizePolicy
- 为满足暂停时间行为的堆收缩
- 堆的收缩速度因子-XX:AdaptiveSizeDecrementScaleFactor=
- 堆的收缩速度因子-XX:AdaptiveSizeDecrementScaleFactor=
- 为满足吞吐量行为堆扩容调整
- 新生代的堆的扩容 -XX:YoungGenerationSizeIncrement=
- 年老代的堆的扩容-XX:TenuredGenerationSizeIncrement=
- 新生代的堆的扩容 -XX:YoungGenerationSizeIncrement=
并发收集器
- 如果响应时间比总吞吐量更重要,并且垃圾收集暂停必须保持在大约一秒以内,则选择G1收集器-XX:+UseG1GC或CMS-XX:+UseConcMarkSweepGC
- 垃圾收集优先的G1,适用于具有大量内存的多处理器机器
- CPU核数与并发收集关系
- 并发收集器的并发收集线程数
- 1 <= K <= 上限{N/4 }
- 单核服务器对并发收集没益处,并发阶段至少需一个处理器收集垃圾
- 并发收集器的并发收集线程数
- 并发模式可能失败和退化
- 如果 CMS 收集器无法在老年代填满之前完成回收不可达对象,或者如果老年代中可用的空闲空间块无法满足分配。收集将在所有应用程序线程停止的情况下完成并退化成年老代的串行收集。无法同时完成收集被称为并发模式失败。
- 并发收集内存溢出的判断
- 如果总时间的 98% 以上花费在垃圾收集上,而堆回收不到 2%,则内存不足错误被抛出。可以通过将选项添加-XX:-UseGCOverheadLimit到命令行来禁用此功能
- 垃圾收集时间两种计算法
- 与应用程序并发运行时的GC时间
- 应用程序停止时候的GC运行时间
- 老年代的内存占用率与GC
- 老年代的占用率超过初始占用率(老年代的百分比),并发收集也会开始
- -XX:CMSInitiatingOccupancyFraction=92
4. 设置合适的内存空闲率
- 堆的空闲率不能低于某个比例,即堆的使用率高,需要扩展堆的大小
- -XX:MinHeapFreeRatio
- 堆的空闲率不能高于某个比例,即堆的使用率低,需要收缩堆的大小
- -XX:MaxHeapFreeRatio
5. 选择性能更好多核CPU
- 随着处理器数量的增加,增加新生代的大小,因为分配可以并行化
- 多核CPU时可以选择指定并行的GC线程数-XX:ParallelGCThreads
垃圾收集器的类型
- 新生代(复制算法)
- Serial(单线程&STW)
- Parallel Scavenge(多线程&STW&吞吐量优先的GC)
- ParNew(多线程&STW)
- 年老代(标记清除)
- Serial Old(单线程&STW)
- Parallel Old(多线程&STW)
- Concurrent Mark Sweep
- G1
- -XX:+UseG1GC
GC对内存排列的影响
- 串行收集器中新生代的剩余空间和年老代的剩余空间不相邻
- 并行收集器中新生代的剩余空间和年老代的剩余空间则相邻
垃圾收集与引用类型
- 强引用
- 它真的很强直到内存溢出
- 软引用
- 合理的利用内存敏感特性
- 弱引用
- 合理的利用 GC 敏感特性
- 配合WeakHashMap使用
- 虚引用
- 合理的利用 GC 敏感特性
- 合理的跟踪被释放的对象
- finalize
- finalize可能让对象复活
常用JVM虚拟机的参数
GC信息打印
- 打印GC的基本或细节信息
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- 打印对象晋升失败的信息
- -XX:+PrintPromotionFailure
- 打印GC产生原因的信息
- -XX:+PrintGCCause
GC日志转储
- 日志文件的位置
- -Xloggc:
- -Xloggc:
- 日志文件的大小
- -XX:GCLogFileSize=512m
- 日志文件的个数
- -XX:NumberOfGCLogFiles=5
堆内存配置
- 绝对大小配置
- 设堆的初始尺寸和最大尺寸(新生代+年老代)
- -Xms & -Xmx
- 新生代初始尺寸和最大尺寸(Eden+2 Survivors)
- -XX:NewSize & -XX:MaxNewSize
- 设堆的初始尺寸和最大尺寸(新生代+年老代)
- 比例大小配置
- 堆中新生代与年老代的比例
- -XX:NewRatio
- 单个幸存区与伊甸区的比例
- -XX:SurvivorRatio
- 堆中新生代与年老代的比例
非堆的配置
- 设置NIO非堆内存大小
- -XX:MaxDirectMemorySize=2g
- 设置线程栈的内存大小避免栈溢出
- -XX:ThreadStackSize=256
- 设置元数据区域初始尺寸和最大尺寸
- -XX:MetaspaceSize=512m
- -XX:MaxMetaspaceSize=1g
- 设置代码缓存的初始尺寸和最大尺寸
- -XX:InitialCodeCacheSize=256m
- -XX:ReservedCodeCacheSize=512m
新生代的晋升
- 基于对象 MinorGC 频度控制进入年老代
- -XX:InitialTenuringThreshold=8
- -XX:MaxTenuringThreshold=15
- 基于对象大小的阈值控制对象进入年老代
- -XX:PretenureSizeThreshold=2m
- 新生代对象总是立即晋升进入年老代(失去分代的意义)
- -XX:+AlwaysTenure
- 新生代对象直到新生代内存耗尽才晋升(阈值失去意义)
- -XX:+NeverTenure
GC并行或并发
- STW阶段的GC线程数
- -XX:ParallelGCThreads=16
并发阶段的GC线程数
最大文件句柄数
- 虚拟内存的大小
- 系统磁盘块大小
- 硬件视角
- 网络吞吐量水平或垂直扩容(网络 I/O 密集型应用)
- 物理磁盘的水平或垂直扩容(磁盘 I/O 密集型应用)
- CPU性能的水平或垂直扩容(CPU计算密集型应用)
- 物理内存的水平或垂直扩容(内存访问密集型应用)
架构视角
Web服务器的性能优化
- 合理的使用负载均衡
- 随机算法
- Hash算法
- 轮询算法
- 最少连接
- 最小延迟
- 混合算法
应用服务器的性能优化
- 水平或垂直扩容服务器
数据库I/O的性能优化
- 合理的使用分布式缓存
- 数据库的分表分库
- 合理的纵向拆表
- 合理的横向拆表
- 合理的分库策略
基于异步消息的性能优化
- 基于消息中间件的业务解耦
- 基于消息中间件的削峰平谷