软件构造知识点总结 - 5
第八章
第一节 软件构造性能的度量原理
性能度量指标
- 时间性能
- 每条指令、每个控制 结构、整个程序的执行时间
- 不同语句或控制结构执行时间的分布情况
- 时间瓶颈在哪里
- 空间性能
- 每个变量、每个复杂结构、整个程序的内存消耗
- 不同变量/数据结构的相对消耗
- 空间瓶颈在哪里
- 随时间的变化情况
内存管理
对象管理模型
三者的差异在于:如何与何时在程序对象与内存对象之间建立联系
静态
- 定义:静态内存是指在程序开始运行时由编译器分配的内存,它的分配是在程序开始编译时完成的,不占用CPU资源。
- 程序中的各种变量,在编译时系统已经为其分配了所需的内存空间,当该变量在作用域内使用完毕时,系统会自动释放所占用的内存空间;
- 不支持递归,不支持动态创建可变长的复杂数据类型;
- 在程序执行期内实体至多关联一个运行时对象
- eg: 基本类型,数组
动态-基于栈
- 栈定义:方法调用和局部变量的存储位置,保存基本类型
- 如果一个方法被调用,它的栈帧被放到调用栈的顶部
- 栈帧保存方法的状态,包括执行哪行代码以及所有局部变量的值
- 栈顶始终是当前运行方法
- 一个实体可以在运行时连续地连接到多个对象,并且运行时机制以堆栈中的后进先出顺序分配和释放这些对象
- 栈无法支持复杂数据结构
- 栈定义:方法调用和局部变量的存储位置,保存基本类型
动态-基于堆
- 堆定义:在一块内存里分为多个小块,每块包含 一个对象,或者未被占用
- 自由模式的内存管理,动态分配,可管理复杂的动态数据结构
- 代码中的一个变量可以在不同时间被关联到不同的内存对象上,无法在编译阶段确定。内存对象也可以进一步指向其他对象
Java垃圾回收机制
内存回收的三种方式
①静态模式下的内存回收:在静态内存分配模式下,无需进行内存回收:所有都是已确定的。
②在栈模式下的内存回收:按block(某个方法)整体进行
③在堆模式下的内存回收:在heap上进行内存空间回收,最复杂——无法提前预知某个object是否已经变得无用。
动态垃圾回收相关概念
- GC(Garbage Collection):识别垃圾并释放其占用的内存
- 垃圾回收器根据对象的“活性”(从root的可达性)来决定是否回收该对象的内存,”死“的对象是需要回收的垃圾
- Root
- 根集合由root对象和局部对象构成
- root对象:
Class
(不能被回收)、Thread
、Java方法/接口的本地变量或参数、全局接口引用等
- 可达/不可达对象(Reachable/Unreachable):free模式
- 从根可以直接或间接到达的对象为可达的,否则为不可达的
- 从根开始,不断将指向的对象加入活动集,剩下的是垃圾
- 活动/死亡对象(Live/dead):
- 在stack和free的结合模式下,对象的引用被视为有向图,可以从根访问的对象为活动对象,否则为死亡对象。
GC的四种算法
引用计数
- 基本思想:为每个object存储一个计数RC,当有其他 reference指向它时,RC++;当其他reference与其断开时,RC–;如 果RC==0,则回收它。
- 优点:简单、计算代价分散,“幽灵时间”短 为0
- 缺点:不全面(容易漏掉循环引用的对象)、并发支 持较弱、占用额外内存空间、等
Mark-Sweep(标记-清除)算法
- 基本思想:为每个object设定状态位(live/dead)并记录,即mark阶段;将标记为dead的对象进行清理,即sweep可阶段。
- 优点:可以处理循环调用,指针操作无开销,对象不变
- 缺点:复杂度为O(heap),高 堆的占用比高时影响性能,容易造成碎片,需要找到root
Copying(复制)算法
- 基本思想:为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
- 优势:运行高效、不易产生内存碎片
- 缺点:复制花费大量的时间,牺牲内存空间
Mark-Compact(标记-整理)算法
- 基本思想:为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
JVM中的GC
Java GC将堆分为不同的区域,各区域采用不同的GC策略,以提高GC的效率
Java内存分配和回收的机制概括的说,就是:分代分配,分代回收。
对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)
年轻代:
- 对象被创建时,内存的分配首先发生在年轻代(大对象可以直接 被创建在年老代)
- 大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消 亡的)
- 为减少GC代价,使用copying算法
- 具体过程
- 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;
- 当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);
- 此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0;
- 当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。
- 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。
年老代:
- 对象如果在年轻代存活了足够长的时间而没有被清理掉,则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。
- 使用Mark-Sweep或Mark-Compact算法;
- Minor GC和full GC独立进行,减小代价;
- 当perm generation满了之后,无法存储更多的元数据,也启动full GC。
GVM GC性能调优
- 尽可能减少GC时间,一般不超过程序执行时间的5%
- 一旦初始分配给程序的内存满了,就抛出内存溢出异常,
- 在启动程序时,可为其配置内存分配的具体大小
- 堆的大小决定着VM将会以何种频度进行GC、每次GC的时间多长。
- 这两个指标具体取值多少为“优”,需要针对特定应用进行分析。
- 较大的heap会导致较少发生GC,但每次GC时间很长
- 如果根据程序需要来设置heap大小,则需要频繁GC,但每次GC的时间较短
- 设定堆的大小的具体方法
- `Xmx/-Xms`:指定年轻代和老年代空间的初始值和最大值;`Xms`小于`Xmx`时,年轻代和老年代所消耗的空间量可以根据应用程序的需求增长或收缩;Java堆的增长不会比`Xms`大,也不会比`Xmx`小
- `XX: NewSize=<n>[g|m|k]`:年轻代空间的初始和最小尺寸,`<n>`是大小,`[g | m | k]`指示大小是否应解释为千兆字节,兆字节或千字节
- `XX: MaxNewSize=<n>[g|m|k]`:年轻代空间的最大值
- `Xmn<n>[g|m|k]`:将年轻代的初始值、最小值、最大值设为同一值
GC模式选择
- 增长或收缩年轻代或老年代的空间时需要Full GC
- Full GC可能会降低吞吐量并导致超出期望的延迟
- 串行收集器(-XX:+UseSerialGC):使用单个线程执行所有垃圾收集工作
- 并行收集器(-XX:+UseParallelGC):并行执行Minor GC,显著减少垃圾收集开销
- 并发低暂停收集器(-XX:+UseConcMarkSweepGC):收集持久代,与执行应用程序同时执行大部分收集,在收集期间会暂停一小段时间
- 增量低暂停收集器(-XX:+UseTrainGC):收集每个Minor的部分老年代,并尽量减少Major的大停顿
- -verbose:gc:打印GC信息
堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为$\frac{1}{1+n}$并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
第二节 动态程序分析方法与工具
- Jstat:获取JVM的Heap使用和GC的性能统计数据,命令如-gcutil
- Jmap:输出内存中的对象分布情况 如:jmap -clstats
- Jhat:导出heap dump,浏览/查询其中的对象分布情况
- jstack:获取Java线程的stack trace 具体用途如下:
- 定位线程出现长时间停顿的原因,如多线程间死锁、死循环、请求外部资源 导致的长时间等待等。
- 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没 有响应的线程到底在后台做什么事情,或者等待什么资源。
- Visual VM:提供了一个可视化界面,用于查看Java应用程序在JVM上运行时的详细信息,使用各种技术,包括jvmstat,JMX,Serviceability Agent(SA)和Attach API等
- MAT:内存堆导出文件的分析工具,生成饼状图等,能够对问题发生时刻的系统内存状态获取一个整体印象,找到最有可能导致内存泄露的对象,进一步查看其是否有异常行为。
Memory Dump(堆转储文件)
正如Thread Dump文件记录了当时JVM中线程运行的情况一样,Heap Dump记录了JVM中堆内存运行的情况,可使用jmap或JConsole命令生成,jhat分析。
使用 jmap 命令生成
使用JConsole生成
使用jhat分析
Stack Trace
可使用jstack查看,定位线程出现长时间停顿的原因。
第三节 代码调优的设计模式和I/O
代码调优
代码调优的概念
- 代码调优:代码调优不是为了修复bug,而是对正确的代码进行修改以提高其性能,其常常是小规模的变化
- 调优不会减少代码行数
- 不要猜原因,而应有明确的优化目标
- 不要边写程序边调优
- 不是性能优化的第一选择
- 代码行数与性能之间无必然的联系
- 代码调优建立在对程序性能的精确度量基础之上(profiling)
- 当程序做过某些调整之后,要重新profiling并重新了解需要优化的性能瓶颈,微小的变化能导致优化方向大不相同
- 性能从不是追求的第一目标,正确性比性能更重要
单例模式(Singleton Pattern)
享元模式(Flyweight Pattern)
原型模式(Prototype Pattern)
对象池模式(Object Pool Pattern)