第八章 面向性能的构造技术
Performance Metrics 性能度量指标
Memory Performance 存储性能
– Memory Allocation: Static, Stack-based, and Heap-based
– Java Memory Model
– Garbage Collection and its Basic Algorithms: Reference Counting, MarkSweep, Mark-Compact, and Copying
– Garbage Collection and its Tuning in JVM
Algorithm Performance 算法性能
I/O Performance I/O性能
得到系统的时间
Garbage Collection
在静态内存分配模式 下,无需进行内存回收:所有都是已确定的。
在栈上进行内存空间回收:按block(某个方法)整体进行
在heap上进行内存空间回收,最复杂 ——无法提前预知某个object是否已经变得无用。
Reachable Objects vs. Unreachable Objects
对象的“活性”:可达/不可达
内存回收的首要问题是:如何把 可达对象与不可达对象分离开来?
活对象:从root可达的对象
死对象:从root不可达 Definition of Garbage Collection
Definition of Garbage Collection
垃圾回收器根据对象的“活性”(从root的可达性)来决定是否回收该对象的内存
“死”的对象 就是需要回收的“垃圾”
如果程序员在代码中人工实现GC: – 太少:导致内存泄露,很多dead objects仍占据着内存空间 – 太多:原本live的对象本回收了,导致程序执行失效
Basic Algorithms of Garbage Collection(四种GC基本算法)
Reference counting 引用计数
Mark-Sweep 标记-清除
Mark-Compact 标记-整理
Copying 复制
Reference counting(引用计数)
引用计数的基本思想:为每个object存储一个计数RC,当有其他 reference指向它时,RC++;当其他reference与其断开时,RC--;如 果RC==0,则回收它。
引用计数方法的优点:简单、计算代价分散,“幽灵时间”短=0
引用计数方法的缺点:不全面(容易漏掉循环引用的对象)、并发支 持较弱、占用额外内存空间、等。
Mark-Sweep(标记清除)
标记清除:为每个object设定状态位(live/dead)并记录,即mark阶段;将 标记为dead的对象进行清理,即sweep可阶段。
综合:循环垃圾自然收集。在指针操作上没有运行时开销。松耦合到突变者:不移动对象——不破坏任何变异体不变量——优化器友好-只需要一个引用来发现每个活对象(相当)。而不是找到每一个引用)
该算法有如下缺点:
- 标记和清除过程的效率都不高。
- 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作。
Mark-Compact(标记整理)
在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。标记—整理算法的回收情况如下所示:
copying(复制)
为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。
优点为:
- 每次只对一块内存进行回收,运行高效。
- 只需移动栈顶指针,按顺序分配内存即可,实现简单。
- 内存回收时不用考虑内存碎片的出现。
它的缺点是:可一次性分配的最大内存缩小了一半。
JVMGC模型
In the young generation 针对年轻代:只有一小部分对象可较长时间 存活,故采用copy算法减少GC代价
In the old generation 针对年老代:这里的对象有很高的幸存度,使用Mark-Sweep或Mark-Compact算法
只有当某 个区域不能再为对象分配内存时(满),才启动GC
针对young generation,使用minor GC进行垃圾收集
Minor GC所需时间较短
如果历经多 次minor GC仍存活下来,将其copy到old generation
如果old generation满了,则启动full GC
Old generation满,意味着无法进行下一次minor GC了,Minor GC和full GC独立进行,减小代价,
当perm generation满了之后,无 法存储更多的元数据,也启动full GC。
尽可能减少GC时间,一般不超过程序 执行时间的5% 一旦 初始分配给程序的内存满了,就抛出内存溢出异常
如果根据程序需要 来设置heap大小,则需要频繁GC,但每次GC的时间较短 如果根据程序需要 来设置heap大小,则需要频繁GC,但每次GC的时间较短
如果根据程序需要 来设置heap大小,则需要频繁GC,但每次GC的时间较短
初始和最大的heap尺寸:–Xms 1024M –Xmx 2048M
heap尺寸可随时间变化 heap尺寸变化时需 要full GC
old generation的尺寸不需要设 置,根据其他各参数的取值可计算得到
GC模式的选择
I/O
对于写文件:
buffer
Stream
write
读文件:
reader
scanner
stream
stream 以字符的方式读入
常见I/o策略
Dynamic Program Analysis
Dynamic Program Analysis(动态程序分析)
代码调优
代码调优不是为了修复bug,而是对正确 的代码进行修改以提高其性能。
通常是小规 模的变化
代码行数与性能之间无必然的联系
性能从不是追求的第一目标,正确性比性能更重要
代码调优的步骤
代码调优的设计模式
(1) Singleton Pattern(单例模式)
(2) Flyweight Pattern轻量模式
(3) Prototype Pattern原型模式
直接new的时空代价高,尤其 是需要与外部I/O、网络、数据库打交道时候。例如ConcreteGraph
在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能 会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。
浅拷贝:使用一个已知实例的成员变量对新创建实例的成员变量逐个 赋值。
深拷贝:类的拷贝方法不仅要复制对象的所有非引用成员变量值(简单 数据类型),还要为引用类型(对象)的成员变量创建新的实例,并且初始化为原对象的值。
在让你的ADT支持Cloneable接口的时候,千万注意自己override的 clone()正确的实现了deep copy。
(4) Object Pool Pattern 对象池模式
代价:原本可被GC的对象,现在要留在pool中,导致内存浪费—— 用空间换时间
(5) *Canonicalizing Objects 规范化
*Avoiding Garbage Collection规避垃圾回收
尽可能使用简单数据类型,对类的成员变量也是如此。
局部的简单数据类型在stack中存储,GC代价低;
类的简单类型成员变量在GC的时候代价也低,若为object成员变量则 需GC