【學習筆記】—JVM(一)Java內存區域的劃分和相應作用及對象在其中的分佈

參考:《深入理解Java虛擬機》周志明著

一、JVM數據區

JVM數據區

1. 程序計數器:

  當前線程所執行的字節碼的信號指示器。
  JVM的多線程是通過線程輪流切換並分配處理器執行時間(操作系統)實現 因此每條線程都需要有一個獨立的程序計數器,各線程互不影響,稱之爲“線程私有”的內存

字節碼解釋器工作時就是通過改變計數器的值來選取下一條要執行的字節碼命令

  唯一一個不會拋出任何內存溢出(OutOfMemoryError簡稱OOM)的區域

2. 虛擬機棧:

  線程私有。
  java方法執行的內存模型:每一個方法執行時都會創建一個棧幀,用於存儲局部變量表,操作棧數,動態鏈接,方法出口等信息。每一個方法從調用直至完成對應着入棧和出棧。
  局部變量表: 存放編譯期可知的各種數據類型,對象的引用(reference)和returnAddress類型(指向了一條字節碼指令的地址)。其中long和double 會佔用2個局部變量空間(Slot),其餘都只佔用1個。所需內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在棧中分配多大的局部變量空間是完全確定的,方法運行期間不會改變局部變量表的大小
  會拋出StackOverflowError和OOM。

3.本地方法棧:

  線程私有。
  和虛擬機棧作用非常相似,區別在於本地方法棧爲虛擬機用到的Native方法服務

Native方法:
  簡單來說就是一個Java調用非Java代碼的接口,該方法的實現由非Java語言實現。因爲在虛擬機中並沒有規範本地方法棧中使用的語言,使用方式和數據結構,因此虛擬機可以自由實現他,如偏向底層操作系統層的c語言之類的方法來實現。

native static public void methodName();

除了不能與abstract修飾使用外,其他均可以。

  會拋出StackOverflowError和OOM。

4.Java堆:

  在虛擬機啓動時創建,是所管理內存中最大的一塊,被所有線程共享使用。
  所有的對象實例以及數組都要在堆上分配
  堆細分爲:
     - 新生代
     - 老年代
  新生代一般佔據1/3的空間,且可再細分爲:Eden空間,From Survivor空間,To Survivor空間
  無論怎麼分,存儲的都是對象實例,劃分區域主要是便於垃圾回收。java堆可以在物理上不連續,邏輯上連續即可。
  會拋出OOM

5.方法區(永久代,元數據區):

  共享區
  用於存儲已被虛擬機加載的類的信息,常量,靜態變量,即時編譯器編譯後的代碼等數據

Java虛擬機規範沒有多大限制,甚至可以選在不實現垃圾回收,但是這一部分的回收是很有必要的

  運行時常量池:
    Class文件中有一項是常量池,用於存放編譯期生成的各種字面量和符號引用,這些內容在類加載後進去方法區的運行時常量池
  除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中
  具備動態性,運行期間也有可能有新的常量放入池中

public class test{
	public static void main(String[] arg){
		String str1 = new StringBuilder("計算機"),append("軟件").toString();
		System.out.println(str1.intern() == str1);//JDK1.6返回false,1.7返回true
		
		String str2 = new StringBuilder("Ja"),append("va").toString();
		System.out.println(str2.intern() == str2);//JDK1.6返回false,1.7返回false
	}
}

  JDK1.6的intern()方法會把首次遇到的字符串實例複製到永久代中,返回的也是永久代中這個字符串的實例。而JDK1.7只是在常量池中記錄首次出現的實例引用,上訴代碼中的"Java"已經出現過了。
  會拋出OOM

二、虛擬機中的對象

1.對象的創建

  - 當虛擬機遇到一條new命令時,先去檢查命令參數是否能在常量池中定位到一個類的符號引用,並檢查是否被加載,解析初始化過。若沒有則先初始化。
   - 檢查通過後爲其分配內存,兩種方式:

指針碰撞: 假設Java堆中內存絕對規整,所有用過的內存放在一邊,沒有使用過的放在一邊,中間放一個指針作爲分界點的指示器,分配內存就是把這個指針向空閒的一邊移動和對象同等大小的距離。(若垃圾收集器使用的是複製算法或者標記-整理例如,Serial,ParNew可以實現內存的絕對規整)

空閒列表: 若內存不是規整的,空閒與使用的內存相互交錯,虛擬機就要在一張表上面記錄哪些內存是可以用的,分配時從表中找到一塊足夠大的空間分配給對象(垃圾收集器使用的是標記-清除算法,例如CMS則使用空閒列表)

  併發創建對象時分配地址避免正在分配還沒來得及修改,下一個對象又使用了同樣的指定分配地址的兩種方法
    ·CAS配上重試保證原子性

樂觀鎖用到的機制就是CAS,Compare and Swap(比較並交換)。
CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。通常將 CAS 用於同步的方式是從地址 V 讀取值 A,執行多步計算來獲得新 值 B,然後使用 CAS 將 V 的值從 A 改爲 B。如果 V 處的值尚未同時更改,則 CAS 操作成功。

    ·把內存分配按照線程劃分在不同空間中進行,即在線程創建之時預先在隊中分配一小塊內存,稱之爲本地線程分配緩衝
  - 分配完內存之後,設置對象的必要信息即設置對象頭哪個類的實例,如何找到元數據,對象的哈希碼,年齡代等信息
  此時從JVM的角度來看一個對象已經創建完畢,但是從JAVA的角度來看要執行完<init>方法纔算完全的創建。

2.對象的內存佈局

  主要分爲3個區域
  I.對象頭(Header)
    第一部分:用於存儲對象自身運行時數據:哈希碼,GC分代年齡,鎖狀態標誌,線程持有的鎖,偏向線程ID等,官方稱之爲Mark Word
    第二部分:類型指針,即對象指向他的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例,但並不是所有的都要求保留類型指針,也就是說查找對象的元數據信息不一定要經過對象本身,例如通過句柄訪問
  II.實例數據(Instance)
    對象真正存儲的有效信息。從父類繼承的,子類自身定義的都要存儲,相同寬度的字段總是被分配在一起,且父類中定義的變量會出現在子類之前
  III.對齊填充(padding)
    僅起佔位作用,HotSpot要求對象的大小必須是8字節的倍數,對象頭剛好是8的倍數,所以實例數據部分沒有對齊時就需要填充

3.對象的訪問定位

  句柄訪問:

在Java堆中劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,句柄中包含了對象的實例數據與類型數據各自的地址信息。

在這裏插入圖片描述

  該方式比較穩定,在對象被移動(垃圾回收使用標記-整理之類的算法就會產生移動,非常普遍的行爲)時只會改變句柄中的實例指針,而不會修改reference本身。
  直接指針訪問:

直接指針訪問
  該方式訪問速度快,比句柄少了一次定位指針的的時間,而對象的訪問又是非常頻繁的。

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