【软件构造】课件精译(十九) 面向性能的软件构造技术
一、性能指标
性能
运行时程序性能:主要分为时间性能和空间性能
影响运行时性能的因素
空间性能:内存分配、垃圾回收
时间性能:算法、数据结构、I/O、网络、并发与多线程等
如何获得内存消耗
如何获得执行时间
二、内存分配
回忆:操作系统中的内存处理
OS通过虚拟内存机制管理物理内存的使用,将物理内存映射为虚拟内存,为每个进程分配虚拟内存空间。
内存管理:操作系统层面、应用程序层面
对象模型(主要讲了Java数据存储与内存分配方式)
三种内存管理的模式:静态内存分配、基于栈的模式、基于堆的模式
(后两者属于动态分配模式)
Entity是指属性、参数、局部变量和结果等在代码中的名字,其值为对象或对对象的引用。
静态模式:在程序执行期内entity至多attach一个运行时对象。
该技术在程序加载时或开始时一劳永逸地为所有对象分配空间(并将它们附加到相应的实体)
栈:主要存储方法调用和本地变量
一个entity在运行时可以先后attach多个对象,运行时机制以堆栈中的后进先出顺序分配和释放这些对象。
当一个对象被释放时,相应的entity会再次attach到之前attached的对象(如果有的话)。
基于堆:将内存分为多份,每份保存对象或未使用。也称为*模式,可通过显式请求动态创建对象的完全动态模式。 一个entity可先后attached任意数量的对象; 对象创建的模式在编译时通常是不可预测的, 对象还可以包含对其他对象的引用。支持创建复杂的动态数据结构。
三、Java内存模型
对象创建在heap中,Java支持对象的自动回收。
每个线程有自己的栈,栈中包含所有方法的局部变量,线程只能访问自己的线程栈,线程创建的局部变量其他线程不可见,即使两个线程的代码一样,创建的同名变量仍然在各自的栈中,每个线程有自己版本的局部变量。
基本类型的局部变量保存在线程栈中,一个线程可能会将基本类型变量的副本传递给另一个线程,但它本身不能共享基本类型局部变量。对象类型数据保存在堆中,如果对象被指派到某个局部变量,或者作为其他对象的成员变量,创建的对象仍然在堆中。
总之,Java内存模型有以下关键点:
基本数据类型的局部变量保存在线程栈中;
局部变量引用了对象,引用保存在栈中,对象本身存储在堆中;
对象包含的方法和方法包含的局部变量存储在栈中;
对象的成员变量同对象一起存储在堆中,不论成员变量的类型是基本类型还是对象类型(对其他对象的引用) ;
静态的类变量同类的定义一起保存在堆中。
如果两个线程同时调用同一个对象上的一个方法,它们都可以访问该对象的成员变量,但是每个线程都有自己的局部变量副本。
Java虚拟机的内存结构
栈用来:
为方法传递参数、返回结果、在评估表达式时存储中间结果、存储本地变量
堆用来:
存储对象、数组(Java中数组是对象)以及使用new操作符
四、垃圾回收
静态模式中不存在内存回收问题,动态模式(stack-based,heap-based)则需要考虑内存空间回收问题。
基于块结构的语言中:在给定块中声明的所有实体同时发生对象分配,从而允许为整个程序使用单个堆栈。Heap模式中,对象的创建在编译时未知,所以无法预测对象何时无用(可回收)。
可到达对象与不可到达对象
根集合由系统的root对象以及局部entity、子程序的参数或返回值构成,在free模式下管理内存,第一步是区分可达对象和不可达对象,执行计算的过程称为变化器,因为它在动态更改对象图。
垃圾回收(GC)
如果没有自动垃圾回收
①防御性编程:进行防御性拷贝,每个人拥有自己的灯光系统,自己负责自己灯光的开关
②配对原则:一个new对应一个delete,让产生内存分配的对象也作为释放该内存的对象,谁开的灯,谁负责关闭
③拥有者概念:只有拥有者被允许释放该对象,最后的拥有者执行释放,每个拥有者或者传递所有权或者释放。
④检测技术:可通过维护一个对象分配表发现内存分配错误,分配和释放前都要检查内存分配表,从而解决内存泄露、悬挂引用和重复释放这几个问题
⑤多个所有者:共享对象
通过计数器来记录对象的引用数目
显式释放的危险
清理内存过于缓慢,造成可用内存不足(内存泄露)
清理了非垃圾对象,造成悬挂指针;重复释放,破坏了内存管理
主要问题:内存泄露、悬挂引用、重复释放与浪费调试时间
五、内存回收的基本算法
GC的代价度量
执行时间、延迟时间、内存消耗和其他度量
GC的基本算法
GC的主要任务是区分可达对象和不可达对象(存活对象与死亡对象)
记录引用次数
在每个对象上记下一个注释,指示对象的实时引用数。 如果对象的引用计数变为零,则抛出该对象。
缺点:造成引用循环的陷阱
标记-清除
记下你需要的对象(根),然后以递归方式记录活动对象所需的任何内容之后,检查所有对象并丢弃没有标记的对象
每个对象都有一个标志,标志是否属于live集
标记-压缩
将标记放在您需要的物体上,将带有标记的任何东西移到车库后面,烧掉车库前面的一切
复制
将您需要的物体移动到新车库,然后递归移动新车库中物体所需的任何物品,之后,烧毁旧车库
六、JVM中的垃圾回收
Java垃圾回收将堆划分成不同的区域(generation代),以便GC可以更快地识别可以删除的对象
新对象分配到young generation中,GC后仍然存活的对象,提升到old generation中,PermGen/Metaspace中保存VM和class的元数据,以及类的静态变量
根据不同代的不同特征,采用不 同的GC算法
The Yong and Old Generations
在young generation中,每次GC会发现大量死亡对象,少量存活对象,复制算法适合,代价低
在old generation中,对象存活率高,适合采用标记算法
The PermGen and Metaspace
PermGen中保存类的定义、静态方法 、静态对象的引用等内容
Java 8 开始,用Metaspace替代了PermGen,区别在内存分配方面
Metaspace的容量是自动增长的,此外,当类的元数据使用达到了metaspace最大值时,会自动触发GC
降低了OutOfMemory 的风险
但仍然需要监控和调优,避免内存泄漏
三个空间任何一个已满,且存在对空间的额外需求时,会发生GC
Minor, Major and Full Garbage Collection
年轻代中内存不够时,发生Minor GC
Minor GC后存活的对象升级到老年代
老年代空间不够时,进行full GC
full GC针对年轻代、老年代和永久代,当没有空间提供给minor GC将对象提升到老年代中,或者永久代中无空间保存class元数据时发生。
年轻代中的垃圾回收
GC时,Eden区中所有存活的对象都会被复制到ToSpace中,FromSpace区中仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到ToSpace中。
经过这次GC后,Eden Space和FromSpace区被清空, FromSpace和ToSpace互换角色。ToSpace始终为空。Minor GC会一直重复这样的过程,直到ToSpace区被填满,此时会将所有对象移动到年老代中。
七、JVM中的垃圾回收调控
GC中的性能考虑
吞吐量:总时间中非用于垃圾回收的时间
暂停: 由于GC导致的暂停次数
不同用户有不同的GC需求,E.g.,Web用户追求高吞吐量,而图形交互程序则追求低暂停。
需要根据各种情况权衡代的容量,一个代的容量不影响其他代的回收频率和暂停时间。
没有普适的容量设置准则,应根据应用的内存使用和用户需求进行具体问题具体分析。
最佳做法是将GC时间控制在执行时间的5%之内
JVM在启动时提供了缺省的设置,可根据具体需求自行重新设定。
(1)调控VM的堆尺寸
JVM堆大小决定了虚拟机收集垃圾的频率 和时间长短。对于特定的程序,应分析实际情况后进行调整。较大的堆,GC速度慢,GC频率低。如果根据内存需求设置堆大小,则完整垃圾回收速度会更快,但会更频繁地发生。
老年代大小同年轻代大小相关,老年代的初始大小是-Xms减去-XX:NewSize;老年代的最大值是-Xmx减去-XX:MaxNewSize; 如果-Xms和-Xmx设置为相同的值并且使用-Xmn,或者XX:NewSize与-XX:MaxNewSize的值相同,则老年代的大小为-Xmx(或-Xms)减去-Xmn。
(2)选择垃圾回收的方案
串行收集器:使用一个线程进行垃圾回收,执行时会冻结所有的应用线程,不适合多线程应用。适合不要求低暂停时间和单机程序。
并行收集器:对年轻代的回收采用并行方式(多个线程) ,对老年代的回收还是单线程。
CMS垃圾收集器:利用多垃圾回收线程,适用于短回收暂停,且能够在应用程序运行时与垃圾收集器共享处理器资源。应用程序平均响应较慢,但不会停止响应以执行垃圾收集。
G1(Garbage First)垃圾收集器:适用于运行在多处理器大内存空间的应用程序。
(3)使用verbose垃圾回收
详细的垃圾收集选项(-verbose:gc)使您可以准确测量垃圾收集的时间和资源。
(4)手动请求垃圾回收
Garbage Collect调用JVM的System.gc()方法来执行垃圾收集。 然后,JVM实现决定请求是否实际触发垃圾回收。
八、在Java中使用高效的I/O
加速I/O:常用技巧buffer
Buffer:通常用软件实现buffer机制,利用速度快的RAM,缓冲区通常用于接收数据的速率与处理数据速率之间存在较大差异的情况下,或者在速率可变的情况下(例如打印机池或在线视频流)。
上层应用组件不需要等待下层组件真实地接受全部数据,即可返回操作,加快了上层组件的处理速度,从而提升系统整体性能。
例如使用BufferWriter
1:InputStream/OutputStream
InputStreamand和OutputStream是基于字节的
2:Reader/Writer
Reader和Writer是基于char的包装器。
3:Nio
4:Scanner
5:Lines
Java I/O推荐
日常使用:Buffered{Reader,Writer}
随意使用:Scanner
流整合:Files.lines,不支持并行
Async: java.nio.AsynchronousFileChannel 异步文件通道
还有很多有用的API,根据需要使用
NIO: New I/O
旧的Java iostream类有太多的软件层,影响速度,而New I/O通过一组buffer类,允许将数据从JVM移动到OS,只需最少的内存间复制;使用统一的通道类,允许数据直接从缓冲区输入到文件和sockets;并且首次在Java中提供了文件锁定。
NIO中的Buffers
Buffer对象是数据的容器,行为上类似字节数组。
Buffer的属性
文件位置和上限
limit属性定义可用于写入的最后一个空间,或者已将多少数据写入文件。写完后,可以调用flip()方法设置当前位置值的限制,并将位置重置为零,准备读取。
ByteBuffer类
实践中最重要的缓冲类可能是ByteBuffer类。 这表示原始字节的固定大小向量。
创造Buffers
ByteBuffer allocate(int capacity)
ByteBuffer allocateDirect(int capacity)
ByteBuffer wrap(byte [] array)
ByteBuffer wrap(byte [] array, int offset, length)
备注:
allocate()使用大小容量的普通Java后备数组创建一个ByteBuffer
allocateDirect()也许是最有趣的情况,创建一个直接的ByteBuffer,由系统内存的容量字节支持。
wrap()方法创建ByteBuffer,由用户分配的全部或部分数组支持。
其他ByteBuffer中的初始类型
ByteBuffer putChar(char value)
ByteBuffer putChar(int index, char value)
ByteBuffer putInt(int value)
ByteBuffer putInt(int index, int value)
Channels
打开Channels
SocketChannel sc = SocketChannel.open() ;
Sc.connect(new InetSocketAddress(hostname, portnumber)) ;
使用Channels
int read(ByteBuffer dst)
int write(ByteBuffer src)
举例:将一个Channel中的内容复制到另一个
内存映射文件
Scatter/Gather
SocketChannels
基本的Socket Channel操作
其他Channels的特性
举例
(以上内容老师在课件中也注释较少,不是重点,故省略了很多细节,具体参考原版课件