java內存管理機制剖析(一)

最近利用工作之餘學習研究了一下java的內存管理機制,在這裏記錄總結一下。

1.1 java內存區域

當java程序運行時,java虛擬機會將內存劃分爲若干個不同的數據區域,這些內存區域創建和銷燬的時間各不相同,所承擔的功能也不相同,他們各司其職,各盡所責。這些區域的劃分如下圖
image.png

運行時數據區主要有五個區,分別是 堆 ,方法區,虛擬機棧,本地方法棧,程序計數器,下面我來一一詳細講解這五個數據區

java堆是java虛擬機管理內存中最大的一塊,它是被所有線程共享的一塊內存區域,在虛擬機啓動時創建,此內存的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在堆分配內存

java虛擬機規定,java堆可以處於物理上不連續的內存空間中,只要邏輯上連續即可。在實現時,既可以實現固定大小的,也可以是擴展的,可以通過配置-Xmx和-Xms來擴展大小。如果堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError

方法區

方法區也是被所有線程共享的一塊內存區域,在Java虛擬機規範中,方法區是堆的邏輯組成部分,但他又被與堆區分開來,別名稱爲Non-Heap,它主要的存儲內容有下面幾點

  • 類型的完整有效名
  • 類型直接父類的完整有效名
  • 類型的修飾符(public,abstract,final的某個子集)
  • 類型的常量池
  • 域(Field)信息
  • 方法(Method)信息
  • 除了常量外的所有靜態(static)變量

總結起來就是主要用於存儲已被虛擬機加載的類信息,常量,靜態變量,編譯器編譯後的代碼等數據

這裏我在介紹一下常量池,域信息和方法信息

常量池

常量池也稱爲運行時常量池(Runtime Constant Pool),用於存放編譯期生成的各種字面量和符號引用,它是這個類型用到的常量的一個有序集合,包括實際的常量(String, Integer, 和Floating point常量)和類型,域和方法的符號引用
池中的數據項像數組項一樣,是通過索引訪問的。 因爲常量池存儲了一個類類型所使用到的所有類型,域和方法的符號引用,所以它在java程序的動態鏈接中起了核心的作用

域(Field)信息

域的相關信息包括:域名; 域類型; 域修飾符(public, private, protected,static,final volatile,transient的某個子集)

方法(Method)信息

方法的相關信息包括:方法名, 方法的返回類型(或 void), 方法參數的數量和類型(有序的),方法的修飾符(public, private, protected, static, final, synchronized, native, abstract的一個子集),除了abstract和native方法外,其他方法還有保存方法的字節碼(bytecodes)操作數棧和方法棧幀的局部變量區的大小

java虛擬機規範對方法區的限制比較寬鬆,除了和java堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不是實現垃圾收集。垃圾收集行爲在方法區也比較少出現,當方法區無法滿足內存分配時,會拋出OutOfMemoryError

虛擬機棧

虛擬機棧是線程私有的,它的生命週期與線程相同,當我們start一個線程時,jvm會爲當前線程開闢一塊虛擬機棧,噹噹前線程死亡時,線程的虛擬機棧也會銷燬。

代碼中每個方法在執行的同時,都會創建一個棧幀(Stack Frame)用於存儲局部變量表,操作數棧,動態鏈接,方法出口等信息。每一個方法調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

局部變量表存放了編譯期可知的各種基本數據類型(boolean,byte,char,short,int,float,long,double),對象引用和returnAddreass類型

JVM對這個區域規定了兩種異常情況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出SstackOverFlowError異常;如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryErro異常

本地方法棧

本地方法棧和虛擬機棧的作用是一樣的,只不過本地方法棧是虛擬機執行java方法時開闢的棧,而本地方法棧是虛擬機用到Native方法時,開闢的棧。

程序計數器

程序計數器是一塊較小的內存,它可以看作是當前線程的字節碼的行號指示器。在虛擬機的概念模型裏,字節碼解釋器工作時就是通過改變這個計數器的值來選取嚇一跳需要執行的字節碼指令,分支,循環,跳轉,異常處理,線程恢復等基礎功能。

由於java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器都只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各線程指尖計數器互不影響,獨立存儲。

1.2 對象創建

瞭解了內存的數據區域,我們可以進一步瞭解對象是如何創建的了。這裏先通過一張流程圖一窺java的對象創建過程
image.png
可以看到,當虛擬機遇到一條new指令時,首先去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經加載。如果沒有,則執行加載。

加載完成後,便會在堆中爲對象分配內存,JVM有兩種分配方式①指針碰撞,②空閒列表,下面我詳細講講這兩種分配方式。

指針碰撞

當java堆中內存是整齊的,所有用過的內存都放一邊,空閒的內存放在另一邊,中間放着一個指針座位分界點的指示器,那所分配內存就僅僅是把那個指針向空閒空間那邊摞動一段與對象大小相等的距離,這種分配就叫指針碰撞

空閒列表

當Java堆的內存並不是完整的,已分配的內存和空閒內存相互交錯,JVM通過維護一個列表,記錄可用的內存塊信息,當分配操作發生時,從列表中找到一個足夠大的內存塊分配給對象實例,並更新列表上的記錄。這種分配方式稱爲空閒列表

當JVM所採用的垃圾收集器帶有壓縮整理功能時,java堆是規整的,這個時候會採用指針碰撞分配內存,否則會採用空閒列表分配內存。對象創建是一個非常頻繁的行爲,進行堆內存分配時還需要考慮多線程併發問題,可能出現正在給對象A分配內存,指針或記錄還未更新,對象B又同時分配到原來的內存,解決這個問題有兩種方案:

  • 1、採用CAS保證數據更新操作的原子性;
  • 2、把內存分配的行爲按照線程進行劃分,在不同的空間中進行,每個線程在Java堆中預先分配一個內存塊,稱爲本地線程分配緩衝(Thread Local Allocation Buffer, TLAB);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章