IBM JVM的GC分为三个步骤Mark phase(标记)Sweep phase(清扫)Compaction phase(内存紧缩) 在了解这些过程之前我们先看一下IBMJava中的对象的Layout和Heap lay out 一个Java对象在IBM vm中的结构如下
size+flags
mptr
locknflags
objectdata
size+flags
这是一个byte的slot( 平台)这个slot的主要功能就是描述对象的尺寸由于IBMJava中的对象都是以byte的倍数分配的因此对象的尺寸其实就是真实尺寸/存放在byte的slot中另外在这个slot的低三位是保留字段起到标记对象的作用他们分别为 bit:swapped bit这个交换位被用于Compaction phase即内存紧缩阶段使用同时 这一位在标记堆栈溢出的时候(mark stack overflow)也被用于标记NotYetScanned状态 bitdosed bit这个位用于标示这个对象是否被某个堆栈或者寄存器reference到了
如果这个标志被至位则这个对象就不能在当前的GC cycle中被删除而且如果某个reference指向的内存不是一个真实的reference比如是一个简单的float 或者integer变量但是它的值恰巧就是Heap中某个Object的地址的时候我们就不能修改这个refernece这种对象的bit也被置为bit:pinned bit标记一个对象是否是一个一个钉扣对象(PINNED object)一个Pinned Object也不能被GC删除因为他们可能在Heap之外被reference到了典型的一个例子就是Thread还记得我上面说的僵死县城么?它不能被删除的道理就是这个另外一种PinnedObject就是 JNI Object即被本地代码使用的对象
Mptr:
在平台上也是byte的slotMptr有两个功能
如果mptr不是一个数组则Mptr指向一个方法块(method block)你可以通过这个method block来得到一个类块(class block)这个类块告诉你这个Object是属于哪个class的实例method block和class block由Class Loader分配而不是heap在heap中进行分配
如果mptr是一个数组(Array)mptr包含了这个对象中数组的元素个数 lockflags
在平台上也是byte的slot但是这个slot只有低位被用到
bit:是array flag如果这个位被置位那么这个对象就是一个数组同时mptr字段就包含了数组的元素个数
bit是hashed和moved bit如果这个位被置位那么他就告诉我们这个对象在被hashed以后被删除了
Object Data
就是这个对象本身的数据
Heap layout:
heap top
heap limit
heap base
heap base是heap的起始地址heap top是heap的结束地址heaplimit 是当前程序使用的那段heap可以进行扩展和收缩的极限你可以用Xmx参数在java运行的时候对heap top和heap base进行控制
Alloc bits 和 mark bits
heap top allocmax markemax
heap limit alloc size marksize
heap base
上面这个结构描述了heap和alloc bits 以及markbits之间的关系allocbits和markbits都是元素为个bit的vector他们与heap有同样的长度下面是两个对象被分配以后在heap和两个vector中的表现
heaptop allocmax markmax
heaplimit allocsize marksize
objecttop
objectbase objectallocbit objectmarkbit
objecttop
objectbase objectallocbit
如上面的结构如果一个对象在heap被alloc出来那么在allocbits中就标示出这个对象的起始地址所在的地址allocbits中只标记起始地址但是这个过程告诉我们这个对象在那里被创建但是不告诉我们这个对象是否存活当在mark phase中如果某一个对象比如object仍然存活那么就在markbits中对应的地址上标记一下The free list
IBM jvm中的空闲块用用一个free list链标示如图
freechunck freechunck freechunckn
size size size
next>next>next>NULL
freeStorage freeStorage freestorge
有了这些基本概念我们来看看Mark phase的工作情况
MarkPhase
GC的Mark phase将标记所有还活着的对象这个标记所有可达对象的过程称为tracingJvm的活动状态(active state)是由下面几个部分组成的每个线程的保存寄存器(saved registers)描述线程的堆栈Java类中的静态元素以及局部和全局的JNI(Java Native Interface)引用在Jvm中的方法调用都在C Stack上引发一个Frame这个Frame包含了对象实例为局部变量的assignment结果或者传入方法的参数所有这些引用在Tracing过程中都被同等对待实际上我们可以把一个线程的堆栈看城一系列bytes slot的集合然后对每一个堆栈都从顶向下对这些slot进行扫描在扫描的过程中都必须校验每个slot是否指向heap当中的一个真实的对象因为在前面我就说过很有可能这些slot值仅仅是一个int或float但是他们的值恰巧就等于heap中的一个对象地址因此在扫描的时候必须相当的保守扫描的时候必须保证所有的指针都是一个对象而且这个对象没有在GC中被删除只有符合下面条件的slot才是一个指向对象的指针必须以byte的倍数分配的内存必须在heap的范围之内(即大于heapbase小于heaptop)对应的allocbit必须置为满足这些条件的对象引用我们称为roots并且把他们的dosed bit置为表示不能被GC删除我想大家已经知道C#中为何连Int和Float都是OBject的原因了吧在C#中因为都是OBject因此在tracing的过程中就减少了一次校验这个减少对性能起到很大的影响 如果扫描完成那么Tracing过程便能安全精确的执行也就是说我们可以在roots中通过reference找到他对应的objects由于他们是真实的reference那么我们就能够在compactionphase中移动对应的对象并且修改这些reference
Trace过程使用了一个可以容纳k的slots的stack所有的引用逐个push进入这个堆栈并且同时在markbits中进行标记当push和mark的工作完成之后我们开始pop出这些slot并且进行trace
常规的对象(非数组对象)将通过mptr去访问classblockclassblock将会告诉我们从这个对象中找到的其他对象的reference在那里?当我们在classblock找到一个refernce以后如果发现他没有被mark那么我们就在markallocbits中mark他然后把他再压入堆栈
数组对象利用mptr去访问每个数组元素如果他们没有mark则mark然后压入堆栈
Trace过程一直持续进行直到堆栈为空
MarkStack OverFlow
由于markStack限制了尺寸因此它可能会溢出如果溢出发生那么我们就设定一个全局的标志来表明发生了MarkStack OverFlow然后我们将那些不能push入stack的OBject的bit设定为NotYetScanned然后当tracing过程完成以后检验全局标志如果发现有overflow则把NotYetScanned的对象再次压入堆栈开始新的tracing过程
并行Mark(Parallel Mark)
由于使用逐位清扫(bitwise sweep)和内存紧缩规避功能GC将化大部分的时间是用于Mark而非前面两项这就导致了IBM JVM需要开发一个GC的并行版本并行GC的目的不是以牺牲单CPU系统上的效能来换取在路对称CPU系统上的高效率
并行Mark的基本思想就是通过多个辅助线程(helper thread)和一个共享工作的工具来减少Marking的时间在单CPU系统中执行GC工作的只有一个主线程Parallel mark仍然需要这个主线程的参与他充当了管理协调的角色这个Thread所要执行的工作和单CPU上的一样多包括他必须扫描CStack来鑒别需要收集的roots指针一个有N路对称CPU的系统自动含有n个helper thread并且平均分布在每个CPU上master thread将scan完的reference集合进行分块然后交给helper thread独立完成mark工作
每个Helper thread都被分配了一个独立的本地mark stack以及一个shareable queuesharqueue将存放help thread在mark overflow的时候的NotyetScanned对象然后由master thread将sharequeue中的对象balance到其他已经空闲的thread上去
并发Mark(Concurrent mark)
Concurrent mark的主要目的在于当heap增长的时候减少GC的pause time只要heap到达heap limit的时候Concurrent mark就会被执行在Concurrent phase中GC要求应用中的每个线程(不是指helper thread而是应用程序自己开启的线程以便充分利用系统资源)扫描他们自己的堆栈来得到roots然后使用这些roots来同步的trace 可达对象Tracing工作是由一个后台的低优先级的线程执行同时程序自己开启的线程在分配内存的时候必须执行heap lock allocation
由于使用程序自己开启的线程并发的执行mark live objects我们必须纪录那些已经trace过的object的变化这个功能是采用一个叫写闸(write barrier) 来实现的这个写闸在每次改变引用的时候被激活它告诉我们什么时候一个对象被跟新过了以便我们从新扫描那部分heap写闸的具体实现是Heap会分配出byte的内存段每个段都分配了一个byte在卡表中(card table)无论何时一个对象的reference被更新cardtable将同步纪录这个对象的起始地址使用Byte而不用bit的原因是写byte要比写bit快倍而且我们可能希望空余的bit会在未来被用到
当Concurrent mark执行完毕以后STW collection(stop total world)将会被执行stw的意思是指suspend所有程序自己开启的线程因此我们可以看到如果使用Concurrent mark那