CMS算法实现总结-1

源代码 2024-9-19 23:33:48 133 0 来自 中国
一、数据结构
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方法中,如下图:

GenCollectorPolicy的initialize_generations方法会指定包罗的Generation的范例,如下:

GenCollectedHeap会在初始化时调用GenerationSpec::init完成各Generation的初始化,着实现如下:
各Generation的类继续关系如下:
4.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负责实际的内存管理,其类继续关系如下:
5.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如下图:
6.png 2、CodeCache
CodeCache用于缓存差别范例的天生的汇编代码,如热门方法编译后的代码,各种运行时的调用入口Stub,每个字节码对应的实验代码,一些高频访问的数据函数和底层方法等。所有的汇编代码在CodeCache中都是以CodeBlob及其子类的情势存在的。通常CodeBlob会对应一个CodeBuffer,负责天生汇编代码的天生器会通过CodeBuffer将汇编代码写入到CodeBlob中,写入的起始地点就是该段汇编指令的调用地点。
CodeCache只是对外的接口而已,详细的内存管理都是CodeHeap实现的。从CodeHeap中分配CodeBlob时,待分配的CodeBlob的大小会按照segment的大小向上对齐,一个segment可以明确为一个内存页,是操纵体系分配内存的最小粒度,从而制止内存碎片。详细分配时会先在生存空闲内存块的List中查找,如果没有再按照雷同top指针移动的方式来分配一块新的内存块,如果剩余内存不敷则实验扩容,扩容乐成后再次实验分配。如果一个CodeBlob被开释了,则对应的空闲内存块会被归还到List中管理。其内存结构如下图:3、MetaSpace
MetaSpace比力特殊,一个ClassLoader实例对应一个MetaSpace,由该ClassLoader加载的类元数据都会从绑定的MetaSpace中分配内存,当ClassLoader实例被烧毁了,则对应的MetaSpace也会跟着开释;MetaSpace的底层并不是一个连续的地点空间,而是一个由多个VirtualSpaceNode组成的链表,每个VirtualSpaceNode都对应一段连续的地点空间,留意这个链表是多个MetaSpace实例共享的,其内存结构如下:
分配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负责的,其团体的类数据结构如下: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对象实例,用来生存类的实例属性,其类继续关系如下:
objArrayOopDesc表现对象数组大概多维数组,typeArrayOopDesc表现根本范例数组,instanceOopDesc就是平常的Java对象,markOopDesc表现Java对象中的对象头。
Klass用来生存每个类的元数据,好比类继续关系,类实例包罗的字段及其存储位置,类界说的所有方法,类界说中用到的注解等,class文件中包罗的所有信息都会生存Klass中,其类继续关系如下:
11.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字节的数据,通过差别的位来形貌对象的状态,源码解释如下:
12.png 此中末了的两位即lock有4个值,如下:

13.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,其类继续关系如下:
跟CMS干系的子类叫VM_CMS_Operation,其类继续关系如下:

15.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字节的内存地区,修改对象引用范例属性大概修改引用范例数组时会将对应的写入地点转换成对应的卡表项,将该卡表项标志为脏的,其映射逻辑如下:
16.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,其他的都是0static 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退出安全点。
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-11-22 05:14, Processed in 0.235884 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表