JVM(二)JVM內存布局


這幾天我再次閱讀了《深入理解Java虛擬機》之第二章“Java內存區域與內存溢出異常”,同時也參考了一些網上的資料,現在把自己的一些認識和體會記錄一下。

 (本文為博主原創文章,轉載請注明出處)

一、概述

在網上看到很多的各種文章來寫Java內存布局/Java內存模型(JMM)/Java內存分配和回收等。初學者,往往容易被搞混淆,這些東西到底都是些啥?講的是不是同一個東西?如果不是同一個東西,那它們之間又有什么區別和聯系?說句實話,筆者在看到這些文章和概念時,一樣是有這些疑問的。下面我來說說我的理解,如果有人覺得有異議或者不對的地方,歡迎討論和指出,謝謝!

1.1 Java內存布局

我認為這是一個靜態的概念, 即JVM在概念和作用上對其管理的內存進行划分。如整體上來看,JVM的內存分為堆區和非堆區,而非堆區又包括了方法區、JVM棧、本地方法棧、程序計數器等。直接上圖:

圖1.1

 

1.2 Java內存模型(JMM)

這個講的是Java多線程的情況下,多線程之間的內存通信模型。即如果多個線程存在共享同一塊內存的情況下,JVM通過一個什么樣的內存模型來解決多線程對共享內存的讀取和寫入數據的問題。借用網上的一張圖:

圖1.2

從上圖中我們可以看到,JMM的核心思想是把堆中的內存分成了兩部分:主內存和本地內存,主內存為所有線程共享,而本地內存則由線程獨享,同時,對於共享變量,在線程的本地內存中都保存了一個副本共當前線程使用。其他更具體的細節,筆者會在后續的博客中繼續跟進和分析。當然,讀者也可以自行在網上搜,會有很多的文章講JMM的。

參考:http://www.infoq.com/cn/articles/java-memory-model-1

 

1.3 Java內存分配和回收

這是一個側重JVM對內存的使用和回收方法的概念。即JVM是如何分配內存,如何回收內存?通常Hotspot的JVM的分配和回收算法(分代垃圾回收)中,又體現出了如下的內存布局圖,同樣先借用網上的一張圖:

圖1.3

在JVM的內存空間中把堆空間分為年老代和年輕代。將大量(據說是90%以上)創建了沒多久就會消亡的對象存儲在年輕代,而年老代中存放生命周期長久的實例對象。年輕代中又被分為Eden區(聖經中的伊甸園)、和兩個Survivor區。新的對象分配是首先放在Eden區,Survivor區作為Eden區和Old區的緩沖,在Survivor區的對象經歷若干次收集仍然存活的,就會被轉移到年老區。
參考:http://www.cnblogs.com/douba/p/a-simple-example-demo-jvm-allocation-and-gc.html

 

講完了這三個概念的比較,接下來進入正題“JVM內存布局”

 

二、Java虛擬機運行時數據區划分

運行時的數據區域划分,可以參考圖1.1,現在就幾個數據區域的功能和作用做詳細的介紹。

2.1 JVM堆(heap)

其主要作用是用於為幾乎所有的對象實例和數組實例的實例化提供內存空間。說通俗點,所有采用new關鍵字產生的對象的空間都在此分配。如:Map<String,String> map = new HashMap<>(); 這個map所引用的對象即位於JVM的堆中。

JVM heap的特點是:1)內存不一定連續分配,只要邏輯上是連續的即可;

           2)內存的大小可以通過JVM的參數來控制:如 -Xms = 1024M -Xmx = 2048M; 表示JVM Heap的初始大小為1GB,最大可自動伸縮到2GB;

           3)平時所說的Java內存管理即指的是內存管理器對這部分內存的管理(創建/回收);

           4)所有線程共享 

而JVM Heap的內存在采用分代垃圾回收算法的Hotspot JVM中,這片區域會被分割成新生代/老年代/永久代,而新生代中又分成了兩部分Eden區和Survivor區(分為from space和to space,各自占Survivor區的50%的內存大小)。此部分的詳細信息,可以參考圖1.3 。然而,在內存分配的時候,JVM通常會從Heap中為每個線程分配一個線程緩沖區(Thread Local Allocation Buffer, TLAB),TLAB為線程私有,因此在為對象分配內存時,首先會先從TLAB中分配內存,這樣做可以保證在分配內存的時候不用鎖住整個堆區,減少鎖開銷。

 

2.2 方法區

主要用於存儲已經由JVM加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。在Hotspot JVM中,設計者使用“永久代”來實現方法區,這樣帶來的好處是,JVM可以不用再特別的寫代碼來管理方法區的內存,而可以像管理JVM 堆一樣來管理。帶來的弊端在於,這很容易造成內存溢出的問題。我們知道,JVM的永久代使用 --XX:MAXPermSize來設定永久代的內存上限。

值得一提的是,運行時常量池也屬於方法區的一部分,所以,它的大小是受到方法區的限制的。運行期間也可以將新的常量放入池中,運用的比較多的就是String的intern()方法。

 

2.3 JVM棧(Java虛擬機棧)

JVM棧是伴隨着線程的產生而產生的,屬於線程私有區域,生命周期和線程保持一致,所以,JVM的內存管理器對這部分內存無需管理;

從圖1.1中可以看出,棧中有包含了多個棧幀(frame),frame伴隨的方法的調用而產生,方法調用完畢,frame也就出棧,從JVM棧中被移除。frame主要保存方法的局部變量、操作數棧、動態鏈接、方法出口(reternAddress或者拋Exception)。

虛擬機規范中,對此內存區域規定了兩種異常情況:

  1)當線程請求的戰的深度大於JVM所允許的最大深度,則拋出StackOverflowError;

  2)當棧的內存是可動態擴展的時候,如果擴展時發現堆內存不足時,會拋出OutofMemoryError;

 

2.4 本地方法棧

與JVM棧非常類似,只不過,本地方法棧是為Java的Native method方法調用的時候服務。JVM規范並未定義如何實現該區域,但是,在Hotspot JVM中,本地方法棧和JVM棧合二為一,所以,本地方法棧同樣會拋出StackOverflowError和OutofMemoryError。

 

2.5 程序計數器

簡稱PC(Program Counter Register),為線程私有,如果執行的是一個Java方法,那么此時PC里保存的是當前Java字節碼的指令地址;如果說執行的是Native方法,那么PC的內容則為空。

 

2.6 直接內存

這部分內存不屬於JVM的內存區域,在這里提出來也是為了在心里對這個有一定的了解。在JDK1.4中提出來的NIO中,就存在這直接訪問機器內存的情況,從而避免了機器內存到JVM內存之間數據的來回拷貝情況,能夠提高性能。在虛擬機內存調優時,可能常常容易被遺忘的部分就是這部分內存了。這也是有時候,Java程序實際占用的內存大於-Xmx所限制的內存大小的原因之一。

 

三、Hotspot虛擬機對象分析

 

3.1 對象的布局

Hotspot JVM中,對象在內存中的布局包括3塊區域:對象頭(header),對象實例數據(Instance Data)和對齊填充(Padding)。

 

3.1.1 對象頭(Header)

對象頭又包含兩部分內容:Markword和reference(類類型指針)

 

3.1.1.1 Markword

用於存儲運行時自身的數據,包括哈希碼、GC分代年齡、偏向鎖標記、線程持有的鎖、偏向線程ID、偏向時間戳等。

從markOop.hpp中的注釋中,我們可以詳細的了解到Markword各個bit位所代表的內容和含義

// 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)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

以32位的系統為例,能總結出如下表:

圖3.1

3.1.2 reference(類類型指針)

用於指向對象的類的元數據的指針,通過這個指針,JVM可以知道,這個對象是屬於哪個類的實例對象。當然,並不是所有的JVM的實現都會把對象的類類型指針保留在對象上的(這里是由Java對象的內存訪問定位實現來決定的)。另外,如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,因為虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是從數組的元數據中無法確定數組的大小。  

當Java對象的內存訪問定位是直接指針訪問時(大部分JVM這樣實現),才會在對象的頭部保存類類型指針,如圖

圖3.2 直接指針訪問對象

當Java對象的內存訪問定位采用的是句柄訪問時,則不會在對象的頭部保存類類型指針,如圖

圖3.3 句柄訪問對象

3.2 JVM對象內存分配過程

通過源碼(bytecodeInterpreter.cpp)中的來分析,如下:

// 如果是new操作的時候
CASE(_new): { u2 index
= Bytes::get_Java_u2(pc+1); ConstantPool* constants = istate->method()->constants(); if (!constants->tag_at(index).is_unresolved_klass()) { // Make sure klass is initialized and doesn't have a finalizer       // 1. 確保類已經初始化,及類已經加載/驗證/准備/解析/初始化完成了。
Klass* entry = constants->slot_at(index).get_klass();
      // 確保類已經被解析
assert(entry
->is_klass(), "Should be resolved klass"); Klass* k_entry = (Klass*) entry;
      // 確保類已經被初始化 assert(k_entry
->oop_is_instance(), "Should be InstanceKlass"); InstanceKlass* ik = (InstanceKlass*) k_entry;
      // 2. 如果類已經初始化並且能夠進行快速內存分配
if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
        // 獲取對象實例的大小 size_t obj_size
= ik->size_helper(); oop result = NULL; // If the TLAB isn't pre-zeroed then we'll have to do it
       // 如果在TLAB上分配內存,如果TLAB上事先未將內存進行置零處理,則后續在內存分配完畢之后,需要將對象所在的內存進行置零操作 bool need_zero = !ZeroTLAB; if (UseTLAB) {
         // 如果開啟了線程的TLAB,則直接在線程的TLAB上給對象分配內存,如果分配成功,則返回對象的地址,否則返回null,是否開啟TLAB是通過虛擬機參數:-XX:+/-UseTLAB參數來設定,默認是開啟的。 result
= (oop) THREAD->tlab().allocate(obj_size); } // Disable non-TLAB-based fast-path, because profiling requires that all // allocations go through InterpreterRuntime::_new() if THREAD->tlab().allocate // returns NULL. #ifndef CC_INTERP_PROFILE if (result == NULL) {
        // 如果在TLAB上未成功分配到內存給對象,則從堆上為對象分配內存 need_zero
= true; // Try allocate in shared eden retry:
        // 獲取堆頂的地址 HeapWord
* compare_to = *Universe::heap()->top_addr();
        // 獲取分配obj_size字節的內存后,堆頂的位置 HeapWord
* new_top = compare_to + obj_size;
        // 如果堆頂地址小於或等於堆的最大內存地址,則采用原子操作CAS,將堆頂的地址指向new_top,從而完成內存的分配
if (new_top <= *Universe::heap()->end_addr()) { if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) { goto retry; }
          // 如果CAS操作成功,則表示內存分配成功,然后把原來堆頂的內存地址賦值給指向對象的引用,即對象的內存地址 result
= (oop) compare_to; } } #endif if (result != NULL) { // Initialize object (if nonzero size and need) and then the header
         // 如果對象不為空,且需要置零,則置零處理
if (need_zero ) {
          // 我們知道,對象的頭是不能置零處理的,只有對象的body部分能置零處理,所以跳過對象的頭部內存sizeof(oopDesc)/oopSize部分 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); // Must prevent reordering of stores for object initialization // with stores that publish the new object. OrderAccess::storestore(); SET_STACK_OBJECT(result, 0);
        // 更新PC的指令地址,執行嚇一條指令 UPDATE_PC_AND_TOS_AND_CONTINUE(
3, 1); } } } // Slow case allocation
// 如果未執行快速內存划分,則執行慢速內存划分,可能會采用鎖的機制來完成,效率會比較低
CALL_VM(InterpreterRuntime::_new(THREAD, METHOD->constants(), index), handle_exception); // Must prevent reordering of stores for object initialization // with stores that publish the new object. OrderAccess::storestore(); SET_STACK_OBJECT(THREAD->vm_result(), 0); THREAD->set_vm_result(NULL); UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1); }

 從以上源碼,簡單的總結下對象內存分配的過程:

  1). 確保類已經初始化,及類已經加載/驗證/准備/解析/初始化完成了;

  2). 判斷該類的實例對象是否能夠進行快速內存分配,如果是,則繼續,否則跳到10;

  3). 判斷JVM是否開啟了TLAB選項,如果是則繼續,否則跳到7;

  4). 從線程的TLAB進行內存分配;

  5). 判斷TLAB內存分配是否成功,如果失敗,則繼續,否則跳到7;

  6). 如果TLAB內存分配失敗,且未定義CC_INTERP_PROFILE,則通過CAS繼續進行快速內存分配;

  7). CAS內存分配成功(如果失敗,則會拋出OutOfMemoryError的異常,而導致程序終止),則根據實際情況看是否要對對象的內存進行初始化操作(置零);

  8). 設置對象的頭的MarkWord和對象所屬類的元數據信息;

  9). 設置PC的地址為下一條字節碼的地址,並繼續執行下一條字節碼,本次內存分配結束;

  10). 進入JVM的慢速內存分配通道,進行內存分配。

 

四、總結

 本文首先闡述了一下讓筆者在學習之初比較困惑的3個問題,然后結合有關參考資料和Hotspot的源碼分析對JVM的內存布局以及對象內存的分配過程做了比較細致的學習和分析,同時對對象在JVM中的表示進行分析。基本上弄清楚了JVM中的內存概念布局。后續還需要在JMM和垃圾回收的算法等方面加強對JVM的內存模型理解。

 

參考資料:

2. http://blog.csdn.net/shiyong1949/article/details/52585256
內存區域介紹
3. http://www.cnblogs.com/douba/p/a-simple-example-demo-jvm-allocation-and-gc.html
最簡單例子圖解JVM內存分配和回收
4.http://www.idouba.net/java-gc-policies/
java 垃圾回收策略
5. 虛擬機配置相關參數
JDK7及以前版本:http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
JDK8: http://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
6. java並發之內存模型(JMM)
http://www.idouba.net/topics/jvm/
7.http://www.cnblogs.com/SaraMoring/p/5713732.html
JVM內存堆布局圖解分析
8. http://blog.csdn.net/yangzl2008/article/details/43202969
Java中的逃逸分析和TLAB中以及Java對象分配
9. http://blog.csdn.net/zhoufanyang_china/article/details/54601311
不得不了解的對象頭


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM