这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
软件工程上的性能优化知识
性能优化的层面:业务代码 -> SDK -> 基础库 -> 语言运行时(runtime) -> OS
-
业务层优化:针对特定场景,具体问题具体分析,容易获得比较大的性能收益;
-
语言运行时的优化:考虑更多的场景,解决更通用的性能问题(比如内存分配问题)。
用数据来驱动优化,采用pprof等自动化性能分析工具来进行优化。
软件质量至关重要,在保证接口稳定的情况下如何实现性能优化:
- 测试用例:覆盖尽可能多的场景,用测试驱动开发;
- 文档:做了什么,没做什么,能达到怎样的效果;
- 隔离:保持行为的一致性,通过选项控制是否开启优化;
- 日志:必要的日志输出达到功能的可观测性;
自动内存管理
自动内存管理都是针对动态内存,即程序在运行时根据需求动态分配的内存。
自动内存管理(垃圾回收),由程序语言的运行时系统管理动态内存,避免程序员的手动内存管理,保证内存使用的正确性和安全性。
自动内存管理的三个任务:为新对象分配空间;找到存活对象;回收死亡对象的内存空间。
1 Garbage collector (GC)相关概念
- Mutator:业务线程,分配新对象,修改对象指向关系
- Collector:GC线程,找到存活对象,回收死亡对象的内存空间
Garbage collector (GC)算法
- Serial GC:只有一个collector回收的GC算法;
- Parallel GC:支持多个collectors同时回收的GC算法,性能比Serial GC更高;
- Concurrent GC:mutator(s) 和 collector(s) 可以同时执行的GC算法,Collectors必须感知对象指向关系的改变;
评价GC算法的标准
- 安全性:不能回收存活的对象
- 吞吐率:花在GC上的时间(越短越好)
- 暂停时间:业务是否感知(越短越好)
- 内存开销:GC元数据开销(越小越好)
2 追踪垃圾回收
当一个指针指向的对象关系不可达时,该对象就可以被回收;
追踪垃圾回收一共三个步骤:标记根对象、找到可达对象(标记)、清理不可达对象
-
标记根对象:如静态变量、全局变量、常量、线程栈等
-
找到可达对象:求指针指向关系的闭包
-
清理不可达对象:将存活对象复制到另外的内存空间,然后将死亡对象的内存标记为可分配,最后移动并且整理存活对象。
3 分代GC (Generational GC)
分代假说:很多对象在分配出来后就会很快回收,每个对象根据GC次数设置一个“年龄”(例如一个对象经历两次GC后依然存活,则该对象“年龄”为2)
分代GC目的:对年轻的对象和老年的对象制定不同的GC策略,从而降低内存管理的开销,不同的年龄对象处于heap的不同区域。
-
年轻代(Young Generation):常规的对象分配,GC吞吐率高,可以采用Copying collection,如下图所示
-
老年代(Old Generation):对象趋于一直存活,反复复制的话开销较大,可以采用make-sweep collection,如下图所示
4 引用计数
每个对象都有一个与之相关联的引用数目,当且仅当一个对象的引用数大于0时,该对象存活。
举个例子:当第一个指针p指向某个内存空间(对象)O时,该对象计数为1,当第二个指针q也指向同一对象O时,该对象计数变成2,最后指针p和指针q都指向null时,该对象O计数变成0,这时对象O就可以被回收。
引用计数的优点:
- 内存管理操作被平摊到了程序执行过程中;
- 内存管理不需要了解runtime的实现细节,例如C++的只能指针。
引用计数的缺点:
- 维护引用计数的开销大,通过原子操作保证引用计数操作的原子性和可见性;
- 无法回收环形数据结构(可以参考 weak reference);
- 每个对象都会引入额外的内存空间存储引用数目;
- 回收大数据结构的内存时依然可能引发暂停。