日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

G1的基本概念(G1源码分析和调优读书笔记)

發布時間:2023/12/20 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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%時不需要動態拓展。

size_t G1CollectorPolicy::expansion_amount() { //根據歷史信息獲取平均GC時間double recent_gc_overhead = recent_avg_pause_time_ratio() * 100.0;double threshold = _gc_overhead_perc;//G1 GC時間與應用時間占比超過閾值才需要動態擴展//這個閾值的值為10% 上文提過計算方式if (recent_gc_overhead > threshold) {// We will double the existing space, or take// G1ExpandByPercentOfAvailable % of the available expansion// space, whichever is smaller, bounded below by a minimum// expansion (unless that's all that's left.)const size_t min_expand_bytes = 1*M;size_t reserved_bytes = _g1->max_capacity();size_t committed_bytes = _g1->capacity();size_t uncommitted_bytes = reserved_bytes - committed_bytes;size_t expand_bytes;size_t expand_bytes_via_pct =uncommitted_bytes * G1ExpandByPercentOfAvailable / 100;expand_bytes = MIN2(expand_bytes_via_pct, committed_bytes);expand_bytes = MAX2(expand_bytes, min_expand_bytes);expand_bytes = MIN2(expand_bytes, uncommitted_bytes);......} else {return 0;} } //G1內存拓展時間書后面部分會介紹

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標記信息等。

class oopDesc {friend class VMStructs;private:volatile markOop _mark;union _metadata {Klass* _klass;narrowKlass _compressed_klass;} _metadata;//靜態變量用于快速訪問BarrierSetstatic BarrierSet* _bs;

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對象,還有其他的對象也在堆中。

mtNone = 0x0000, // undefinedmtClass = 0x0100, // JVM中java類mtThread = 0x0200, // JVM中線程對象mtThreadStack = 0x0300,mtCode = 0x0400, // JVM中生成的編譯代碼mtGC = 0x0500, // GC的內存mtCompiler = 0x0600, // 編譯器使用的內存mtInternal = 0x0700, // JVM中內部使用的類型,不屬于上述類型。mtOther = 0x0800, // 不是由JVM使用的內存mtSymbol = 0x0900, //符號表使用內存mtNMT = 0x0A00, // mNMT使用內存mtChunk = 0x0B00, // chunk用于緩存mtJavaHeap = 0x0C00, // Java 堆mtClassShared = 0x0D00, // 共享類數據mtTest = 0x0E00, // Test type for verifying NMTmtTracing = 0x0F00, // memory used for Tracingmt_number_of_types = 0x000F, // number of memory types (mtDontTrack// is not included as validate type)mtDontTrack = 0x0F00, // memory we do not or cannot trackmt_masks = 0x7F00,

線程

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源码分析和调优读书笔记)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。