G1的基本概念(G1源码分析和调优读书笔记)
G1的基本概念
分區
分區(Heap Region, HR)或稱堆分區,是G1堆和操作系統交互的最小管理單位。
G1的分區類型大致可以分為四類:
1.自由分區
2.新生代分區
3.大對象分區
4.老生代分區
其中新生代分區又可以分為Eden和Survivor;大對象分區又可以分為:大對象頭分區和大對象連續分區。
堆分區默認大小計算方式 ↓
// 判斷是否是設置過堆分區大小,如果有則使用; //沒有,則根據初始內存和最大分配內存,獲得平均值,并根據HR的個數得到分區的大小,和分區的下限比較,取兩者的最大值。 void HeapRegion::setup_heap_region_size(size_t initial_heap_size, size_t max_heap_size) {uintx region_size = G1HeapRegionSize;if (FLAG_IS_DEFAULT(G1HeapRegionSize)) {size_t average_heap_size = (initial_heap_size + max_heap_size) / 2;region_size = MAX2(average_heap_size / TARGET_REGION_NUMBER,(uintx) MIN_REGION_SIZE);}//對region_size按2的冪次對齊,并且保證其落在上下限范圍內。int region_size_log = log2_long((jlong) region_size);// Recalculate the region size to make sure it's a power of// 2. This means that region_size is the largest power of 2 that's// <= what we've calculated so far.region_size = ((uintx)1 << region_size_log);//確保region_size落在[1MB,32MB]之間// Now make sure that we don't go over or under our limits.if (region_size < MIN_REGION_SIZE) {region_size = MIN_REGION_SIZE;} else if (region_size > MAX_REGION_SIZE) {region_size = MAX_REGION_SIZE;}// 根據region_size 計算一些變量,比如卡表大小// And recalculate the log.region_size_log = log2_long((jlong) region_size);// Now, set up the globals.guarantee(LogOfHRGrainBytes == 0, "we should only set it once");LogOfHRGrainBytes = region_size_log;guarantee(LogOfHRGrainWords == 0, "we should only set it once");LogOfHRGrainWords = LogOfHRGrainBytes - LogHeapWordSize;guarantee(GrainBytes == 0, "we should only set it once");// The cast to int is safe, given that we've bounded region_size by// MIN_REGION_SIZE and MAX_REGION_SIZE.GrainBytes = (size_t)region_size;guarantee(GrainWords == 0, "we should only set it once");GrainWords = GrainBytes >> LogHeapWordSize;guarantee((size_t) 1 << LogOfHRGrainWords == GrainWords, "sanity");guarantee(CardsPerRegion == 0, "we should only set it once");CardsPerRegion = GrainBytes >> CardTableModRefBS::card_shift;按照默認值計算,G1可以管理的最大內存為
2048 X 32MB =64GB。假設設置xms=32G,xmx=128G,則每個堆分區的大小為32M,分區個數動態變化范圍從1024到4096個。
region_size的一半以上的大對象直接進入老生代。
新生代大小
新生代大小指的是新生代內存空間的大小,前面提到的G1新生代大小按分區組織,首先需要計算整個新生代的大小。
如果G1推斷出最大值和最小值相等,那么說明新生代不會動態變化,即代表G1在后續對新生代垃圾回收的時候可能不滿足期望停頓的時間。
//初始化新生代大小參數,根據不同的jvm參數判斷計算新生代大小,供后續使用。 G1YoungGenSizer::G1YoungGenSizer() : _sizer_kind(SizerDefaults), _adaptive_size(true),_min_desired_young_length(0), _max_desired_young_length(0) { //如果設置了NewRatio且同時設置NewSize或MaxNewSize的情況下,則NewRatio被忽略 if (FLAG_IS_CMDLINE(NewRatio)) {if (FLAG_IS_CMDLINE(NewSize) || FLAG_IS_CMDLINE(MaxNewSize)) {warning("-XX:NewSize and -XX:MaxNewSize override -XX:NewRatio");} else {_sizer_kind = SizerNewRatio;_adaptive_size = false;return;}}//參數傳遞有問題,最小值大于最大值if (NewSize > MaxNewSize) {if (FLAG_IS_CMDLINE(MaxNewSize)) {warning("NewSize (" SIZE_FORMAT "k) is greater than the MaxNewSize (" SIZE_FORMAT "k). ""A new max generation size of " SIZE_FORMAT "k will be used.",NewSize/K, MaxNewSize/K, NewSize/K);}MaxNewSize = NewSize;}//根據參數計算分區個數if (FLAG_IS_CMDLINE(NewSize)) {_min_desired_young_length = MAX2((uint) (NewSize / HeapRegion::GrainBytes),1U);if (FLAG_IS_CMDLINE(MaxNewSize)) {_max_desired_young_length =MAX2((uint) (MaxNewSize / HeapRegion::GrainBytes),1U);_sizer_kind = SizerMaxAndNewSize;_adaptive_size = _min_desired_young_length == _max_desired_young_length;} else {_sizer_kind = SizerNewSizeOnly;}} else if (FLAG_IS_CMDLINE(MaxNewSize)) {_max_desired_young_length =MAX2((uint) (MaxNewSize / HeapRegion::GrainBytes),1U);_sizer_kind = SizerMaxNewSizeOnly;} }//使用G1NewSizePercent來計算新生代的最小值 uint G1YoungGenSizer::calculate_default_min_length(uint new_number_of_heap_regions) {uint default_value = (new_number_of_heap_regions * G1NewSizePercent) / 100;return MAX2(1U, default_value); }//使用G1MaxNewSizePercent來計算新生代的最大值 uint G1YoungGenSizer::calculate_default_max_length(uint new_number_of_heap_regions) {uint default_value = (new_number_of_heap_regions * G1MaxNewSizePercent) / 100;return MAX2(1U, default_value); }//這里根據不同的參數輸入來計算大小 //recalculate_min_max_young_length在初始化時被調用,在堆空間改變時也會被調用 void G1YoungGenSizer::recalculate_min_max_young_length(uint number_of_heap_regions, uint* min_young_length, uint* max_young_length) {assert(number_of_heap_regions > 0, "Heap must be initialized");switch (_sizer_kind) {case SizerDefaults:*min_young_length = calculate_default_min_length(number_of_heap_regions);*max_young_length = calculate_default_max_length(number_of_heap_regions);break;case SizerNewSizeOnly:*max_young_length = calculate_default_max_length(number_of_heap_regions);*max_young_length = MAX2(*min_young_length, *max_young_length);break;case SizerMaxNewSizeOnly:*min_young_length = calculate_default_min_length(number_of_heap_regions);*min_young_length = MIN2(*min_young_length, *max_young_length);break;case SizerMaxAndNewSize:// Do nothing. Values set on the command line, don't update them at runtime.break;case SizerNewRatio:*min_young_length = number_of_heap_regions / (NewRatio + 1);*max_young_length = *min_young_length;break;default:ShouldNotReachHere();}另一個問題,分配新的分區時何時拓展,一次拓展多少內存?
G1是自適應拓展空間的。
參數-XX:GCTimeRatio表示GC與應用耗費時間比,G1中默認為9,計算方式為_gc_overhead_perc = 100.0x(1.0/(1.0+GCTimeRatio)),即G1 GC時間與應用時間占比不超過10%時不需要動態拓展。
G1停頓預測模型
G1是一個響應優先的GC算法,用戶可以設定期望停頓時間由參數MaxGCPauseMills控制,默認值為200ms。
G1會在這個目標停頓時間內完成垃圾回收的工作。
G1使用停頓預測模型來滿足期望,預測邏輯基于衰減平均值和衰減標準差。
卡表和位圖
GC最早引入卡表是為了對內存的引用關系做標記,從而根據引用關系快速遍歷活躍對象。
可以借助位圖的方式,記錄內存塊之間的引用關系。用一個位來描述一個字,我們只需要判定位圖里面的位是否有1,有的話則認為發生了引用。
以位為粒度的位圖能準確描述每一個字的引用關系,但是包含信息太少,只能描述兩個狀態:引用和未被引用。但是如果增加一個字節來描述狀態,則位圖需要256kb的空間,這個數字太大,開銷占了25%。所以一個可能的做法是位圖不再描述一個字,而是一個區域,JVM使用512字節作為單位,用一個字節描述512字節的引用關系。
G1中還使用了bitmap,用bitmap可以描述一個分區對另外一個分區的引用情況,也可以描述內存分配的情況。
并發標記時也使用了bitmap來描述對象的分配情況。
對象頭
java代碼首先被翻譯成字節碼(bytecode),在JVM執行時才能確定要執行函數的地址,如何實現java的多態調用,最直觀的想法是把java對象映射成C++對象或者封裝成C++對象,比如增加一個額外的對象頭,里面指向一個對象,而這個對象存儲了java代碼的地址。
所以JVM設計了對象的數據結構來描述java對象,這個結構分為三塊區域:對象頭 、實例數據和對齊填充 。
而我們剛才提到的類似虛指針的東西就可以放在對象頭中,而JVM設計者還利用對象頭來描述更多信息,對象的鎖信息、GC標記信息等。
1.標記信息
第一部分標記信息位于MarkOop。
以下三種情況時要保存對象頭:
1.使用了偏向鎖,并且偏向鎖被設置了
2.對象被加鎖了
3.對象被設置了hash_code
2.元數據信息
第二部分元數據信息字段指向的是Klass對象(Klass對象是元數據對象,如Instance Klass 描述java對象的類結構),這個字段也和垃圾回收有關系。
內存分配和管理
JVM通過操作系統的系統調用進行內存的申請,典型的就是mmap。
mmap使用PAGE_SIZE為單位來進行映射,而內存也只能以頁為單位進行映射,若要映射非PAGE_SIZE整數倍的地址范圍,要先進行內存對齊,強行映射。
操作系統對內存的分配管理典型的分為兩個階段:
保留和提交。
保留階段告知系統從某一地址開始到后面的dwSize大小的連續虛擬內存需要供程序使用,進程其他分配內存的操作不得使用這段內存;
提交階段將虛擬地址映射到對應的真實物理地址中,這樣這塊內存就可以正常使用。
JVM常見對象類型
ResourceObj:線程有一個資源空間,一般ResourceObj都位于這里。定義資源空間的目的是對JVM其他功能的支持,如CFG、在C1/C2優化時可能需要訪問運行時信息(這些信息可以保存在線程的資源區)。
StackObj:棧對象,聲明的對象使用棧管理。其實例對象并不提供任何功能,且禁止New/Delete操作。對象分配在線程棧中,或者使用自定義的棧容器進行管理。
ValueObj:值對象,該對象在堆對象需要進行嵌套時使用,簡單地說就是對象分配的位置和宿主對象(即擁有)是一樣的。
AllStatic: 靜態對象,全局對象,只有一個。值得一提的是C++初始化沒有通過規范保證,可能會有兩個靜態對象相互依賴的問題,初始化時可能會出錯。JVM中很多靜態對象初始化都是顯示調用靜態初始化函數。
MetaspaceObj: 元對象,比如InstanceKlass這樣的元數據就是元對象。
CHeapObj:
這是堆空間的對象,由new/delete/free/malloc管理。其中包含的內容很多,比如java對象、InstanceOop(后面提到的G1對象分配出來的對象)。除了Java對象,還有其他的對象也在堆中。
線程
JVM線程圖 如上
JavaThread:就是要執行Java代碼的線程,比如Java代碼的啟動會創建一個JavaThread運行;對于Java代碼的啟動,可以通過JNI_CreateJavaVM來創建一個JavaThread,而對于一般的Java線程,都是調用java.lang.thread中的start方法,這個方法通過JNI調用創建JavaThread對象,完成真正的線程創建。
CompilerThread:執行JIT的線程。
WatcherThread:執行周期性任務,JVM里面有很多周期性任務,例如內存管理中對小對象使用了ChunkPool,而這種管理需要周期性的清理動作Cleaner;JVM中內存抽樣任務MemProf?ilerTask等都是周期性任務。
NameThread:是JVM內部使用的線程,分類如圖2-1所示。
VMThread:JVM執行GC的同步線程,這個是JVM最關鍵的線程之一,主要是用于處理垃圾回收。簡單地說,所有的垃圾回收操作都是從VMThread觸發的,如果是多線程回收,則啟動多個線程,如果是單線程回收,則使用VMThread進行。
VMThread提供了一個隊列,任何要執行GC的操作都實現了VM_GC_Operation,在JavaThread中執行VMThread::execute(VM_GC_Operation)把GC操作放入到隊列中,然后再用VMThread的run方法輪詢這個隊列就可以了。
當這個隊列有內容的時候它就開始嘗試進入安全點,然后執行相應的GC任務,完成GC任務后會退出安全點
ConcurrentGCThread:并發執行GC任務的線程,比如G1中的ConcurrentMark
Thread和ConcurrentG1RefineThread,分別處理并發標記和并發Refine,這兩個線程將在混合垃圾收集和新生代垃圾回收中介紹。
WorkerThread:
工作線程,在G1中使用了FlexibleWorkGang,這個線程是并行執行的(個數一般和CPU個數相關),所以可以認為這是一個線程池。
線程池里面的線程是為了執行任務(在G1中是G1ParTask),也就是做GC工作的地方。VMThread會觸發這些任務的調度執行(其實是把G1ParTask放入到這些工作線程中,然后由工作線程進行調度)。
JVM線程狀態:
//新創建線程 case NEW : return "NEW"; //可運行或者正在運行 case RUNNABLE : return "RUNNABLE"; //調用Thread.sleep()進入睡眠 case SLEEPING : return "TIMED_WAITING (sleeping)"; //調用Object.wait()進入等待 case IN_OBJECT_WAIT : return "WAITING (on object monitor)"; //調用Object.wait(long)進入等待且有過期時間 case IN_OBJECT_WAIT_TIMED : return "TIMED_WAITING (on object monitor)"; //JVM內部調用LockSupport.park()進入等待 case PARKED : return "WAITING (parking)"; //JVM內部調用LockSupport.park()進入等待,且有過期時間 case PARKED_TIMED : return "TIMED_WAITING (parking)"; //進入一個同步塊 case BLOCKED_ON_MONITOR_ENTER : return "BLOCKED (on object monitor)"; //終止 case TERMINATED : return "TERMINATED"; default : return "UNKNOWN";操作系統的線程狀態:
ALLOCATED, // 分配了但未初始化INITIALIZED, // 初始化完未啟動RUNNABLE, // 已經啟動并可被執行或者正在運行MONITOR_WAIT, // 等待一個MonitorCONDVAR_WAIT, // 等待一個條件變量OBJECT_WAIT, // 通過調用Object.wait()等待對象BREAKPOINTED, //調式狀態SLEEPING, // 通過Thread.sleep()進入睡眠ZOMBIE // 僵尸狀態,等待回收棧幀
棧幀(frame)在線程執行時和運行過程中用于保存線程的上下文數據,JVM設計了棧幀,這是垃圾回收中國最重要的根,棧幀的結構在不同的CPU中并不相同,在x86中代碼如下所示:
_pc = NULL;//程序計數器,指向下一個要執行的代碼地址_sp = NULL;//棧頂指針_unextended_sp = NULL;//異常棧頂指針_fp = NULL;//棧底指針_cb = NULL;//代碼塊的地址_deopt_state = unknown;//這個字段描述從編譯代碼到解釋代碼反優化的狀態棧幀也和GC密切相關,在GC過程中,通常第一步就是遍歷根,Java線程棧幀就是根元素之一,遍歷整個棧幀的方式是通過StackFrameStream,其中封裝了一個next指針,其原理和上述的代碼一樣通過sender來獲得調用者的棧幀。
我們將Java的棧幀來作為根遍歷堆,對對象進行標記并收集垃圾。
句柄
線程不但可以執行java代碼,也可以執行本地代碼(JVM里的代碼)。JVM沒有區分Java棧和本地方法棧,如果通過棧進行處理則必須要區分這兩種情況。
JVM設計了handleArea,這是一塊線程的資源區,在這個區域分配句柄并管理所有的句柄,如果函數還在調用中,那么句柄有效,句柄關聯的對象也就是活躍對象。
為了管理句柄的生命周期,引入了HandleMark,通常HandleMark分配在棧上,在創建HandleMark的時候標記handleArea對象有效,在HandleMark對象析構的時候從HandleArea中刪除對象的引用。
在HandleMark中標記Chunk的地址,這個就是找到當前本地方法代碼中活躍的句柄,因此也就可以找到對應的活躍的OOP對象。下面是HandleMark的構造函數和析構函數,它們的主要工作就是構建句柄鏈表,代碼如下所示:
總結
以上是生活随笔為你收集整理的G1的基本概念(G1源码分析和调优读书笔记)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 转载(工信部):启明信息技术股份有限公司
- 下一篇: 色彩敏感度测试 l 据说只有1%的设计师