JVM學習--Java內存管理&&異常

概述

按技術體系

	JAVA CARD:支持一些java小程序運行在小內存設備
	
	JAVA ME:支持java程序運行在移動終端
	
	JAVA SE:支持面向桌面級應用的java平臺
	
	JAVA EE:支持使用多層架構的企業應用

未來趨勢

模塊化+混合語言+多核並行+進一步豐富的語法

數據區域

在這裏插入圖片描述

程序計數器(PCR)

  • 作用:
    當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時通過改變計數器的值來選取下一條需要執行的字節碼指令

  • 由於JVM的多線程通過線程輪流切換並分配處理器執行時間來實現的,故在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)只會執行一條線程中的指令,每條線程都需要有一個獨立的程序計數器,各條線程之間的程序計數器互不影響,相互獨立。這類內存區域稱之爲"線程私有"的內存

  • 若線程執行的是java方法,則記錄正在執行的虛擬機字節碼指令的地址;
    若線程執行的Natvie方法,則該計數器的值爲空

棧(Stack)

java虛擬機棧

  • 線程私有,生命週期與線程一致

  • 存放JVM中的局部變量表部分

    局部變量表存放了編譯期剋制的各種基本數據類型(boolean,byte,int,char,short,long,float,double)、對象引用(reference類型)、returnAddress類型(指向了一條字節碼指令的地址)
    long和double會佔用倆個局部變量空間,其餘的只佔用一個
    方法運行期間不會改變局部變量表的大小

  • 倆種異常
    StackOverflowError
    線程請求的棧深度大於虛擬機所允許的深度
    OutOfMemoryError
    JVM可以動態擴展,當擴展時無法申請到足夠的內存

本地方法棧

爲虛擬機使用到的Native方法服務

堆(Heap)

  • 特點:

所有線程共享的一塊內存區域
在虛擬機啓動時創建
存放對象實例,幾乎所有的對象實例都在這裏分配內存
可以是物理上不連續的內存空間,在邏輯上連續即可

  • java堆是垃圾收集器管理的主要區域,很多時候也成爲GC堆

從內存回收的角度來看,java堆可細分爲:新生代和老年代
從內存分配的角度來看,java堆可劃分出多個線程私有的分配緩衝區

  • 當堆中沒有內存完成實例分配,並且無法再擴展的時候,將會拋出OutOfMemoryError異常

方法區(MA)

-特點:

各個線程共享的內存區域,用於存放已經JVM加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據
可以是物理上不連續的內存空間,在邏輯上連續即可
可選擇固定大小,也可擴展
可選擇不實現垃圾收集

  • 內存的回收目標主要是針對常量池的回收和對類型的卸載
  • 當方法區中無法滿足內存分配需求,將會拋出OutOfMemoryError異常

運行時常量池(RCP)

  • 方法區的一部分,用於存放編譯期生成的各種字面量和符號引用,在類加載後存放到方法區的運行時的常量池中
  • 相對於Class文件的常量池的區別是運行時常量池具備動態性
  • 當RCP無法再勝青島內存你是,將會拋出OutOfMemoryError異常

直接內存

並非JVM運行時的數據區的一部分,也不是JVM規範中的定義的內存區域
JDK1.4中新加入NIO類,引入了一種基於通道與緩衝區的I/O方式,可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作爲引用進行操作,提高性能

HotSpot虛擬機

對象的創建

類加載檢查

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

分配內存

對象所需要得內存在類加載完成後即可確定
分配方式:
1、指針碰撞(BTP)
JAVA堆中的內存是絕對規整的,所有被使用過的內存都放在一起,空閒的內存放在另一邊,中間是一個指針作爲分界點的指示器
2、空閒列表(FL)
JAVA堆中的內存是不規整的,已被使用的內存和空閒的內存相互交錯在一起,虛擬機必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄

如何保證線程安全?

  • 對分配內存空間的動作進行同步處理——實際上虛擬機是採用CAS配上失敗重試的方式保證更新操作的原子性;
  • 把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local AllocationBuffer,TLAB),哪個線程要分配內存,就在哪個線程的本地緩衝區中分配,只有本地緩衝區用完了,分配新的緩存區時才需要同步鎖定。

執行構造函數

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);
}
}
}

對象的內存佈局

對象頭(Header)

一、存儲對象自身的運行時數據
這部分主要用來存儲對象自身的運行時數據,如hashcode、gc分代年齡等。mark word的位長度爲JVM的一個Word大小,也就是說32位JVM的Mark word爲32位,64位JVM爲64位。
爲了讓一個字大小存儲更多的信息,JVM將字的最低兩個位設置爲標記位,不同標記位下的Mark Word示意如下:
在這裏插入圖片描述
lock:2位的鎖狀態標記位,由於希望用盡可能少的二進制位表示儘可能多的信息,所以設置了lock標記。該標記的值不同,整個mark word表示的含義不同。
在這裏插入圖片描述
biased_lock:對象是否啓用偏向鎖標記,只佔1個二進制位。爲1時表示對象啓用偏向鎖,爲0時表示對象沒有偏向鎖。
age:4位的Java對象年齡。在GC中,如果對象在Survivor區複製一次,年齡增加1。當對象達到設定的閾值時,將會晉升到老年代。默認情況下,並行GC的年齡閾值爲15,併發GC的年齡閾值爲6。由於age只有4位,所以最大值爲15,這就是-XX:MaxTenuringThreshold選項最大值爲15的原因。
identity_hashcode:25位的對象標識Hash碼,採用延遲加載技術。調用方法System.identityHashCode()計算,並會將結果寫到該對象頭中。當對象被鎖定時,該值會移動到管程Monitor中。
thread:持有偏向鎖的線程ID。
epoch:偏向時間戳。
ptr_to_lock_record:指向棧中鎖記錄的指針。
ptr_to_heavyweight_monitor:指向管程Monitor的指針

二、類型指針
指向它的類型元數據的指針

實例數據(Instance Data)

是對象真正存儲的有效信息,即我們在程序代碼裏面所定義的各種類型的字段內容,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄起來。
這部分的存儲順序會受到虛擬機分配策略參數(-XX:FieldsAllocationStyle參數)和字段在Java源碼中定義順序的影響。
HotSpot虛擬機默認的分配順序爲longs/doubles、ints、shorts/chars、bytes/booleans、oops(OrdinaryObject Pointers,OOPs),從以上默認的分配策略中可以看到,相同寬度的字段總是被分配到一起存放,在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。如果HotSpot虛擬機的+XX:CompactFields參數值爲true(默認就爲true),那子類之中較窄的變量也允許插入父類變量的空隙之中,以節省出一點點空間。

對齊填充(Padding)

這並不是必然存在的,也沒有特別的含義,它僅僅起着佔位符的作
用。由於HotSpot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是任何對象的大小都必須是8字節的整數倍。對象頭部分已經被精心設計成正好是8字節的倍數(1倍或者2倍),因此,如果對象實例數據部分沒有對齊的話,就需要通過對齊填充來補全。

訪問定位

使用句柄

Java堆中將可能會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自具體的地址信息。
在這裏插入圖片描述

使用指針

Java堆中對象的內存佈局就必須考慮如何放置訪問類型數據的相關信息,reference中存儲的直接就是對象地址,如果只是訪問對象本身的話,就不需要多一次間接訪問的開銷。
在這裏插入圖片描述

區別

使用句柄來訪問的最大好處就是reference中存儲的是穩定句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而reference本身不需要被修改。

使用直接指針來訪問最大的好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象訪
問在Java中非常頻繁,因此這類開銷積少成多也是一項極爲可觀的執行成本。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章