天天看點

《深入了解java虛拟機》讀書筆記(一):Java記憶體區域适用對象

适用對象

java入門階段的小白、看過原書後需要快速複習的同學。

運作時資料區域

《深入了解java虛拟機》讀書筆記(一):Java記憶體區域适用對象
  • 程式計數器:“線程私有”,如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛拟機位元組碼指令的位址;如果正在執行的是Native方法,這個計數器值則為空(Undefined)。
  • Java虛拟機棧:Java虛拟機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。虛拟機棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時 都會建立一個棧幀(Stack Frame[1])用于存儲局部變量表(基本資料類型,對象引用,returnAddress類型)、操作數棧、動态連結、方法出口 等資訊。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛拟機棧中入棧到出棧的過程。 可抛出StackOverflowError異常和OutOfMemoryError異常。
  • 本地方法棧:與虛拟機棧所發揮的作用是非常相似,而本地方法棧則為虛 拟機使用到的Native方法服務。本地方法 棧區域也會抛出StackOverflowError和OutOfMemoryError異常。
  • Java堆:是Java虛拟機所管理的記憶體中最大的一塊。 Java堆是被所有線程共享的一塊記憶體區域,在虛拟機啟動時建立。所有的對象執行個體以及數組都要在堆上配置設定。(現在已經不那麼絕對了)從記憶體回收的角度來看,是以Java堆中還可以細分為:新生代和老年代。Java堆可以處于實體上不連續的記憶體空間中,隻要邏輯上 是連續的即可。會抛出OutOfMemoryError異 常。
  • 方法區:它用于存儲已被虛 拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。Java虛拟機規範對方法區的限制非常寬松,除了和Java堆一樣不需要連續的記憶體和可以 選擇固定大小或者可擴充外,還可以選擇不實作垃圾收集。當方法區無法滿足記憶體配置設定需求時,将抛出 OutOfMemoryError異常。
  • 運作時常量池:用于存放編譯期生成的各種字面量和符号引用。Java語言并不要求常量一定隻有編譯期才能産生,也就是并非預置入Class檔案中常量池的内容才能進入方法區運作時常量池,運作期間也可能将新的常量放入池中。當常量池無法再申 請到記憶體時會抛出OutOfMemoryError異常。
  • 直接記憶體:它可以使用Native函數庫直接配置設定堆外記憶體,然後通過一個存儲 在Java堆中的DirectByteBuffer對象作為這塊記憶體的引用進行操作。既然是記憶體,肯定還是 會受到本機總記憶體(包括RAM以及SWAP區或者分頁檔案)大小以及處理器尋址空間的限制。各個記憶體區域總和大于實體記憶體限制(包括實體的和作業系統級的限制)将導緻動态擴充時出現OutOfMemoryError異常。

對象建立

僅限制于普通java對象,不包括數組和class對象。

虛拟機遇到一條new指令時,首先将去檢查這個指令的參數是否能在常量池中定位到一 個類的符号引用,并且檢查這個符号引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。為對象配置設定空間的任務等同于把 一塊确定大小的記憶體從Java堆中劃分出來。分為:指針碰撞和空閑清單兩種方式。選擇哪種配置設定方式由 Java堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決 定。是以,在使用Serial、ParNew等帶Compact過程的收集器時,系統采用的配置設定算法是指針 碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時,通常采用空閑清單。

如何確定線程安全? 解決這個問題有兩種方案,一種是對配置設定記憶體空間的動作進行同步處理 ——實際上虛拟機采用CAS配上失敗重試的方式保證更新操作的原子性;另一種是把記憶體分 配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先配置設定一小塊内 存,稱為本地線程配置設定緩沖(Thread Local Allocation Buffer,TLAB)。哪個線程要配置設定内 存,就在哪個線程的TLAB上配置設定,隻有TLAB用完并配置設定新的TLAB時,才需要同步鎖定。

接下來,虛拟機要對對象進行必要的設定,例如這個對象是哪個類的執行個體、如何才能找 到類的中繼資料資訊、對象的哈希碼、對象的GC分代年齡等資訊。這些資訊存放在對象的對 象頭(Object Header)之中。

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

對象的記憶體布局

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

  • 對象頭:包括兩部分資訊,第一部分用于存儲對象自身的運作時資料。對象頭的另外一部分是類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指 針來确定這個對象是哪個類的執行個體。并不是所有的虛拟機實作都必須在對象資料上保留類型 指針,如果對象是一個Java數組,那在對象頭中還必須有一塊用于記錄數組長度的資料。
  • 執行個體資料:對象真正存儲的有效資訊,也是在程式代碼中所定義的各種類 型的字段内容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。
  • 對齊填充: 并不是必然存在的,也沒有特别的含義,它僅僅起着占位符的作用。當對象執行個體資料部分沒有對齊時(對象起始位址必須是8的位元組的整數倍),就需要通過對齊填充來補全。

對象通路定位

我們的Java程式需要通過棧上的reference資料來操作堆上的 具體對象。對象通路方式也是 取決于虛拟機實作而定的。目前主流的通路方式有使用句柄和直接指針兩種。

《深入了解java虛拟機》讀書筆記(一):Java記憶體區域适用對象
《深入了解java虛拟機》讀書筆記(一):Java記憶體區域适用對象

使用句柄來通路的最大好處就是reference中存儲的是穩 定的句柄位址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時隻會改變句柄中 的執行個體資料指針,而reference本身不需要修改。 使用直接指針通路方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷, 由于對象的通路在Java中非常頻繁,是以這類開銷積少成多後也是一項非常可觀的執行成 本。就本書讨論的主要虛拟機Sun HotSpot而言,它是使用第二種方式進行對象通路的。