本文整理自:第五届字节跳动青年训练营 后端组
什么是性能优化
提高软件系统处理能力,减少不必要消耗,充分利用计算机算力
- 业务层优化
- 针对特定场景和具体问题
- 容易获得较大收益
- 语言运行时优化
- 面向全公司的优化,非特定场景
- 解决更通用的性能问题
- 考虑更多场景
在优化时需要根据数据驱动,比如使用自动化性能分析工具–pprof。依靠数据优化而非主观猜测,并且首先优化最大的瓶颈
性能优化时需要注意:
- 软件质量至关重要
- 保证接口稳定的情况下改进具体实现
- 测试驱动:测试用例覆盖尽可能多的场景,方便回归
- 文档:做了什么,没错什么,能达到什么效果
- 隔离:通过选型控制是否开启优化
- 可观测:必要的日志输出
一、自动内存管理
自动内存管理又称垃圾回收,由程序语言的运行时系统管理动态内存
- 避免手动内存管理,专注于实现业务逻辑,不需要像C一样使用malloc和free函数管理内存
- 保证内存使用的正确性和安全性,解决了double-free problem和use-after-free problem问题
自动内存管理的三个任务是:
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象内存空间
1.1 相关概念
- Mutator:业务线程,分配新对象,修改对象指向关系
- Collector: GC线程,找到存活对象,回收死亡对象空间
有以下分配算法
Serial GC算法:只有一个collector,mutator执行到一定位置需gc时,collector会中断mutator,转为执行collect的gc流程
Parallel GC算法:支持多个collectors同时回收的GC算法,也需要中断mutator
Concurrent GC:多个mutator和多个collector可以同时执行,也就是mutator不需要被中断。
- 评价GC算法
- 安全性:不能回收存活操作
- 吞吐率:花在GC上的时间
- 暂停时间:业务是否感知
- 内存开销:GC元数据开销
垃圾回收主要是用两种方法
- 追踪垃圾回收
- 引用计数
1.2 追踪垃圾回收
对象被回收的条件:指针指向关系不可达的对象,分三步:
-
标记根对象
- 将静态变量、全局变量、常量、线程栈等标记为存活
-
标记可达对象
- 求指针指向关系的传递闭包:从根对象出发找到可达对象
-
清理不可达对象
清理不可达对象有三种方法:- 将存活对象复制到另外的内存空间,然后将原来的内存空间全部回收(copying gc)
- 将死亡对象的内存标记为可分配,下次需要分配新空间时直接使用这些空间(mark-sweep gc)
- 移动并且整理存活对象,比如位于1、3、4、7的内存地址的对象是存活的,那么将他们移动到0、1、2、3,下次分配直接从4开始(mark-compact gc)
我们需要根据对象的生命周期,使用不同的标记和清理策略,首先,我们需要学习分代GC的相关内容
分代GC
分代GC引申自分代假说,分代假说指出:most objects die young。也就是很多对象分配出来后很快就不再使用了。每个对象的年龄指的是经历GC的次数,每一次GC都会将已经死亡的对象回收,如果一个对象经历了许多次GC都没有被回收,也就证明它是频繁被使用的。
年轻代:
- 常规对象分配,一般分配后很快就死亡
- 由于存活对象很少,可以采用copy collection
- gc吞吐率高
老年代
- 对象倾向于一直活着,反复复制开销大
- 可以采用mark-sweep collection
于是,在设计GC的时候,会倾向于针对年轻的和老年的对象制定不同的GC策略,从而降低整体的内存开销。不同年龄的对象处于heap的不同区域
1.3 引用计数
每个对象都有一个与之关联的引用数目,对象只有当引用数大于0的时候是存活的。也就对象没有被引用的时候就会死亡。
优点是内存管理的操作被平摊在了程序执行过程中,在程序执行时就完成了回收,而不需要等gc线程来回收。内存管理也不需要了解runtime的实现细节
缺点:
- 维护引用计数开销较大:通过原子操作保证对引用计数操作的原子性和可见性
- 无法回收环形数据结构
- 内存开销大:每个对象都需要引入额外内存空间存储引用计数
- 回收内存时依旧可能引发暂停