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;
//靜態變數用於快速訪問BarrierSet
static 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, // undefined
mtClass = 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 NMT
mtTracing = 0x0F00, // memory used for Tracing
mt_number_of_types = 0x000F, // number of memory types (mtDontTrack
// is not included as validate type)
mtDontTrack = 0x0F00, // memory we do not or cannot track
mt_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, // 等待一個Monitor
CONDVAR_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垃圾回收器在併發場景調優
- 簡單有效的G1 GC調整技巧 - JAXenterGC
- 《CSS重構:樣式表效能調優》讀書筆記CSS筆記
- 探索G1垃圾回收器
- CMS、G1收集器
- FutureTask原始碼分析筆記原始碼筆記
- 原始碼分析筆記——OkHttp原始碼筆記HTTP
- Koa 原始碼閱讀筆記原始碼筆記
- CopyOnWriteArrayList原始碼閱讀筆記原始碼筆記
- ArrayList原始碼閱讀筆記原始碼筆記
- LinkedList原始碼閱讀筆記原始碼筆記
- python原始碼閱讀筆記Python原始碼筆記
- guavacache原始碼閱讀筆記Guava原始碼筆記
- LongAdder原始碼閱讀筆記原始碼筆記
- Java G1 垃圾收集器Java
- 讀書筆記-乾淨程式碼筆記
- 如何在Java 9以上的JVM中微調G1垃圾回收? - DZone效能JavaJVM
- [讀書筆記] Ruby 中的 Block 和 Iterator筆記BloC
- 讀書筆記筆記
- 讀書筆記2-記憶體優化篇筆記記憶體優化
- 《讀書與做人》讀書筆記筆記
- ThreadLocal原始碼解讀和記憶體洩露分析thread原始碼記憶體洩露
- 垃圾回收之G1收集過程
- iOS WebviewJavascriptBridge 原始碼研讀筆記iOSWebViewJavaScript原始碼筆記
- Express Session 原始碼閱讀筆記ExpressSession原始碼筆記
- 《夢斷程式碼》讀書筆記(二)筆記
- 夢斷程式碼讀書筆記(一)筆記
- 《程式碼大全》讀書筆記-構建的前期筆記
- 讀書筆記3-卡頓優化篇筆記優化
- 弄明白CMS和G1,就靠這一篇了
- Swift標準庫原始碼閱讀筆記 - Array和ContiguousArraySwift原始碼筆記
- webpackDemo讀書筆記Web筆記
- Vue讀書筆記Vue筆記
- 散文讀書筆記筆記
- Cucumber讀書筆記筆記
- HTTP 讀書筆記HTTP筆記
- postgres 讀書筆記筆記