天天看點

JVM系列(記憶體管理二)_HotSpot虛拟機對象建立詳解

3. HotSpot虛拟機對象探秘

3.1 對象的建立

Java是一門面向對象的程式設計語言,在Java程式運作過程中無時無刻都有對象被建立出來。在語言層面上,建立對象(例如克隆、反序列化)通常僅僅是一個new關鍵字而已,而在虛拟機中,對象(文中讨論的對象限于普通Java對象,不包括數組和Class對象等)的建立又是怎樣一個過程呢?

(1) 判斷對象對應的類是否加載、連結、初始化

虛拟機遇到一條new指令時,首先将去檢查這個指令的參數是否能在常量池中定位到一個類的符号引用,并且檢查這個符号引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。

(2) 為對象配置設定記憶體

在類加載檢查通過後,接下來虛拟機将為新生對象配置設定記憶體。對象所需記憶體的大小在類加載完成後便可完全确定,為對象配置設定空間的任務等同于把一塊确定大小的記憶體從Java堆中劃分出來。假設Java堆中記憶體是絕對規整的,所有用過的記憶體都放在一邊,空閑的記憶體放在另一邊,中間放着一個指針作為分界點的訓示器,那所配置設定記憶體就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種配置設定方式稱 為“指針碰撞”(Bump the Pointer)。如果Java堆中的記憶體并不是規整的,已使用的記憶體和空閑的記憶體互相交錯,那就沒有辦法簡單地進行指針碰撞了,虛拟機就必須維護一個清單,記錄上哪些記憶體塊是可用的,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體, 并更新清單上的記錄,這種配置設定方式稱為“空閑清單”(Free List)。選擇哪種配置設定方式由 Java堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。是以,在使用Serial、ParNew等帶Compact過程的收集器時,系統采用的配置設定算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時,通常采用空閑清單。

(3) 處理并發安全問題

除如何劃分可用空間之外,還有另外一個需要考慮的問題是對象建立在虛拟機中是非常頻繁的行為,即使是僅僅修改一個指針所指向的位置,在并發情況下也并不是線程安全的, 可能出現正在給對象A配置設定記憶體,指針還沒來得及修改,對象B又同時使用了原來的指針來配置設定記憶體的情況。解決這個問題有兩種方案,一種是對配置設定記憶體空間的動作進行同步處理 ——實際上虛拟機采用CAS配上失敗重試的方式保證更新操作的原子性;另一種是把記憶體配置設定的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先配置設定一小塊記憶體,稱為本地線程配置設定緩沖(Thread Local Allocation Buffer,TLAB)。哪個線程要配置設定記憶體,就在哪個線程的TLAB上配置設定,隻有TLAB用完并配置設定新的TLAB時,才需要同步鎖定。 虛拟機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。

(4) 初始化配置設定到的記憶體空間

記憶體配置設定完成後,虛拟機需要将配置設定到的記憶體空間都初始化為零值(不包括對象頭), 如果使用TLAB,這一工作過程也可以提前至TLAB配置設定時進行。這一步操作保證了對象的執行個體字段在Java代碼中可以不賦初始值就直接使用,程式能通路到這些字段的資料類型所對應的零值。

(5) 設定對象的對象頭

接下來,虛拟機要對對象進行必要的設定,例如這個對象是哪個類的執行個體、如何才能找到類的中繼資料資訊、對象的哈希碼、對象的GC分代年齡等資訊。這些資訊存放在對象的對象頭(Object Header)之中。根據虛拟機目前的運作狀态的不同,如是否啟用偏向鎖等,對象頭會有不同的設定方式。

(6)執行init方法進行初始化

在上面工作都完成之後,從虛拟機的視角來看,一個新的對象已經産生了,但從Java程式的視角來看,對象建立才剛剛開始——<init>方法還沒有執行,所有的字段都還為零。 是以,一般來說(由位元組碼中是否跟随invokespecial指令所決定),執行new指令之後會接着執行<init>方法,把對象按照程式員的意願進行初始化,這樣一個真正可用的對象才算完全産生出來。

下面的代碼是HotSpot虛拟機bytecodeInterpreter.cpp中的代碼片段(這個解釋器實作很少有機會實際使用,因為大部分平台上都使用模闆解釋器;當代碼通過JIT編譯器執行時差異就更大了。不過,這段代碼用于了解HotSpot的運作過程是沒有什麼問題的)。

HotSpot解釋器的代碼片段:

//確定常量池中存放的是已解釋的類
 if(!constants->tag_at(index).is_unresolved_klass()){ 
	 //斷言確定是klassOop和instanceKlassOop(這部分下一節介紹) 
	 oop entry=(klassOop)
	 *constants->obj_at_addr(index); 
	 assert(entry->is_klass(),"Should be resolved klass"); 
	 klassOop k_entry=(klassOop)entry; 
	 assert(k_entry->klass_part()->oop_is_instance(),"Should be instanceKlass"); 
	 instanceKlass * ik=(instanceKlass*)k_entry->klass_part(); 
	 //確定對象所屬類型已經經過初始化階段 
	 if(ik->is_initialized()&&ik->can_be_fastpath_allocated()) { 
		 //取對象長度 size_t obj_size=ik->size_helper(); 
		oop result=NULL; 
		//記錄是否需要将對象所有字段置零值 bool need_zero=!ZeroTLAB;
		//是否在TLAB中配置設定對象
		if(UseTLAB){
			result=(oop)THREAD->tlab().allocate(obj_size);
		}
		if(result==NULL){
			need_zero=true;
			//直接在eden中配置設定對象 retry: HeapWord 
			* compare_to = *Universe:heap()->top_addr(); 
			HeapWord * new_top = compare_to + obj_size;
			/*
			* cmpxchg是x86中的CAS指令,這裡是一個C++方法,通過CAS方式配置設定空間,如果并發失敗, 轉到retry中重試,直至成功配置設定為止
			*/ 
			if(new_top<=*Universe:heap()->end_addr()){
				if(Atomic:cmpxchg_ptr(new_top,Universe:heap()->top_addr(),compare_to)!=compare_to){ 
					goto retry; 
				} 
				result=(oop)compare_to; 
			} 
		} 
		if(result!=NULL){
			//如果需要,則為對象初始化零值 
			if(need_zero){
				HeapWord * to_zero=(HeapWord*)result+sizeof(oopDesc)/oopSize;
				obj_size-=sizeof(oopDesc)/oopSize;
				if(obj_size>0){ 
					memset(to_zero,0,obj_size * HeapWordSize); 
				}
			} 
			//根據是否啟用偏向鎖來設定對象頭資訊 
			if(UseBiasedLocking){ 
				result->set_mark(ik->prototype_header());
			}else{
				result->set_mark(markOopDesc:prototype());
			} 
			result->set_klass_gap(0); 
			result->set_klass(k_entry); 
			//将對象引用入棧,繼續執行下一條指令 
			SET_STACK_OBJECT(result,0);
			UPDATE_PC_AND_TOS_AND_CONTINUE(3,1); 
		}
	}
}
           

3.2 對象的記憶體布局

在HotSpot虛拟機中,對象在記憶體中存儲的布局可以分為3塊區域:對象頭(Header)、 執行個體資料(Instance Data)和對齊填充(Padding)。

(1) HotSpot虛拟機的對象頭包括兩部分資訊,第一部分用于存儲對象自身的運作時資料, 如哈希碼(HashCode)、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分資料的長度在32位和64位的虛拟機(未開啟壓縮指針)中分别為32bit和 64bit,官方稱它為“Mark Word”。對象需要存儲的運作時資料很多,其實已經超出了32位、 64位Bitmap結構所能記錄的限度,但是對象頭資訊是與對象自身定義的資料無關的額外存儲成本,考慮到虛拟機的空間效率,Mark Word被設計成一個非固定的資料結構以便在極小的空間記憶體儲盡量多的資訊,它會根據對象的狀态複用自己的存儲空間。例如,在32位的 HotSpot虛拟機中,如果對象處于未被鎖定的狀态下,那麼Mark Word的32bit空間中的25bit用于存儲對象哈希碼,4bit用于存儲對象分代年齡,2bit用于存儲鎖标志位,1bit固定為0,而在其他狀态(輕量級鎖定、重量級鎖定、GC标記、可偏向)下對象的存儲内容見下表。

JVM系列(記憶體管理二)_HotSpot虛拟機對象建立詳解

對象頭的另外一部分是類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體。并不是所有的虛拟機實作都必須在對象資料上保留類型指針,換句話說,查找對象的中繼資料資訊并不一定要經過對象本身。 另外,如果對象是一個Java數組,那在對象頭中還必須有一塊用于記錄數組長度的資料,因為虛拟機可以通過普通Java對象的中繼資料資訊确定Java對象的大小,但是從數組的中繼資料中 卻無法确定數組的大小。

下面代碼為HotSpot虛拟機markOop.cpp中的代碼(注釋)片段,它描述了32bit下Mark Word的存儲狀态。

代碼markOop.cpp片段:

// Bit-format of an object header(most significant first,big endian layout below):

// 32 bits:

// -------//hash:25------------>|age:4 biased_lock:1 lock:2(normal object)

// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2(biased object)

// size:32------------------------------------------>|(CMS free block)

// PromotedObject*:29---------->|promo_bits:3----->|(CMS promoted object)

(2) 接下來的執行個體資料部分是對象真正存儲的有效資訊,也是在程式代碼中所定義的各種類型的字段内容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。這部分的存儲順序會受到虛拟機配置設定政策參數(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。HotSpot虛拟機預設的配置設定政策為longs/doubles、ints、shorts/chars、 bytes/booleans、oops(Ordinary Object Pointers),從配置設定政策中可以看出,相同寬度的字段總是被配置設定到一起。在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。如果CompactFields參數值為true(預設為true),那麼子類之中較窄的變量也可能會插入到父類變量的空隙之中。

(3) 第三部分對齊填充并不是必然存在的,也沒有特别的含義,它僅僅起着占位符的作用。 由于HotSpot VM的自動記憶體管理系統要求對象起始位址必須是8位元組的整數倍,換句話說, 就是對象的大小必須是8位元組的整數倍。而對象頭部分正好是8位元組的倍數(1倍或者2倍), 是以,當對象執行個體資料部分沒有對齊時,就需要通過對齊填充來補全。

3.3 對象的通路定位

建立對象是為了使用對象,Java程式通過棧上的reference資料來操作堆上的具體對象。由于reference類型在Java虛拟機規範中隻規定了一個指向對象的引用,并沒有定義這個引用應該通過何種方式去定位、通路堆中的對象的具體位置,是以對象通路方式也是取決于虛拟機實作而定的。目前主流的通路方式有使用句柄和直接指針兩種。

(1) 如果使用句柄通路的話,那麼Java堆中将會劃分出一塊記憶體來作為句柄池,reference中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊,如圖所示。

JVM系列(記憶體管理二)_HotSpot虛拟機對象建立詳解

(2) 如果使用直接指針通路,那麼Java堆對象的布局中就必須考慮如何放置通路類型資料的 相關資訊,而reference中存儲的直接就是對象位址,如圖所示。

JVM系列(記憶體管理二)_HotSpot虛拟機對象建立詳解

這兩種對象通路方式各有優勢

  1. 使用句柄來通路的最大好處就是reference中存儲的是穩定的句柄位址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時隻會改變句柄中的執行個體資料指針,而reference本身不需要修改。
  2. 使用直接指針通路方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷, 由于對象的通路在Java中非常頻繁,是以這類開銷積少成多後也是一項非常可觀的執行成本。就本書讨論的主要虛拟機Sun HotSpot而言,它是使用第二種方式進行對象通路的,但從整個軟體開發的範圍來看,各種語言和架構使用句柄來通路的情況也十分常見。
參考書籍: <<深入了解Java虛拟機>> 周志明