1. 程式人生 > >帶你讀深入理解Java虛擬機器

帶你讀深入理解Java虛擬機器

5年碼農一枚,一直在傳統行業,現在的工作輕鬆卻無趣,打算給自己3個月時間年前換個有挑戰性的工作。之前工作中沒有太注重理論知識的學習,對新技術也沒有深入瞭解。以此為界,從《深入理解Java虛擬機器》開始,以換高薪工作為目的,將自己這段時間所學記錄下來,作為一個總結。這裡會把書中知識點內容詳實記錄下來,方便以後檢視,對想讀此書卻遲遲拿不起書的童鞋也可以通過此快速瞭解書中內容且不會有大的遺漏。好了,備戰,學習結果如何,3個月後見分曉。越努力越幸運,加油!

第一章 走近Java

這一部分主要介紹了Java發展史和JDK編譯。Java發展史瞭解一下就好,況且讀完了真心沒記住啥,還是從後面乾貨開始走起。至於JDK編譯,是否按書上實操不影響後面的閱讀(書中介紹了Linux和Mac下的編譯,Windows附錄中有但可能需要自己研究一下),本人是個懶人,實操暫且跳過了,等通讀完本書需要讀JDK原始碼時再去搞。

第二章 Java記憶體區域與記憶體溢位異常

1. 記憶體區域

    Java虛擬機器執行Java程式過程中會把它管理的記憶體劃分為若干不同的資料區域。

圖1. Java虛擬機器執行時資料區

程式計數器

  • 執行緒私有。各條執行緒之間計數器互不影響,獨立儲存。
  • 當前執行緒所執行的位元組碼行號指示器。位元組碼直譯器工作時通過改變這個計數器值選取下一條需要執行的位元組碼指令(分支、迴圈、跳轉、異常處理都需要依賴此計數器)。
  • 多執行緒執行時通過此計數器線上程切換後恢復正確執行位置。

Java 虛擬機器棧

  • 執行緒私有。
  • 方法執行的記憶體模型。 方法執行時建立棧幀(區域性變量表、運算元棧、動態連結、方法出口等)。
  • 區域性變量表存放編譯期可知的基本資料型別、物件引用和returnAddress型別。其中long, double佔用2個區域性變數空間(slot),其餘佔用1個slot。
  • 區域性變數所需記憶體空間編譯期完成分配,執行期不改變其大小。

本地方法棧

  • 與虛擬機器棧類似,為Native方法服務。

Java 堆

  • 所有執行緒共享,虛擬機器啟動時建立。
  • 堆上分配(物件例項及陣列)。 JIT編譯期發展 + 逃逸分析技術 ——> 棧上分配、標量替換(後面章節會講到,只需知道所有物件在堆上分配非絕對)。
  • 又稱GC堆,垃圾收集管理的主要區域。

方法區(非堆 Non-Heap)

  • 所有執行緒共享。
  • 類資訊、常量、靜態變數、JIT編譯後的程式碼等。
  • 此區域垃圾收集:常量池回收和型別的解除安裝。(HotSpot虛擬機器中為永久代)

執行時常量池

  • 方法區的一部分。
  • Class檔案中除類的版本、欄位、方法、介面等描述資訊外,還有一項是常量池。 存放編譯期生成的各種字面量和符號引用。這部分內容將在類載入後進入方法區的執行時常量池中存放。
  • 除符號引用,還會把翻譯出來的直接引用存入執行時常量池中。
  • 動態性。執行期也可放入新的常量。eg. String.intern()方法。

直接記憶體

  • NIO:可以使用Native函式庫直接分配堆外記憶體。避免Java堆和Native堆來回複製資料。

2. HotSpot虛擬機器物件

建立物件

建立物件查詢falsetrueinvokespecial指令new類的符號引用(常量池)(檢查符號引用代表的類是否已被載入、解析和初始化)類載入分配記憶體(所需記憶體、類載入後可完全確定)1. Java堆規整,只需移動指標:    指標碰撞(Bump the Pointer)    Serial, ParNew等帶壓縮功能的垃圾收集器2. 不規整,維護列表記錄哪些記憶體塊可用:    空閒列表(Free List);  CMS等基於Mark-Sweep垃圾收集問題:非執行緒安全eg. 給物件A分配記憶體,指標沒來得及修改,物件B使用原指標分配記憶體1) 同步分配記憶體空間的動作: CAS + 失敗重試(原子性)2) 本地執行緒分配緩衝(TLAB): 每個執行緒在Java堆中預先分配一塊記憶體分    配動作按照執行緒劃分在不同空間。TLAB用完分配新的TLAB時同步鎖定。將分配到的記憶體空間初始化為零值(不包括物件頭)/使用TLAB時,提前至TLAB分配時進行物件設定(類資訊、物件雜湊碼、物件GC分代年齡等資訊。存放於物件頭。)執行<init>方法
程式碼清單1. 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部分:物件頭(Header)、例項資料(Instance Data)、對齊填充(Padding)。

  • 物件頭 – 第一部分,儲存物件自身的執行時資料。(如雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等)。這部分資料長度在32位和64位虛擬機器中分別為32位和64位,官方稱為"Mark Word"。 由於物件需要儲存的執行時資料很多,考慮到虛擬機器的記憶體使用,markOop被設計成一個非固定的資料結構,以便在極小的空間儲存儘量多的資料,根據物件的狀態複用自己的儲存空間,32位虛擬機器的markOop實現如下:
程式碼清單2. 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)

– 第二部分,型別指標,即物件指向它的類元資料的指標,虛擬機器通過這個指標確定這個物件是哪個類的例項(陣列還有一塊記錄陣列長度)。

  • 例項資料:物件真正儲存的有效資訊。
  • 對齊填充:僅起佔位符的作用。HotSpot VM要求物件起始地址必須是8位元組整數倍,換句話說,就是物件的大小必須是8位元組的整數倍。當例項資料部分沒有對齊時,通過對齊填充來補全。

物件的訪問定位

  • 使用控制代碼。 物件移動時只改變控制代碼中例項指標,reference不需要修改。 在這裡插入圖片描述
圖2. 通過控制代碼訪問物件
  • 使用直接指標。 速度快(節省一次指標定位的時間開銷)。HotSpot使用此種方式。 在這裡插入圖片描述
圖3. 通過直接指標訪問物件

3. 實戰:OutOfMemoryError異常

     這部分還是照著書本敲吧。程式碼不多,跟著敲一遍會對各種虛擬機器啟動引數有一個認識,對各種OOM有個瞭解。理論與實戰結合才能更好的理解虛擬機器,這裡一定要動動手實踐一下。具體程式碼就不一一貼出來了。 1)Java 堆溢位      -XX:+HeapDumpOnOutOfMemoryError: 出現OOM時Dump堆轉儲快照。      -Xms20m -Xmx20m 最小堆 最大堆20M 2)虛擬機器棧和本地方法棧溢位      -Xss引數:設定棧容量。      StackOverflowError: 單執行緒時,棧幀太大 or 虛擬機器棧容量太小。      如果是建立過多執行緒導致記憶體溢位,在不能減少執行緒數或更換64位虛擬機器的情況下,就只能通過減少最大堆和減少棧容量來換取更多的執行緒。 3)方法區和執行時常量池溢位(PermGen Space)      JDK1.6前,執行時常量池分配在永久代中,-XX:PermSize和-XX:MaxPermSize限制方法區大小,間接限制其中常量池容量。      JDK1.7,String.intern() 方法測試結果與JDK1.6不同。P56~P57書中例子。      產生大量類填滿方法區,導致溢位。CGLib、JSP、OSGi等。 4)本機直接記憶體溢位      使用NIO,可導致本機直接記憶體溢位。

參考資料:

  《深入理解Java虛擬機器》