笔记书籍:JRockit权威指南深入理解JVM
作者:马库斯.希尔特 马库斯.拉杰格伦 著
翻译:曹旭东
3.1自动内存管理
自动内存管理(automatic memory management)是指无须使用老式的内存释放操作,例如free操作符 ,就可以自动回收废弃对象占用的内存的垃圾回收技术。
大部分自动内存管理系统使用的都是引用跟踪技术,即执行垃圾回收时沿着对象的引用关系遍历堆中对象,以确定哪些需要回收,哪些需要保留。
堆:特指在使用垃圾回收的环境中,所有用于存储对象的、非线程局部的内存空间(non-thread local memory)。
3.1.1自适应内存管理
自适应内存管理(adaptive memory menagement)是指主要通过运行时反馈来调整内存管理系统的行为。
出于性能考虑,自适应内存管理必须要正确利用运行时反馈,这其中包括修正垃圾回收策略,自动缩放堆大小,以适当的时间间隔整理内存碎片,以及判断何时 stop the world(STW)等。STW是指暂停正在执行的Java应用程序,转而执行垃圾回收的某些操作。
3.1.2自动内存管理的优点
1 加快软件开发速度
2 自动内存管理解决了内存分配问题
3 自适应内存管理可以根据应用程序的具体运行状态,适时地修正垃圾回收策略,例如改变执行垃圾回收任务的线程数量或其他与垃圾回收相关的参数。
3.1.3自动内存管理的缺点
1 降低应用程序的执行效率
2 影响垃圾回收器工作时间长短的主要因素是堆中存活对象(live object)的总数量,而不是堆的大小
3 使用垃圾回收机制仍然可能存在内存泄漏,如果应用程序错误地保存了很多本应被回收掉的对象的引用,这些对象被认定为存活对象。
3.2堆管理基础
3.2.1对象的分配与释放
一般来说,为对象分配内存时,并不会直接在堆上划分内存,而是先在线程局部缓冲(thread local buffer)或其他类似的结构中找地方放置对象,然后随着应用程序的运行,新对象的不断分配,垃圾回收逐次执行,这些对象有可能被提到堆中,或被垃圾回收释放。
内存管理系统为了给对象找到合适的位置,使用空闲列表((free list)——串联起内存中可用内存块的链表)来管理内存中的可用空闲区域,并按照某个维度的优先级排序。
3.2.2碎片与整理
碎片化:当死对象被垃圾回收器清除后,就会在堆中留下一个个的孔洞。
当死对象被清除后,所释放的内存空间则为孔洞,若孔洞不足以存放新的对象(内存分配不足)则抛出OutOfMemoryError错误,就会 采取一系列补救措施,这个过程就为整理(compaction)
注意:整理是一个STW操作。执行整理操作时,会遍历对象的引用关系图,并假设互相引用的对象很有可能会被依次访问到,
所以垃圾回收器会尽量将有引用关系的对象紧挨着放在一起 ,这样做是为了可以更好地利用缓存。同时,如果对象的生命
周期差不多的话,都可以在垃圾回收后得到更大的连续空间。
各种垃圾回收算法可以不同程度上抑制内存碎片化的进程(例如使用分代垃圾回收),或者实现自动整理(例如暂停并复制)
3.3垃圾回收算法
在这里分为两种类型:
引用计数垃圾回收:记录某个时刻有多少存货对象引用了某个指定对象。当某个对象引用计数降为0时,即没有存活对象,则将被回收。
引用跟踪垃圾回收:当执行垃圾回收时,为存货对象建立一个引用关系图,并回收掉那些不可达对象。
3.3.1引用跟踪
引用跟踪垃圾回收的概念
1 将应用程序中所有可见对象标记为存活,然后递归标记可以通过存活对象访问的对象。
根集合
专指上述搜索算法的初始输入集合,即开始执行引用跟踪时的存活对象集合。一般情况下,根
集合中包括了因为执行垃圾回收而暂停的应用程序的当前栈帧中所有的对象,包含了可以从当
前线程上下文的用户栈和寄存器中能得到的所有信息。此外,根集合中还包含全局数据,例如
类的静态属性。简单来说就是,根集合中包含了所有无须跟踪引用就可以得到的对象。
1标记-清理
标记
将根集合中的一个对象添加到队列中
遍历队列中的对象,对每个对象X:
将X标记为可达的
将X标记的引用 添加到队列中
清理
遍历堆中每个对象X:
如果X没有被标记为可达的,则将其回收
执行标记操作之前,首先要遍历存活对象图(live object graph),然后遍历所有堆中对象,识别未标记对象(实际操作并非如此,已经有改进加速了这个过程,并尽量使之可以并行)
简单实现标记-清理算法时,对于每个可达对象,一般都会使用一个标记位(mark bit)来表示该对象是否已经被标记过。为对象分配内存时,为了满足对齐要求,其起始地址一般都是偶数,因此,对象 指针的最低位总是0,正适合用来做标记位。
标记-清理算法的一个变种是可以并行运行的三色标记-清理(tri-coloring mark and sweep)。
设置三个变量值分别代表白色,黑色,灰色。白色标记为死对象,将会被回收;黑色标记为未标记为白色标记的对象;灰色对象为存货对象,但其指向对象状态未知。
在标记开始时,所有根集合标记为灰色对象,其他对象标记为白色。
三色标记算法的伪代码实现如下:
标记
默认将所有对象标记为白色
将根集合中的对象标记为灰色
if 灰色对象 exists:
for x in 灰色对象 :
for y in (x引用 的白色对象):将y标记为灰色
if x 所引有的引用都指向了另一个灰色对象:
将x标记为黑色
清理
回收所有白色对象
2暂停-复制
暂停-复制是一种特殊的引用跟踪垃圾回收技术。(不实用)
将堆划分成两个同样大小的分区, 应用程序使用其中一个分区运行,在运行期间 将存活对象复制到另一个空白分区,等到死对象被回收后将空白分区中的存活对象移动回运行分区。所以不仅仅内存只能运行一半,而且频繁的暂停复制影响内存空间和应用程序性能。
3.3.3 STW详解
STW(stop-the-world),为便于执行垃圾回收而暂停所有应用程序的所有Java线程。
1保守式与准确式
虚拟机需要一些 额外的信息记录栈帧中对象的存储位置,这样才能在执行垃圾回收时正确构建根集合的初始值,此外,如果某个线程因垃圾回收停止,也应明确该线程当前的上下文。
在为对象分配内存空间时,垃圾回收器会记录相关信息。
查找某个线程暂停时的上下文信息需要使用检索表(lookup table)和搜索树(search tree)来查找当前指令寄存器的指令时属于哪个java方法的即可。
java使用的是准确式垃圾回收器(exact garbage collector),可以将对象指针类型数据和其他类型的数据区分开,只需要将元数据信息告知垃圾回收器即可,这些元数据信息,一般可以从java方法的代码中得到。
(一刷只是做了解,不深入笔记)
3.3.4分代垃圾回收
将堆划分为两个或多个称为代的空间,并分别存放具有不同长度生命周期的对象,可以提升垃圾回收的执行效率。
在JRockit中,新创建(young)的对象存放在称为新生代(nursry)的空间中,它的内存大小比老年代小很多。
随着垃圾回收的重复执行,生命周期较长的对象会被提升(promote)到老年代。
1 多个新生代的内存排布
一般情况下只有一个新生代,而在某些情况下则为多个小的新生代分级存储不同年龄的对象以根据对象存活次数减少老年代中的对象数量。
2 写屏障
在分代式垃圾回收中,执行垃圾回收时,可能存在引用双方不在一个代中。因此,如果在执行垃圾回收时将所有这样的引用关系都更新一遍 ,这种操作带来的性能损耗就完全抵消了分代式垃圾回收带来的性能提升。
由于分代式垃圾回收的关键点就是将堆划分为不同的内存空间,并分别处理其中的对象。因此,需要代码生成器提供一些辅助信息来帮助完成垃圾回收。
在 实现分代式垃圾回收时,大部分JVM都是用名为写屏障(write barrier)的技术来记录执行垃圾回收时需要遍历堆的哪些部分。当对象A指向对象B时,即对象B成为对象A的属性的值时,就会触发写屏障,在完成属性域赋值后执行一些辅助操作。
写屏障的传统实现方式为将堆划分为多个小的连续空间(例如每块512字节),每块空间称为卡片,于是,堆被映射为一个粗粒度的卡麦(card table)。当Java应用程序将某个对象赋值给对象引用时,会通过写屏障设置脏标志位 (dirty bit),将该对象所在的卡片标记为脏。
这样遍历从老年代指向新生代的引用时间得以缩短,垃圾回收器在做新生代垃圾回收时,只需要检查老年代中被标记为脏的卡牌所对应的的内存区域即可。
3.3.5吞吐量与延迟
优化应用程序运行时间,缩短垃圾回收所花费的时间是必要的方案。但因垃圾回收是STW式的,会在某个节点停止应用程序线程,但如果并发执行则会记录额外的信息,从而延长垃圾回收时间。并且,对于大多数应用程序来说,低延迟是很重要的。而导致高延迟的一个原因就是CPU将部分时间放在了非应用程序线程上。
因此,在内存管理中需要权衡的就是最大化吞吐量和保持低延迟。
1 优化吞吐量
在不需要考虑延迟的问题下,当应用程序允许暂停时间长达数秒的话,那么就可以尽全力优化以达到最大化吞吐量的目标。
并行垃圾回收方式就是以最大吞吐量为目标,在不同的垃圾回收线程之间做一些同步处理,以避免不同区域中有引用关系存在时可能会出现的种种问题。
2 优化延迟
低延迟优化基本的重点是避免STW式的操作,尽可能让应用程序线程多工作。但是,如果垃圾回收期得到的CPU资源过少 ,无法跟上内存分配的速度,则堆内存溢出,抛出outofmemoryError错误。因此,理想的垃圾回收器应伴随着应用程序的运行完成大部分垃圾回收工作。
此种方式称为并发垃圾回收(concurrent garbage collection),
标记-清理垃圾回收算法:标记是采用并行实现,执行时间最长,大约占垃圾回收时间的90%。
近并发垃圾回收:主旨是在应用线程执行的时候 ,尽快多的完成垃圾回收工作。整个垃圾回收周期包含了几个较短的STW操作环节、需要同步修改对象关系图。
3.36JRockit中的垃圾回收
JRockit中垃圾回收算法的基础是三色标记清理算法,并加入了改进优化以提升并行性,优化了垃圾回收的线程数,兵使其可以与应用程序线程并发运行。
根据优化目标不同,JRockit中的垃圾回收可以分代执行,也可以不分代执行。
1 老年代垃圾回收
在实际实现中,使用则是双色标记算法(使用灰色标记存活对象)。区别在于,JRockit中的存活对象会被放入到垃圾回收线程的线程局部队列(thread local queue)。好处有两点:
——1 并行运行的垃圾回收线程可以使用各自的线程局部数据,而不同步操作数据,
——2 可以使用预抓取(prefetch)方式,按照先入先出的顺序来访问队列中的元素,这样可以提升整体性能。
此外,使用存活标记位(live bit)来标记系统中存活对象,可以快速找出应用程序在并发标记阶段新创建了哪些对象。
为了避免搜索整个存活对象图,JRockit不仅将卡麦用于分代垃圾回收,也用在并发标记后的清理工作。可以根据卡片的状态:脏/干净 在并发标记阶段结束后,只要检查卡麦中对应的卡片状态即可 。
当有新对象创建或者对象引用关系修改了的卡片会被标记为脏。
2 新生代垃圾回收
JRockit在新生代中使用了暂停-复制算法的变种,大致过程是暂停应用程序线程,复制存活对象到堆中的另一个区域或提升到老年代。
最开始新生代是空的,通过写屏障在所偶对象更新引用时将对应的卡片设置为脏,所以查找新生代存活对象时,只要扫描卡麦中脏卡片中含有存活标记位的对象即可。
老年代垃圾回收时需知晓哪些卡片在并发标记阶段被标记为脏即可,因此使用一个额外的卡片记录原始卡表中有哪些卡片被标记为脏,这些改变的卡片集合为修改集(modified union set)
保留区:新生代中的一块内存,用于存储在新生代垃圾回收之后没有被复制到老年代的对象。通过将新创建对象存储在保留区中,使新创建对象在被复制到 老年代之前被 回收掉。
3 永生代垃圾回收
存储一些元数据,例如类对象等。处于永生代的对象不会被回收掉
JRockit中没有永生代,在HotSpot中存在。
JRockit将一些元数据信息存储在堆外的本地内存中,对无用的元数据信息,会持续不断的被垃圾回收器清理。
另外,JRockit总是默认对无用元数据进行清理,而且 存储元数据的空间采用了动态分配内存的策略。
4 内存整理
JRockit采用单线程、非并发执行内存整理工作,对于并行垃圾回收器来说,内存整理是在清理阶段进行的。