CMS算法实现总结-1

一、数据结构
Java的内存整体上可以分为五大类,Java堆,CodeCache,Metaspace,栈内存和JVM自身,栈内存是指Java线程和JVM自身的后台服务线程执行过程中分配的调用栈对应的内存,包括所谓的虚拟机栈和本地方法栈,用于保存执行过程中的本地变量,方法入参,返回地址等方法执行过程中依赖的各种要素;JVM自身是指JVM实现各种功能所依赖的C/C++数据结构所占用的内存。后面两个的地址空间不是连续的,但是位于一段相对固定的地址区间范围内。栈内存分配与释放完全由底层操作系统控制,线程开始执行具体的方法前操作系统负责分配内存,当该方法执行完成对应的调用栈帧内存就会由操作系统自动回收;JVM自身的内存的分配不完全由操作系统控制,JVM对于一些经常被创建和销毁的数据结构(如用于临时保存oop的Handle)都实现了对应的内存管理工具(Handle对应的内存管理类是HandleArea),对象被销毁了其对应的内存并不会归还给操作系统,而是放到一个专门维护空闲内存块的数据结构中保存起来,下次再分配时优先从该数据结构中分配,如果空闲内存块不足再尝试向操作系统申请新的内存块,这样做的目的是为了减少操作系统分配内存的性能损耗。前面3个的地址空间是连续的,即对进程而言,这是一块连续的内存,下面来描述下这三个的数据结构。

1、Java堆
CMS下Java堆的实现类就是GenCollectedHeap,该类实际不特指CMS下的Java堆内存,而是表示一个可能包含多个Generation的堆内存,现有的实现都是只有两个Generation。GenCollectedHeap的初始化在Universe::initialize_heap方法中,如下图:


image.png

GenCollectorPolicy的initialize_generations方法会指定包含的Generation的类型,如下:


image.png

GenCollectedHeap会在初始化时调用GenerationSpec::init完成各Generation的初始化,其实现如下:

image.png

各Generation的类继承关系如下:

image.png

在默认配置下,年轻代的实现类就是DefNewGeneration,如果UseParNewGC为true,则变成ASParNewGeneration,注意UseParNewGC默认为false,如果所在系统是多核系统,在JVM启动时会将UseParNewGC的默认值由false改成true。ParNewGeneration相比DefNewGeneration只是增加了并行遍历根节点的逻辑,GC的流程一样,ASParNewGeneration相比ParNewGeneration增加了自动调整年轻代内存大小的支持;老年代的实现类默认是TenuredGeneration,如果UseConcMarkSweepGC为true,则变成ASConcurrentMarkSweepGeneration,相比ConcurrentMarkSweepGeneration只是增加了自动调整内存大小的支持。注意UseAdaptiveSizePolicy默认为true,表示根据堆内存的实际使用情况和GC耗时等动态调整Generation的内存容量。

Generation的实现是基于Space的,Space负责实际的内存管理,其类继承关系如下:

image.png

其中CompactibleSpace提供内存压缩的支持,即通过对象复制将存活的对象一个挨着一个的放到一起,减少内存碎片,另外因为对象复制的过程中存活的对象覆盖了需要了垃圾回收的对象,所以间接的实现的垃圾回收的效果;ContiguousSpace提供基于移动top指针的连续内存分配的支持,top指针前的内存区域表示已经分配出去了;EdenSpace和ConcEdenSpace只是添加了一个_soft_end属性,分配内存时不能超过_soft_end;CompactibleFreeListSpace的实现比较复杂,大于257字节的空闲内存块放到二叉树中管理,小于257字节的空闲内存块放到对应大小的List数组中,初始状态下List数组都是空的,二叉树中有一大块初始内存,在内存分配的过程中,会不断的从二叉树中的大块内存分割出指定大小的小块内存用来填充对应大小的List数组;实际分配时低于16字节的,从一个类似于ContiguousSpace连续分配内存的LinearAllocBlock中分配,16到257之间的从对应大小的List数组中分配,大于257的从二叉树中分配,所有分配的内存在释放时会按照大小归还到对应大小的List中或者二叉树中,在GC时这些空闲内存块会根据策略配置尽可能的合并成一个大的内存块。

CMS算法默认配置下,各Generation对应的Space如下图:

image.png

2、CodeCache
CodeCache用于缓存不同类型的生成的汇编代码,如热点方法编译后的代码,各种运行时的调用入口Stub,每个字节码对应的执行代码,一些高频访问的数据函数和底层方法等。所有的汇编代码在CodeCache中都是以CodeBlob及其子类的形式存在的。通常CodeBlob会对应一个CodeBuffer,负责生成汇编代码的生成器会通过CodeBuffer将汇编代码写入到CodeBlob中,写入的起始地址就是该段汇编指令的调用地址。

CodeCache只是对外的接口而已,具体的内存管理都是CodeHeap实现的。从CodeHeap中分配CodeBlob时,待分配的CodeBlob的大小会按照segment的大小向上对齐,一个segment可以理解为一个内存页,是操作系统分配内存的最小粒度,从而避免内存碎片。具体分配时会先在保存空闲内存块的List中查找,如果没有再按照类似top指针移动的方式来分配一块新的内存块,如果剩余内存不足则尝试扩容,扩容成功后再次尝试分配。如果一个CodeBlob被释放了,则对应的空闲内存块会被归还到List中管理。其内存结构如下图:
image.png

3、MetaSpace
MetaSpace比较特殊,一个ClassLoader实例对应一个MetaSpace,由该ClassLoader加载的类元数据都会从绑定的MetaSpace中分配内存,当ClassLoader实例被销毁了,则对应的MetaSpace也会跟着释放;MetaSpace的底层并不是一个连续的地址空间,而是一个由多个VirtualSpaceNode组成的链表,每个VirtualSpaceNode都对应一段连续的地址空间,注意这个链表是多个MetaSpace实例共享的,其内存结构如下:

image.png

分配Klass等元数据是从MetaChunk中分配的,每一个分配出去的内存块就是MetaBlock,MetaBlock中除对象头外的剩余空间用来保存Klass等元数据,对象头记录这个内存块的大小。分配MetaChunk时,VirtualSpaceList首先从当前使用的VirtualSpaceNode即_current_virtual_space中分配,当其空间不足时,VirtualSpaceList会创建一个新的VirtualSpaceNode,将旧的VirtualSpaceNode的剩余空间分配成若干个标准大小的Metachunk,保证其空间不浪费,然后将其插入到VirtualSpaceList的_virtual_space_list链表中,将其作为新的VirtualSpaceNode的next节点,新的VirtualSpaceNode变成_current_virtual_space,然后从新节点中分配Metachunk。

 VirtualSpaceNode是由VirtualSpaceList管理的,MetaChunk是由ChunkManager管理的,每个MetaSpace的内存分配都有由该实例对应的SpaceManager负责的,其整体的类数据结构如下:
image.png

VirtualSpaceList和ChunkManager管理的是全局共享的内存块,所以是静态属性,SpaceManager管理的是对应MetaSpace实例的内存,所以是实例属性。这三个类都有两个不同的属性,对应不同的MetadataType,带class的只能用于分配Klass,不带class可用于分配其他的如Method(方法),ConstantPool(常量池),Annotations,Symbol(符号引用)等。SpaceManager分配内存时,首先从_block_freelists中分配,如果内存不足会尝试从_current_chunk中分配,如果分配失败会尝试从对应类型的全局ChunkManager获取一个新的满足大小的Chunk,如果获取失败再从对应类型的全局VirtualSpaceList中获取一个新的Metachunk。获取新的Metachunk后,将其加入到合适的_chunks_in_use列表中,然后从新的Metachunk中分配内存,释放内存时则是将对应的内存块作为MetaBlock归还到_block_freelists中从而被重复利用。

二、关键类/方法
1、oop/Klass
oop是oopDesc* 的别名,oopDesc就是java对象实例,用来保存类的实例属性,其类继承关系如下:

image.png

objArrayOopDesc表示对象数组或者多维数组,typeArrayOopDesc表示基本类型数组,instanceOopDesc就是普通的Java对象,markOopDesc表示Java对象中的对象头。

Klass用来保存每个类的元数据,比如类继承关系,类实例包含的字段及其存储位置,类定义的所有方法,类定义中用到的注解等,class文件中包含的所有信息都会保存Klass中,其类继承关系如下:

image.png

ObjArrayKlass与objArrayOopDesc对应,TypeArrayKlass与typeArrayOopDesc对应;InstanceKlass与instanceOopDesc对应;InstanceClassLoaderKlass是指java.lang.ClassLoader及其子类对应的klass;InstanceMirrorKlass是指java.lang.Class对应的klass,InstanceMirrorKlass对应的instanceOopDesc就是类的class属性了,如String.class,用来保存类的静态属性;InstanceRefKlass是指java.lang.ref.Reference及其子类的Klass。InstanceKlass的三个子类主要改写了父类的引用遍历的逻辑。

2、oop_iterate / adjust_pointers / follow_contents
oop_iterate是oopDesc的方法,用于遍历某个oop的所有引用类型属性,因为入参Closure有多个子类,所以该方法也有多个重载版本,其定义和实现都是借助C中的宏来完成的,最终都转换成了对Klass的某一类方法的调用。要遍历某个oop的所有引用类型属性,关键得知道其引用类型属性在oop中的存放位置,即相对于对象地址的偏移量,这个信息就保存在Klass中,是class文件解析的时候根据该类包含的字段数量和类型计算出来的,所以oop_iterate方法的核心实现都在Klass中。具体分为五种情形:

对于普通的Java类的实例属性,为了方便找到所有的引用类型属性,所有的引用类型属性是挨着一起放在oopDesc内存的后面,并且通过Klass的内部类OopMapBlock来记录起始偏移量和引用类型属性的个数,因为引用类型属性在指针压缩下固定占用4个字节,非指针压缩下固定占用8个字节,所以只要知道了起始偏移量和引用类型属性,就可以顺序读取各引用类型属性对应的4字节或者8字节的数据,里面就保存着所引用的对象地址。注意,如果该类存在父类,则有多少个父类就会增加多少个OopMapBlock,父类的OopMapBlock记录父类的引用类型属性的起始偏移量和个数,子类只记录只属于子类的引用类型属性的起始偏移量和个数。这样做是因为子类实例会保存父类的所有属性,并且子类的属性排在父类属性的后面,从而保证子类实例调用父类的某个方法时可以正确访问父类的属性。
对于对象类型数组,对象数组实例本身记录了数组长度和用于存储数组元素的基地址,所以从基地址开始依次读取数组长度个4字节或者8字节数据即可完成所有引用类型属性的遍历
对于普通的Java类的静态属性,注意静态属性不存在类继承关系,即子类不会保存父类的静态属性,且静态属性是通过InstanceMirrorKlass对应的oopDesc来维护,即每个类对应的class实例如String.class来维护的,class实例本身也定义了引用类型属性。对于class实例本身的引用类型属性依然借助OopMapBlock遍历,其包含的静态属性则通过class实例的注入字段static_oop_field_count和InstanceMirrorKlass新增的_offset_of_static_fields属性完成遍历,前者表示静态字段的个数,是在InstanceMirrorKlass创建时由JVM注入进去的,java.lang.Class类中不包含该字段的定义,该字段的值是class实例初始化时从对应Java类的Klass中取出的;后者表示class字段中静态属性的偏移量,其值是基于Class类本身包含的属性计算出来的,是一个固定值。
对于java.lang.ClassLoader类及其子类实例,除遍历其引用类型属性外,还需要遍历关联的ClassLoaderData实例,该类记录了该ClassLoader加载的所有Klass。
对于java.lang.ref.Reference类及其子类实例,借助OopMapBlock遍历其queue属性和子类的其他自定义引用类型属性,另外三个referent属性,next属性和discovered属性都需要单独处理。
adjust_pointers和follow_contents的实现与oop_iterate基本一致,前者用于遍历某个对象的所有引用类型属性,如果其指向的对象是一个promote对象,则从对象头中取出该对象的新地址,让引用类型属性指向新地址;后者是负责堆空间压缩的MarkSweep类使用的,用来遍历某个对象的所有引用类型属性,如果该对象的对象头未打标,则将其打标并放入一个Stack中,最后会以同样的方式处理Stack中对象的所有引用类型属性。

3、ReferenceProcessor
ReferenceProcessor封装了java.lang.ref.Reference类及其子类实例的处理逻辑,每个Generation都有一个关联的ReferenceProcessor实例,用来处理属于该Generation的Reference实例,其对外的主要有4个方法:

discover_reference:负责将某个Reference实例加入到对应类型的Reference实例链表上,该链表是通过Reference实例的discovered属性构成的,discovered属性记录了链表中下一个Reference实例的对象地址,注意如果discover被禁止了,该Reference实例不是Active状态,Reference实例不在关联的Generation内存区域中,Reference实例的referent对象还有其他强引用,是软引用但是当前不需要清理或者discovered属性不为空则直接返回false,表示没有加入到链表中。该方法是在oop_iterate引用遍历的时候调用的,借助多态自动识别出Reference实例,然后做特殊处理。
process_discovered_references:负责处理加入到Reference实例链表中的所有Reference实例,满足以下两个条件则将其referent对象都作为存活对象处理,将该Reference实例从链表中移除:对于SoftReference实例,referent对象是死的但是不需要清理;对于其他类型的Reference实例,referent对象是存活的。对于链表中剩下的实例,如果不需要将referent对象置为null,则将其作为存活对象处理,注意此时并不会将Reference实例从链表中移除。只有PhantomReference和FinalReference不需要置为null,因为referent对象后面还需要使用。注意本方法还会处理JNI弱引用,如果引用对象是存活的,则将其作为存活对象处理否则置为null。
enqueue_discovered_references:用于将各个DiscoveredList中剩余的Reference实例加入到由静态属性pending和实例属性discovered构成的一个链表中并更新静态pending属性指向最新的链表头,更新Reference实例的next属性指向它自己,即将Reference实例标记成Pending状态,处理完成后再将DiscoveredList恢复成初始状态
preclean_discovered_references:是老年代GC执行预处理时调用的,用来预处理加入到Reference实例链表中的所有Reference实例,同样将referent对象是存活的Reference实例从链表中移除,并将referent对象作为存活对象来处理
注意所有加入到Reference实例链表中的Reference实例都是存活的,因为只有该实例是存活的,即有一个存活对象引用了它,才会调用其oop_iterate方法将其加入到Reference实例链表中。如果某个Reference实例只被对象A引用,且对象A是垃圾对象,则该Reference实例也会作为垃圾对象,不会加入到Reference实例链表中。因此如果希望通过ReferenceQueue知道哪些Reference实例的referent对象被回收了,则必须保证有一个静态属性来保存对Reference实例的引用。

4、markOop / promote / forward
markOop用来表示oopDesc中的对象头,64位系统下实际就是一个8字节的数据,通过不同的位来描述对象的状态,源码注释如下:

image.png

其中最后的两位即lock有4个值,如下:


image.png

0表示有轻量级锁,1表示无锁,2表示有监视器锁,3用于GC时打标,表示该对象是存活对象,最后一个5是biased_lock加上lock的3位的值,表示该对象持有偏向锁,此时前54位表示持有该偏向锁的线程。如果该对象是被promote的对象,后3位用于GC打标,前61位用于保存复制的目标地址,之所以后3能够用于GC打标,是因为Java对象都是按照8字节对齐的,对象地址的后3位肯定是0,因此将其还原的时候只需将后3位改成0即可。上图中最后一个CMS free block表示这是一个空闲内存块。

 promote就是指将某个存活对象oop从eden区拷贝到from区或者老年代的过程,如果对象年龄大于阈值则拷贝到老年代,否则拷贝到to区,如果to区内存不足则拷贝到老年代,如果老年代空间不足则会临时保存该oop,因为有可能是该对象较大,此时其他较小的对象可以正常promote成功的。从to区或者老年代按照对象大小分配好同样大小的内存后,就会将旧对象的数据复制到新分配的内存上,然后增加复制对象的对象年龄,最后将复制对象的地址写入原对象的对象头中并打标,这个动作就是forword。

5、GC_locker 和JNI关键区
JNI关键区就是指jni_GetPrimitiveArrayCritical和jni_ReleasePrimitiveArrayCritical,jni_GetStringCritical和jni_ReleaseStringCritical这两对方法之前的区域,当有线程处于JNI关键区内就不能执行GC,前者返回基本类型数组的基地址,后者返回字符串对象的字符数组基地址,如果GC期间发生了对象复制,因为前面返回的基地址还是指向原来的对象,所以是错误的,原业务线程就无法正常执行了。

GC_locker就是用来实现JNI关键区的,GC时会调用其check_active_before_gc方法判断是否有线程处于JNI关键区,同时通知GC_locker需要GC,如果有则终止执行GC,GC_locker负责在最后一个GC线程退出后触发GC。当GC_locker发现需要GC了,就会阻止其他线程进入到JNI关键区中,直到所有的线程都从JNI关键区中退出,注意最后一个退出的线程是在其触发的GC执行完成后才退出,所以这里是阻塞其他线程直到GC结束为止。

6、VM_Operation / VMThread / ConcurrentMarkSweepThread
VM_Operation表示一个完全由JVM负责执行的操作,比如GC,内存dump等,其子类有很多,跟GC直接相关的子类叫VM_GC_Operation,其类继承关系如下:

image.png

跟CMS相关的子类叫VM_CMS_Operation,其类继承关系如下:


image.png

每个VM_Operation都有3个标准方法,doit_prologue负责执行事前准备工作,如获取锁,doit负责执行具体的操作,如GC,doit_epilogue负责执行事后的资源清理工作,如释放锁。VM_Operation创建后都是调用VMThread::execute((VM_Operation*)方法来执行该操作,具体而言,创建VM_Operation的线程会先执行doit_prologue方法,如果该方法返回true,即执行成功后,才会将该Operation提交到一个由VMThread维护的一个队列中,然后会一直阻塞直到VMThread将该Operation执行完成,最后创建VM_Operation的线程负责执行doit_epilogue方法。

如果VM_Operation需要在安全点下执行,则VMThread在执行时会先调用SafepointSynchronize::begin()方法等待所有其他线程逐步进入到安全点,即线程由可执行状态变成阻塞状态,然后再执行具体的Operation,注意因为进入安全点的损耗很大,为了避免频繁的进出安全点,VMThread会将多个需要在安全点下执行的操作按照加入链表的顺序依次执行完成,等所有需要在安全点下执行的操作都执行完成后再调用SafepointSynchronize::end()方法逐步唤醒其他所有在安全点阻塞的线程开始正常执行。

VMThread是一个本地线程,即直接通过C++代码创建的,而不是通过Java代码创建的Java线程,在JVM启动的时候就会创建该线程,并且会阻塞直到该线程开始正常执行为止。VMThread不是一个后台线程,即不会随着JVM主线程退出而自动退出,而是通过一个状态标识来控制其退出的,并且在退出前还会执行一系列的停止操作,如通知JIT编译器停止编译。VMThread的线程优先级默认是NearMaxPriority,该值是9,最大的是10,正常的线程是5,这样就保证了CPU可以分配尽可能多的执行时间给该线程,从而让该线程尽可能快的完成任务。VMThread的run方法的核心就是loop方法,后者是一个while(true)死循环,不断的从保存待执行的Operation队列中获取待执行的Operation并执行,如果为空则休眠,等待下一次添加Operation时将其唤醒,如果退出的状态标识为true,则退出循环,开始执行VMThread退出前的操作。

ConcurrentMarkSweepThread(后面简称CMSThread)是开启CMS算法时才会创建的一个线程,跟VMThread一样都是一个本地非后台线程,其优先级默认也是NearMaxPriority,是在JVM初始化老年代时创建的。其run方法也是一个while循环,休眠最多2s后判断是否需要执行老年代GC,如果需要则执行老年代的后台GC,否则继续下一次的休眠;如果退出状态标识为true,则终止循环,开始执行CMSThread的退出操作。VMThread和CMSThread的优先级一样,为了避免两个同时执行,CMSThread提供了synchronize和desynchronize方法,这两方法通过CMSThread的_CMS_flag静态属性实现了一个同步锁,简称CMS Token,比如VMThread调用synchronize方法,如果此时CMSThread持有CMS Token则VMThread会休眠直到CMSThread释放CMS Token并唤醒VMThread,然后VMThread获取CMS Token。

CMSThread执行GC时只有两个操作需要在安全点下执行,即上面的VM_CMS_Operation的两个子类对应的操作,其他操作都不需要在安全点下执行,即可能与执行具体业务操作的JavaThread同时执行,为了避免CMSThread执行GC时一直占用CPU导致执行业务操作的JavaThread得不到CPU执行时间,CMSThread提供了asynchronous_yield_request和acknowledge_yield_request方法,前者告诉CMSThread需要让出CPU执行时间,即执行yeild,会增加需要yeild的计数,通常是在老年代分配内存或者老年代扩容时会调用此方法;后者告诉业务线程CMSThread执行过yeild了,会减少yeild的计数。CMSThread在执行GC的某个步骤时,如遍历完了一个oop的所有引用类型属性后就会检查该计数,如果大于0则执行yeild。执行yeild时会先释放之前占用的锁,然后通过sleep 1ms的方式让出CPU,然后将yeild计数减一,等下一次CPU再次执行CMSThead时,同样会检查yeild计数是否大于0,如果是则执行相同的操作,执行yeild计数变成0为止。最后CMSThread再重新获取锁,恢复原来的GC步骤正常执行。

7、BarrierSet / CardTableRS
BarrierSet表示一个对对象属性、数组元素、内存区域读写的一个屏障,提供读写前后的处理动作,现有的实现只提供了其写屏障接口的实现,读屏障接口都是空实现,CMS下其实现类是CardTableModRefBSForCTRS。该类是基于卡表实现的,卡表实际是一个字节数组,每个卡表项对应一个字节,对应一个512字节的内存区域,修改对象引用类型属性或者修改引用类型数组时会将对应的写入地址转换成对应的卡表项,将该卡表项标记为脏的,其映射逻辑如下:

image.png

其中card_shift是一个枚举常量,取值为9,2^9=512,byte_map_base就是字节数组的基地址。
CTRS就是CardTableRS的简写,CardTableRS在构造时会创建并初始化卡表实现类,CardTableRS本身是在表示Java堆的CollectedHeap初始化时创建的。CardTableRS对外提供BarrierSet的引用,提供脏的卡表项遍历,清理,重置等功能。卡表主要用于记录引用类型属性发生变更的老年代对象,GC时会遍历脏的卡表项对应内存区域中的对象的所有引用类型属性,从而找到发生修改的引用类型属性,将老年代新引用的对象标记为存活对象。其他引用类型属性未发生变更的老年代对象认为其引用关系未发生变更,不需要遍历,从而避免了将占总内存三分之二的老年代中的对象全部遍历一遍,大大减少了GC耗时。

8、markBitMap / modUnionTable / CMSBitMap
markBitMap和modUnionTable都是负责执行老年代GC的CMSCollector的属性,前者用于记录哪些对象是存活的,后者用来记录脏的卡表项对应的内存区域,之所以使用modUnionTable是为了避免CMSThread执行GC时长时间占用卡表而影响了业务线程的正常执行。这两个属性的类型都是CMSBitMap,该类是对BitMap的一个包装,BitMap是一个位映射的工具类,可以将某个地址映射到BitMap中的某一位byte,其映射逻辑如下:

//判断某个对象地址是否已打标
inline bool CMSBitMap::isMarked(HeapWord* addr) const {
  assert_locked();
  assert(_bmStartWord <= addr && addr < (_bmStartWord + _bmWordSize),
         "outside underlying space?");
  return _bm.at(heapWordToOffset(addr));
}
 
bool at(idx_t index) const {
    verify_index(index);
    //判断BitMap中对应的映射位是否是1,如果是1,1!=0返回true,表示已经被标记了
    return (*word_addr(index) & bit_mask(index)) != 0;
  }
 
inline size_t CMSBitMap::heapWordToOffset(HeapWord* addr) const {
  //pointer_delta算出addr相对于起始地址的偏移量,单位是字节
  return (pointer_delta(addr, _bmStartWord)) >> _shifter;
}
 
 
//返回在BitMap中对应的映射地址,64位下一个地址有8字节,64位,类似于HashMap中的一个槽位
bm_word_t* word_addr(idx_t bit) const { return map() + word_index(bit); }
 
//将偏移量进一步右移6位,LogBitsPerByte在64位下都是3,LogBitsPerWord是6
//右移6位丢失的精度通过bit_mask补回来
static idx_t word_index(idx_t bit)  { return bit >> LogBitsPerWord; }
 
//返回的值实际是2的整数倍,就64位中只有1位是1,其他的都是0
static bm_word_t bit_mask(idx_t bit) { return (bm_word_t)1 << bit_in_word(bit); }
 
//BitsPerWord在64位下是64,这里实际是bit对64取余
static idx_t bit_in_word(idx_t bit) { return bit & (BitsPerWord - 1); }

markBitMap的_shifter是0,modUnionTable的_shifter是CardTableModRefBS::card_shift - LogHeapWordSize,因为每个卡表项对应的内存块的起始地址的card_shift即后9位都是0,LogHeapWordSize是3,减3是为了将其转换成字宽,即先是转换成字宽,还剩6个0,再左移6位把这6个0去掉,从而让BitMap本身用来映射的内存减少,但是不损失精度。字宽就是一个指针变量占用的字节数,64位下是8字节。

9、根节点oop
根节点oop是GC时必须保留下来的oop,引用遍历时会以这个oop作为起点,遍历其所有的引用类型属性oop,因为根节点oop是存活对象,所以其所有的引用类型属性指向的oop也是存活对象,将其称为二级根节点,于是再遍历二级根节点oop的所有引用类型属性得到三级根节点oop,不断往下遍历直到所有的引用类型属性oop都遍历完成。通常意义的根节点的查找都在GenCollectedHeap::process_roots方法中,主要包含以下几种根节点:

  • Java线程解释执行时调用栈帧中保存的oop,每个Java方法对应一个调用栈帧,对应一个本地变量表,可通过本地遍历表找到其包含的oop,每个栈帧都保存了上一个栈帧的地址,即该方法的返回地址,可借此实现所有一个Java线程所有栈帧的遍历
  • Java线程编译执行时,虽然解释执行和编译执行的底层都是汇编代码,但是解释执行时每个字节码对应的执行汇编代码是固定的,其栈帧结构是固定的;编译执行的汇编代码是编译器生成的,同一个方法在不同编译级别下产生的汇编代码可能不一样,因此编译器生成的汇编代码通过一个单独的OopMap记录栈帧中包含的oop,用来保存汇编代码的CodeBlob通过OopMapSet来保存所有的OopMap,可通过栈帧的基地址获取对应的OopMap,然后完成其中包含的oop遍历
  • 每个ClassLoader实例都对应一个ClassLoaderData,后者保存了前者加载的所有Klass,加载过程中的依赖和常量池引用,ClassLoader实例本身,其加载的所有Klass的java_mirror属性即对应类的class实例,常量池引用如某个字符串对应的String实例,依赖的oop都是根节点
  • Universe中包含的公用的异常oop如_out_of_memory_error_java_heap等,基础类型,各种类型数组对应的class实例如int.class等
  • JNIHandles中包含的全局JNI引用oop,每个Java线程包含的本地JNI引用oop在遍历Java线程的oop时会遍历
  • ObjectSynchronizer中维护的与监视器锁关联的oop
  • Management中维护的 java.lang.management API使用的用于记录内存使用情况,线程的运行情况的oop
    SystemDictionary中包含的系统类加载器实例,java.security.ProtectionDomain实例和各方法的符号引用

10、SafepointSynchronize / 安全点
安全点可以理解为一把全局大锁,理解为JVM的一种状态,当JVM处于安全点就意味着Stop The World(简称STW),即所有的Java线程(不包括VMThread以及JVM自身的后台线程如处理底层操作系统信号的本地线程 )都会停止执行,处于阻塞状态,然后VMThread就可以相对安全的,尽快的执行某个任务,如GC或者堆内存dump。这个安全是相对VMThread而言的,并非是相对Java业务线程,以GC为例,STW可以保证对象的引用关系是不变的,GC过程本身在引用遍历或者对象复制时都会保证对上层的Java业务线程无感知且不影响他们的正常执行。对Java业务线程而言本身没有所谓的安全点的概念,只是在某些条件下去检查JVM是否正在进入安全点了,如果是就会执行特定逻辑让当前线程处于阻塞状态,直到JVM从安全点退出,由阻塞状态恢复正常执行。

进入安全点通过SafepointSynchronize::begin方法实现,退出安全点通过SafepointSynchronize::end方法实现,这两个方法的调用方只有VMThread,即所有需要在安全点下执行的操作都是通过VMThread完成的,VMThread为了减少进入安全点的次数,会在进入安全点后一次性的将多个已提交的需要在安全点下执行的操作执行完成。

处于不同状态的线程,其进入安全点的实现不一样,主要有以下几种:

  • 处于解释执行中,进入安全点时会通知解释器替换字节码路由表实现, 开始执行下一个字节码时会检查安全点的状态,如果需要进入安全点则阻塞当前线程,退出安全点时会通知解释器替换成正常的字节码路由表实现,不检查安全点状态
  • 处于本地代码执行中,即正在执行JNI方法,当Java线程从JNI方法中退出准备切换线程的状态时会检查安全点的状态,如果需要进入安全点则阻塞当前线程,退出安全点后恢复正常执行
  • 处于编译执行中,编译代码在适当的位置上会读取Safepoint Polling内存页,如果需要进入安全点则会将该内存页置为不可访问状态,此时Java线程访问该内存页,Linux内核就会发送一个特定的信号给JVM,JVM中负责监听处理信号的本地线程识别到是该内存页不可访问触发的异常,会检查安全点的状态,如果需要进入安全点则阻塞当前线程
  • 处于JVM执行中,即JVM执行上述三种状态的切换时,JVM会在切换前检查安全点的状态
  • 处于阻塞状态,即等待某个锁或者准备释放某个锁,锁状态变更时会检查是否需要进入安全点,如果是则会阻塞当前线程直到JVM从安全点退出
  • 所有阻塞的线程其实就是等待获取Threads_lock锁,该锁由VMThread在进入SafepointSynchronize::begin方法后占用,在SafepointSynchronize::end执行快完后释放该锁,一旦释放该锁,所有等待获取该锁的线程就会陆陆续续获取锁并恢复正常执行。

各JavaThread都是陆陆续续进入安全点的,SafepointSynchronize::begin方法会获取总的需要进入阻塞状态的线程数,然后不断的循环判断各个线程的状态,如果线程已经进入阻塞状态则将其计数减1,直到计数变成0,所有线程都进入安全点了,SafepointSynchronize::begin方法执行完成,开始执行VMThread的特定VM_Operation,执行完成后调用SafepointSynchronize::end退出安全点。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容