Android記憶體、效能是程式永恆的話題

weixin_34293059發表於2016-09-03

記憶體、效能是程式永恆的話題,實際開發中關於卡頓、OOM也經常是打不完的兩隻老虎,關於卡頓、OOM的定位方法和工具比較多,這篇文章也不打算贅述了,本章主要是來整理一下JVM的記憶體模型以及Java物件的生與死。

生存空間(記憶體區域)

Java程式執行在JVM之上,如果Java物件是一個有血有肉的生靈,那麼它生存環境是怎樣的呢?很多人把Java記憶體分為堆記憶體(Heap)和棧記憶體(Stack),實際上這種劃分比較出粗糙和片面。比較細緻的劃分是這樣的:

記憶體區域

分為程程計數器、虛擬機器棧、本地方法棧、方法區和堆。

程式計數器

理解程式計數器之前,我們先來理解一下執行緒的並行:

執行緒並行.png

感官上兩條執行緒是同時執行的,但是在一個CPU上實際上是切換輪流執行的,在同一時刻,CPU不會同時執行一個執行緒,執行緒的“並行”通過CPU的高速切換來實現的。

執行緒切換執行.png

現在的問題是:執行緒切換回來之後是如何確定當前執行緒之前執行的位置和狀態?
答案是:使用程式計數器。每一條執行緒需要一個獨立的程式計數器來記錄執行緒執行的狀態,各個執行緒之間的計數器互不影響,所以程式計數器是執行緒私有的記憶體區域。所以說執行緒越多開銷越大。

Java 虛擬機器棧

虛 擬機棧也是執行緒私有的記憶體區域,用於儲存方法執行過程中的區域性變數、運算元棧、方法出入口等資訊。執行緒執行每一個方法都會建立一個棧幀,棧幀中就包含了局 部變數表、運算元棧、方法出入口等資訊,區域性變數表存放基本型別的臨時變數,包含boolean、byte、char、short、int、float、 long、doubble和物件應用型別(reference,物件地址)。例如下面一段程式碼:

方法.png

假如某執行緒執行方法a(),那麼該執行緒的棧記憶體大概是這樣的:

棧幀1.png

假如方法執行到15行,方法b()的棧幀建立併入棧:

棧幀2.png

執行完15行到16行,方法b的棧幀出棧:

棧幀1.png

Java虛擬機器棧,就是我們常說的棧記憶體,如果執行緒請求的深度超過虛擬機器執行的深度就會丟擲 StackOverflowError 的異常。

本地方法棧

本地方法棧和虛擬機器棧是類似的,虛擬機器棧是為虛擬機器執行 Java 程式碼服務,本地方法棧是為虛擬機器使用Native 方法服務。在 HotSpot 虛擬機器中本地方法棧也會丟擲 StackOverflowError 的異常。

物件棲息之地-Java堆

堆記憶體,大部分人都比較熟悉了,Java 堆是虛擬機器中站記憶體最大的一塊區域,是所有物件例項的土壤。堆記憶體是執行緒共享的,所有執行緒產生的物件都要在這塊區域中劃分記憶體。

堆記憶體.png

Java 堆是垃圾回收器主要管理的區域,又叫 “GC堆”,從垃圾回收器的角度來看 Java 堆又分為新生代和老生代(和垃圾回收器的分代演算法相關,見:《《 Java 物件之死》); 從執行緒共享的情況來看,Java 堆還可能劃分為每個執行緒劃分一個下的記憶體區域作為執行緒使用的快取區域(Thread Local Allocation Buffer,簡稱TLAB,後面會進一步說明)。無論怎麼劃分,當堆中沒有足夠的空間來存放新的例項時就會丟擲 OutOfMemoryError(OOM) 異常。

方法區

和 Java 堆一樣,也是執行緒共享的,這部分記憶體用於儲存類資訊、常量、靜態變數和即時編譯的程式碼資料。

方法區.png

型別資訊,指的是型別和其指標的對應關係,在建立和訪問物件的時候得到,用於查詢區分型別。例如有兩個類,class A 和 class B ,假如這兩個類都載入了,那麼方法區大概是這樣記錄的:

方法區二.png

常量中還有一部分叫做執行時常量池,這部分並不是在編譯和載入期間產生的,而是執行期間產生的,例如:

String a = "abc";
String b = "def";
String c = a+b;

上述程式碼產生的 “abc” 和 “def” 會被存放到執行時常量池中。方法區的記憶體使用超過限制會丟擲 StackOverflowError 的異常。

物件的“出生”

前面介紹了物件的生存環境:記憶體的區域和各個區域的作用。接下來說說物件的“出生”,一個 new 關鍵字到底包含了那些“不為人知”的過程?

物件建立過程.png

當程式執行遇到一個 new 關鍵字之後,首先會去方法區參賽定位到這個類符合的引用,查詢到是什麼類之後再去檢查這個類有沒有被載入,如果沒有執行類載入過程,類載入過程也是一個比較複雜的過程,這裡不展開論述。

在 類載入完成(或者已經載入過)之後,接下來就開始為新的物件分配記憶體了,為物件分配記憶體一般有兩種方式:“指標碰撞”和“空閒列表”。如果 Java 堆中正在使用的記憶體和空閒記憶體分別都是連續的規整的,中間臨界點存放一個指標作為分界標識,為新物件分配記憶體的時候該指標移動和這個物件大小相等的一段距 離就行了,所以叫“指標碰撞”。如果 Java 堆中正在使用的記憶體和空閒記憶體不是連續的,那麼就沒有辦法是用指標碰撞這種方式分配記憶體了,虛擬機器就必須維護一個列表來記錄那些記憶體是空閒的,在分配記憶體 的時候就衝空閒列表中找一份足夠大的空間類分配給物件,這種方式稱為“空閒列表”。是用“指正碰撞”還是“空閒列表”取決 Java 堆記憶體是否規整,而 Java 堆記憶體是否規整取決於垃圾回收器使用的回收演算法(參考《 Java 物件之死》)是否帶有壓縮功能。

前 面提到 Java 堆記憶體是執行緒共享的,多執行緒同時在堆記憶體中分配記憶體,就要保證記憶體劃分的原子行。如何保證記憶體分配的執行緒安全?一般有兩種方案,第一種方案就是實用同步控 制處理,第二種實用 Thread Local Allocation Buffer(TLAB)方式。第一種用多說了很好理解,不用過多解釋。TLAB,即執行緒本地緩衝,就是預先為每個執行緒分配一個 TLAB ,當執行緒需要使用記憶體的時候就在自己的 TLAB 上分配就好了。

記憶體分配玩之後需要對物件進行必要的設定,例如物件型別資訊、後設資料
物件雜湊碼、GC年齡等。

物件長啥樣子

通過上面的介紹我們知道物件的“出生”過程了,物件“長啥樣”呢?物件在記憶體中可以分為3個部分:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)。
對 象頭包括雜湊碼、GC代年齡、鎖狀態、執行緒持有鎖、偏向執行緒ID、時間戳等,另外還包含型別指標。型別指標的作用是JVM使用這個型別指標來查詢這個物件 是屬於那個類的。例項資料部分才是真正儲存物件資訊那部分。對齊補充部分不是必然存在的,僅僅用於站位,因為 HotSpot VM 要求物件的大小必須是8位元組的整數倍,物件頭的長度已經是8位元組的整數倍,例項資料大小不固定,所以使用Padding部分來填充。

物件佈局.png

和它握手

物件已經建立了,並且已經知道它的大概模樣,如何訪問它呢。在 HotSpot 中使用直接指標訪問的。前面介紹過虛擬機器棧,方法執行過程中建立的棧幀中區域性變數表中儲存了方法的區域性變數,包含基本型別和引用型別,其中引用型別其實就是一個指向物件記憶體地址的指標。

物件訪問.png

推薦:

相關文章