Chapter 8: Software Construction for Performance
第八章主要讲时空性能质量指标
内存管理模型:堆、栈
GC,root、reachable、unreachable、live、dead
GC的四种基本算法
Java/JVM的内存管理模型:各区域、各区域的GC方法
JVM GC性能调优:参数配置、GC模式选择
Java性能调优工具:jstat, jmap,jhat, Visual VM, MAT
Memory dump
Stack trace
Java代码调优的设计模式:singleton, prototype/cloneable,flyweight, object pool
String constant pool
常见的Java I/O方法
第一节主要讲时空性能的指标,以及一些提高性能的方法
运行时程序性能 | Time performance(时间性能) | Space performance(空间性能) |
细节 | ①Execution time:每条指令、每个控制结构、整个程序的执行时间;②Distribution of execution time:不同语句或控制结构执行时间的分布情况;③Time battleneck(时间瓶颈) | ①Memory consumption:每个变量、每个复杂结构、整个程序的内存消耗;②Distribution of memory consumption:不同变量、数据结构的相对消耗;③Space bottleneck(空间瓶颈)④Evolution of memory consumption(内存随时间的变化) |
影响运行性能的因素 | ①Algorithm(算法);②Data structure(数据结构);③Memory allocation(内存分配);④Garbage collection(垃圾回收) | ①Basic statements(基本语句);②Algorithm(算法);③Data structure(数据结构);④I/O(file,database,network communication,etc);⑤Concurrency / multi-thread / lock(并行、多线程、锁) |
Code | 得到执行时间:long time = System.currentTimeMillis(),elapsedTime = stopTime - startTime | 得到内存消耗:Runtime runtime = Runtime.getRuntime(),runtime.gc(),long memory = runtime.totalMemory() - runtime.freeMemory() |
主要介绍三种基本的内存分配模型
差异在于:如何与何时在程序对象与内存对象之间建立联系。
Memory Allocation | Static allocation(静态分配) | Dynamic allocation(动态分配) | |
Define | 静态内存分配,在编译阶段就已经确定好了内存分配 | 在运行时动态分配内存,建立新的内存对象(基于堆和栈的内存管理都是动态分配) | |
Three Modes | Static | Stack-based | Heap-based |
Details | 在将程序load进内存的时候或开始执行的时候,确定所有对象的分配;不支持递归,不支持动态创建的可变长的复杂数据类型 | 栈存储方法调用以及方法执行中的局部数据;后进先出,无法支持复杂数据类型 | 在一块内存里分为多个小块,每块包含一个对象,或者未被占用自;代码中的一个变量可以在不同时间被关联到不同的内存对象上,无法在编译阶段确定,同时内存对象也可以进一步指向其他对象;自由模式的内存管理,动态分配,可管理复杂的动态数据结构 |
About | 某些对象延续的时间比创建它的方法所延续的时间更长(因为方法返回后,栈清空,所以stack不行);递归的数据结构,长度可变的数据结构(所以静态方法和stack都不行);经常要使用不限定长度的数据结构(用堆进行管理) | ||
Space reclamation(内存回收) | 在静态内存分配模式下,无需进行内存回收:所有都是已确定的 | 在栈上进行内存空间回收:按block(某个方法)整体进行 | 在heap上进行内存空间回收,最复杂,无法提前预知某个object是否已经变得无用(在GC中将详情讲解) |
图解基于栈的分配模型
(注意方法参数时通过栈传递的)
图解基于堆的分配模型
主要介绍java中的内存分配模型,java通过JVM(java虚拟机)进行基于堆和栈的管理内存
Thread stack | Heap |
每个运行在JVM中的线程都有自己的线程栈,管理其局部数据,各栈之间彼此不可见;所有局部的基本数据类型都在栈上创建;多线程之间传递数据,是通过赋值而非引用 | 所有的对象(Object,eg. Byte,Integer,Long)都在堆上创建;即使是局部变量的object,也是在堆上创建;堆上创建的对象可被所有线程共享引用;如果两个线程调用同一个对象上的某个方法,他们分别保留该方法的局部变量的拷贝 |
①基本数据类型的局部变量;②指向一个对象的局部变量;③一个对象可能包含方法,这些方法包含局部变量,这些局部变量存储在线程栈上; | ①所有对象(object)存储在堆上;②对象的基本类型成员变量也存储在堆上;③如果一个成员变量是指向一个对象的指针,这个成员变量存储在堆上;④静态类变量存储在堆上 |
考点:
GC,root、reachable、unreachable、live、dead
GC的四种基本算法
对象之间的引用关系构成一个有向图:
- 图的活动对象是可从根访问的对象。
- 执行计算的进程称为改变器(mutator),因为它被视为动态地改变对象图。
root(根对象) | reachable(可达对象) | unreachable(不可达对象) | live(活对象) | dead(死对象) |
在系统执行期间的任何时刻,根的集合由以下对象组成:①系统的根对象 ②附加到本地实体的任何对象或当前正在执行的例程的正式参数(包括函数的本地实体结果) | 根据对象的”活性”有无区分;根对象的所有孩子节点,直接或间接都是可到达的,其他任何对象都是不可达的 | 从root可达 | 从root不可达(需要注意的是,死对象可能存在其他对象的引用) | |
内存回收的首要问题是将可达对象与不可达对象分离开来 |
垃圾回收器根据对象的”活性“(从root的可达性)来决定是否回收该对象的内存,”死“的对象就是需要回收的”垃圾”。
垃圾回收GC:识别”垃圾“对象,把其占用的内存加以回收。
分为手动回收和自动回收两种方式。
手动回收有以下几种方法:
①Defensive Programming(防御式编程):复制对象,而非引用;在局部回收内存;浪费空间但是有用;
②Pairing Principle(配对原则):使用new()
创建,对应的使用delete()
删除;
③Ownership Concept(所有权概念);
④Monitoring Technique(检测技术):使用Malloc()
和Free()
;
⑤Administrator technique (管理员技术)。
下面介绍自动回收GC方法:
类别 | Reference Counting(引用计数) | MarkSweep(标记-清除) | Mark-Compact(标记-整理,介绍较少) | Copying(复制) |
基本思想 | 为每个object存储一个计数RC,当有其他reference指向它时,RC++;当有其他reference与其断开时,RC–;如果RC==0,则回收它 | 为每个object设定状态位(live/dead)并记录,即mark阶段;将标记为dead的对象进行清理,即sweep阶段 | 首先标记每个object(使用mark-sweep的mark阶段),然后将所有标记为live的移到后面,最后将前面所有dead对象回收 | 首先将堆分为Fromspace和Tospace两部分,然后在Tospace中为对象分配内存,当Tospace满时,将其中所有活对象复制到Fromspace中,回收Tospace中所有死对象,周期进行 |
注意点 | 递归释放:一旦对象的RC = 0,它就可以被释放。 但对象可能包含对其他对象的引用,在这个对象被释放之前,其引用的对象的RC应该改变。 | 周期性进行mark,sweep,其中mark位置为对象首位(称为mark-bit)即可 | 类似mark-sweep,多了整理操作,让之后堆使用更方便 | 该GC策略与mark-compact的区别在于:不是在同一个区域内进行整 理,而是将live对象全部复制到另一个区域。 |
优点 | 简单、计算代价分散,“幽灵时间”短->0(一旦没有了引用,立即回收) | 全面:循环收集垃圾;在指针操作上没有运行时间开销;与mutator松耦合;不移动对象, 不会破坏任何mutator不变量,优化器友好,只需要引用每个要发现的活对象(而不需要找到每个引用) | … | 免费压缩;所有对象大小的分配非常简单 ,空间检查是指针比较 ,只需增加空闲指针分配;只处理活对象(通常是堆的一小部分);固定的空间开销 - 释放和扫描指针 - 转发地址可以写在用户数据上;全面:循环收集垃圾;简单实现合理高效的复制GC |
缺点 | 不全面(容易漏掉循环引用的数据)、要持续不断的计算(代价高)、难以支持并发、等 | 停止/开始会导致的停顿和漫长的僵尸时间(开始要遍历所有对象);复杂性是O(heap)而不是O(live) ,在标记阶段访问每个活对象 ,所有活和死对象都在sweep阶段被访问;如果堆已满(经常发生),则需要Mark阶段需要大量标记;碎片和标记堆栈溢出是问题;跟踪收集器必须能够找到根对象(与引用计数不同) | … | 停止和复制开销较大;对象的位置周期性改变;需要其他简单收集器两倍的地址空间;复制大型对象的成本;长期数据可能被重复复制;必须更新所有引用;移动对象可能会破坏mutator不变量;广度优先复制可能会干扰局部模式 |
考点:Java/JVM的内存管理模型:各区域、各区域的GC方法
Java GC将堆分为不同的区域,各区域采用不同的GC策略,以提高GC的效率
针对不同的区域,使用不同的GC策略
Young Generation(年轻代) | Old Generation(年老代) | Pernament Generation(永久代) | ||
Eden | S0(From) | S1(To) | Old | Perm |
java分配的对象在eden创建 | 对象存活空间,YGC后从Eden转移到该处 | minorGC从Young转移到该处 | 保存VM和Java元数据比如Strings和类静态变量 | |
只有一小部分对象可较长时间存活,故采用copy算法减少GC代价,具体就是在S0和S1之间相互拷贝;当没有足够空间时,使用minor GC进行垃圾收集;如果经过多次minor GC仍存活,将其copy到old generation | 这里的对象有很高的幸存度,使用Mark-Sweep或Mark-Compact算法;当没有足够空间时(Old generation满,意味无法进行下一次minor GC),启动full GC | 当perm generation满了之后,无法存储更多的元数据,也启动full GC |
minor GC和f**ull GC**是独立进行的,减少代价;
minor GC仅发生在young generation;
只有当某个区域不能再为对象分配内存时(满),才启动GC。
考点:JVM GC性能调优:参数配置、GC模式选择
调整JVM的垃圾回收的特点:
①尽可能减少GC时间,一般不超过**程序执行时间的**5%
②一旦初始分配给程序的内存满了,就抛出内存溢出异常(OutOfMemoryError)
③在启动程序时,可为其配置内存分配*的具体大小*
调整JVM的垃圾回收的方法
①确定堆的大小
堆的大小决定着VM将会以何种频度进行GC、每次GC的时间多长;这两个指标具体取值多少为“优”,需要针对特定应用进行分析; 较大的heap会导致较少发生GC,但每次GC**时间很长;如果根据程序需要来设置内存需要的heap大小,则需要频繁GC**,但每次GC的时间较短。
初始和最大的heap尺寸
Java -Xms 1024M -Xmx 2048M
heap尺寸可随时间变化,heap尺寸变化时需要full GC
具体设置
②选择GC模式
③ 使用verbose GC查看详细信息以确定堆的大小
eclipse->Run->Run Configuration->Arguments->VM arguments
-server -Xms1024m -Xmx1024m -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
-verbose:gc
是必须写的,-Xms1024m
为heap**初始值,-Xmx1024m
为heap最大值,-XX:+PrintGCDetails
打印GC具体信息,-Xloggc:gc.log
将GC信息保存到日志**gc.log中
得到的数据中如下,其中Allocated Failed即为minor GC;Full GC时间应小于3-5s
④自动记录内存将要不足的情况
⑤手工请求GC
⑥请求线程栈
详情:《算法导论》
I/O of software system
Java I/O APIs
Java nio APIs
详情参考:javadoc
本节,介绍如何利用工具对运行的程序性能进行动态监控,以发现性能瓶颈,并试图加以消除。
考点:
Java性能调优工具:jstat, jmap,jhat, Visual VM, MAT
Memory dump
Stack trace
命令行分析工具 | JConsole | Visual VM | Memory Analyzer(MAT) | |||
jhat | jmap | jstat | jstack | JConsole | Visual VM | MAT |
导出heap dump,浏览、查询其中的对象分布情况 | 输出内存中的对象分布情况 | 获取JVM的heap使用和GC的性能统计数据 | 获取java数据的stack trace | |||
获取heap dump的方法:在运行时使用jmap -dump,jconsole,hprof工具;jhat filenam在http://127.0.0.1:7000浏览和查询(使用OQL语言)heap dump | 使用jmap -dump:format=b,file=filename导出heap dump;jamp -heap得到heap的GC信息、堆配置、堆消耗总结;jmap -permstat得到permanent generation的数据分析 | 使用jstat -gcutil输出heap个区域的占用情况 | 使用jsp得到程序的pid,然后使用jstack得到程序运行时栈情况(– 定位线程出现长时间停顿的原因,如多线程间死锁、死循环、请求外部资源导致的长时间等待等。) | GUI monitoring tool | 用法较多 | 同样用来分析heap dump |
考点
Java代码调优的设计模式:singleton, prototype/cloneable,flyweight, object pool(下节分析)
String constant pool(字符串常量池)
常见的Java I/O方法