Hotspot CMS算法實現總結 (一)

     目錄

一、數據結構

1、Java堆

2、CodeCache

3、MetaSpace

二、關鍵類/方法

1、oop/Klass

2、oop_iterate / adjust_pointers / follow_contents

3、ReferenceProcessor

4、markOop / promote / forward

5、GC_locker 和JNI關鍵區

6、VM_Operation / VMThread / ConcurrentMarkSweepThread

7、BarrierSet / CardTableRS

8、markBitMap / modUnionTable / CMSBitMap

 9、根節點oop


       之前的多篇博客已經陸陸續續將CMS算法的實現所依賴的相關類都講解了一遍,雖然還是有很多的細節需要進一步的補充,但是整體的實現思路和框架已經很清晰了,本篇博客就對其做一個整體的回顧與總結。

一、數據結構

     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的類繼承關係如下:

在默認配置下,年輕代的實現類就是DefNewGeneration,如果UseParNewGC爲true,則變成ASParNewGeneration;老年代的實現類就是TenuredGeneration,如果UseConcMarkSweepGC爲true,則變成ASConcurrentMarkSweepGeneration。注意UseAdaptiveSizePolicy默認爲true,表示根據堆內存的實際使用情況和GC耗時等動態調整Generation的內存容量。

Generation的實現是基於Space的,Space負責實際的內存管理,其類繼承關係如下:

其中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如下圖:

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中,其類繼承關係如下:

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中。具體分爲五種情形:

  1. 對於普通的Java類的實例屬性,爲了方便找到所有的引用類型屬性,所有的引用類型屬性是挨着一起放在oopDesc內存的後面,並且通過Klass的內部類OopMapBlock來記錄起始偏移量和引用類型屬性的個數,因爲引用類型屬性在指針壓縮下固定佔用4個字節,非指針壓縮下固定佔用8個字節,所以只要知道了起始偏移量和引用類型屬性,就可以順序讀取各引用類型屬性對應的4字節或者8字節的數據,裏面就保存着所引用的對象地址。注意,如果該類存在父類,則有多少個父類就會增加多少個OopMapBlock,父類的OopMapBlock記錄父類的引用類型屬性的起始偏移量和個數,子類只記錄只屬於子類的引用類型屬性的起始偏移量和個數。這樣做是因爲子類實例會保存父類的所有屬性,並且子類的屬性排在父類屬性的後面,從而保證子類實例調用父類的某個方法時可以正確訪問父類的屬性。
  2. 對於對象類型數組,對象數組實例本身記錄了數組長度和用於存儲數組元素的基地址,所以從基地址開始依次讀取數組長度個4字節或者8字節數據即可完成所有引用類型屬性的遍歷
  3. 對於普通的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類本身包含的屬性計算出來的,是一個固定值。
  4. 對於java.lang.ClassLoader類及其子類實例,除遍歷其引用類型屬性外,還需要遍歷關聯的ClassLoaderData實例,該類記錄了該ClassLoader加載的所有Klass。
  5. 對於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個方法:

  1. discover_reference:負責將某個Reference實例加入到對應類型的Reference實例鏈表上,該鏈表是通過Reference實例的discovered屬性構成的,discovered屬性記錄了鏈表中下一個Reference實例的對象地址,注意如果discover被禁止了,該Reference實例不是Active狀態,Reference實例不在關聯的Generation內存區域中,Reference實例的referent對象還有其他強引用,是軟引用但是當前不需要清理或者discovered屬性不爲空則直接返回false,表示沒有加入到鏈表中。該方法是在oop_iterate引用遍歷的時候調用的,藉助多態自動識別出Reference實例,然後做特殊處理。
  2. process_discovered_references:負責處理加入到Reference實例鏈表中的所有Reference實例,滿足以下兩個條件則將其referent對象都作爲存活對象處理,將該Reference實例從鏈表中移除:對於SoftReference實例,referent對象是死的但是不需要清理;對於其他類型的Reference實例,referent對象是存活的。對於鏈表中剩下的實例,如果不需要將referent對象置爲null,則將其作爲存活對象處理,注意此時並不會將Reference實例從鏈表中移除。只有PhantomReference和FinalReference不需要置爲null,因爲referent對象後面還需要使用。注意本方法還會處理JNI弱引用,如果引用對象是存活的,則將其作爲存活對象處理否則置爲null。
  3. enqueue_discovered_references:用於將各個DiscoveredList中剩餘的Reference實例加入到由靜態屬性pending和實例屬性discovered構成的一個鏈表中並更新靜態pending屬性指向最新的鏈表頭,更新Reference實例的next屬性指向它自己,即將Reference實例標記成Pending狀態,處理完成後再將DiscoveredList恢復成初始狀態
  4. 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字節的數據,通過不同的位來描述對象的狀態,源碼註釋如下:

其中最後的兩位即lock有4個值,如下:

 

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,其類繼承關係如下:

    每個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字節的內存區域,修改對象引用類型屬性或者修改引用類型數組時會將對應的寫入地址轉換成對應的卡表項,將該卡表項標記爲髒的,其映射邏輯如下:

其中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方法中,主要包含以下幾種根節點:

  1. Java線程解釋執行時調用棧幀中保存的oop,每個Java方法對應一個調用棧幀,對應一個本地變量表,可通過本地遍歷表找到其包含的oop,每個棧幀都保存了上一個棧幀的地址,即該方法的返回地址,可藉此實現所有一個Java線程所有棧幀的遍歷
  2. Java線程編譯執行時,雖然解釋執行和編譯執行的底層都是彙編代碼,但是解釋執行時每個字節碼對應的執行彙編代碼是固定的,其棧幀結構是固定的;編譯執行的彙編代碼是編譯器生成的,同一個方法在不同編譯級別下產生的彙編代碼可能不一樣,因此編譯器生成的彙編代碼通過一個單獨的OopMap記錄棧幀中包含的oop,用來保存彙編代碼的CodeBlob通過OopMapSet來保存所有的OopMap,可通過棧幀的基地址獲取對應的OopMap,然後完成其中包含的oop遍歷
  3. 每個ClassLoader實例都對應一個ClassLoaderData,後者保存了前者加載的所有Klass,加載過程中的依賴和常量池引用,ClassLoader實例本身,其加載的所有Klass的java_mirror屬性即對應類的class實例,常量池引用如某個字符串對應的String實例,依賴的oop都是根節點
  4. Universe中包含的公用的異常oop如_out_of_memory_error_java_heap等,基礎類型,各種類型數組對應的class實例如int.class等
  5. JNIHandles中包含的全局JNI引用oop,每個Java線程包含的本地JNI引用oop在遍歷Java線程的oop時會遍歷
  6. ObjectSynchronizer中維護的與監視器鎖關聯的oop
  7. Management中維護的 java.lang.management API使用的用於記錄內存使用情況,線程的運行情況的oop
  8. SystemDictionary中包含的系統類加載器實例,java.security.ProtectionDomain實例和各方法的符號引用

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章