c++動態記憶體管理與智慧指標
1 物件
1.1 物件的記憶體佈局
1.1.1 物件頭
物件頭由兩部分組成,第一部分是用來儲存執行時資料(雜湊碼、GC年齡分代、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等)的"Mark Word",這部分有動態定義、空間複用的特點。第二部分是型別指標(使用控制代碼進行訪問時則沒有)。另外陣列物件還需要記錄其長度(可擴充套件型陣列長度不確定)(普通物件可通過元資料資訊獲取物件大小)。
1.1.2 例項資料
這部分資料的儲存順序受策略引數和其在原始碼中定義順序的影響,一般父類變數在子類變數前面儲存,子類較窄的變數可以插入父類變數的空隙之中。
1.1.3 對齊填充
物件的大小必須為8位元組的整數倍,否則要通過對齊填充來補全。
1.2 物件的建立
1.2.1 類載入檢查
如果已經被載入、解析、初始化過則直接進入記憶體分配的步驟,如果沒有則要先執行類載入的過程,然後再進入記憶體分配的步驟。
1.2.2 記憶體分配
1.2.2.1 “指標碰撞”和“空閒列表”
記憶體分配有兩種方式,如果虛擬機器堆記憶體是絕對規整的,則可以使用“指標碰撞”的方式為物件分配記憶體(以指標為界,移動指標分配記憶體)。如果堆記憶體不規整,則通過“空閒列表”的方式為物件分配記憶體(維護一個列表,用來記錄記憶體)。堆記憶體是否規整,由其所採用的垃圾收集器是否帶有空間壓縮整理能力決定。
1.2.2.2 分配記憶體時的執行緒安全問題
為物件分配記憶體時可能出現多個執行緒競爭一個指標的情況。解決這種問題的第一種方式是採用CAS配上失敗重試的方式保證更新操作的原子性,第二種方式是為本地執行緒分配緩衝(TLAB)(執行緒私有的堆空間)。
1.2.3 初始化零值
物件的例項欄位因此可以不賦初值而直接使用。
1.2.4 物件頭的設定
這步完成以後,在虛擬機器的視角中一個新的物件已經產生。
1.2.5 執行初始化方法
執行程式碼中的賦值操作。
1.3 物件的訪問定位
1.3.1 使用控制代碼進行訪問
在堆中分配一塊記憶體作為控制代碼池。控制代碼中包含了指向例項地址和指向型別資訊地址(型別資訊在元空間中)的指標。例項物件因此不需要放置指向型別資訊的指標。使用控制代碼訪問物件,當物件因為某種原因(帶有空間壓縮能力的垃圾收集器在垃圾收集時經常移動物件)而移動時,只需要改變控制代碼中的例項指標。
1.3.2 使用直接指標進行訪問
例項物件中需要包含指向型別資訊的指標,不需要間接訪問物件的開銷,效率更高。
2 執行緒共享的空間
2.1 堆(heap)
2.1.1 所有物件都應在堆上分配
隨著逃逸分析技術的成熟,物件也變得不一定要在堆上進行分配。當確定一個物件不會逃逸出執行緒時,可以在棧上分配物件,這樣物件會隨著棧幀出棧而銷燬(棧上分配)。當確定一個物件不會逃逸出方法時,可以將其拆散,將其用到的成員變數恢復為原始型別來訪問(標量替換)(不會建立這個物件,而直接建立它的成員變數)。
2.1.2 Java堆可以處於物理上不連續的空間
大物件如陣列為了實現簡單、儲存高效的目的扔有可能要求連續的空間進行儲存。
2.1.3 執行緒私有的堆(Thread Local Allocation Buffer,TLAB)
這是為了更好地回收記憶體、更快的分配記憶體,同時這樣還能解決物件分配時的執行緒安全問題。
2.1.4 Java堆溢位異常分析
2.1.4.1 獲取堆轉儲快照
通過設定 -XX:+HeapDumpOnOutOfMemoryError引數可以上虛擬機器在出現記憶體溢位時Dump出記憶體轉儲快照。
2.1.4.2 分析記憶體異常型別
可以使用記憶體映像分析工具進行分析(如:Eclipse Memory Analuzer)。
2.1.4.2.1 記憶體洩漏
物件無法被回收。可以根據洩露物件的型別資訊以及它到GC Roots引用鏈的資訊定位到這些物件建立時的位置。
2.1.4.2.2 記憶體溢位
存活物件過多。可以調整堆大小、優化物件生命週期。
2.2 方法區(Method Area)
在JDK8之後放在了背地記憶體中實現的元空間之中,之前是放在永久代之中。方法區主要包含了型別資訊、靜態變數、即時編譯器編譯後的程式碼快取、常量池這幾部分。字串常量池從JDK7起被從永久代移動到了Java堆中。
2.2.1 執行時常量池
主要是Class檔案中的常量池表中的各種字面量與符號引用,還有執行期加入的常量(String::inturn()方法可以向執行期常量池中加入字串常量)。方法區的回收主要是對常量池的回收和對型別的解除安裝。
2.2.2 元空間防範記憶體溢位的手段
-XX:MaxMetaspaceSize(預設值為-1,即不限制),-XX:MetaspaceSize(初始大小-在垃圾收集後根據剩餘空間大小自動增大或者減小),-XX:MinMetaspaceFreeRatio(垃圾收集後最小的剩餘空間百分比)。
3 執行緒私有的區域
3.1 虛擬機器棧和本地方法棧
虛擬機器棧為Java方法服務(位元組碼),本地方法棧為本地方法服務(Native)。一個方法從被呼叫到執行結束就對應著一個棧幀從入棧到出棧的過程。
3.1.1 棧幀
棧幀用於存放區域性變量表、運算元棧、動態連線、方法出口等資訊。
3.1.1.1 區域性變量表
存放著(編譯期可知的)基本資料型別(8種)、物件的引用(直接引用地址或者控制代碼地址)、returnAddress型別(指向一條位元組碼指令的地址)。區域性變量表所需的變數槽(Slot)數量在編譯期就完全確定。64位長度的long和double型別的資料會佔用兩個變數槽,其餘只會佔用一個。
3.1.2 棧溢位
3.1.2.1 StackOverflowError異常
執行緒請求的棧深度大於虛擬機器允許的最大深度或者新的棧幀無法分配記憶體時丟擲這個異常。
3.1.2.2 OutOfMemoryError異常
當允許動態擴充套件時,擴充套件棧容量申請不到足夠的記憶體丟擲這個異常。另一種原因是建立多執行緒時,新執行緒的棧申請不到足夠記憶體而丟擲這個異常(減少堆記憶體為棧留出更多記憶體或者減少棧容量)。
3.2 程式計數器
程式計數器是一塊較小的空間,是當前執行緒所執行的位元組碼的行號指示器。
3.2.1 程式控制流的指示器
分支、迴圈、跳轉、異常處理器和執行緒恢復都依賴程式計數器實現。Java執行緒切換後依賴程式計數器進行恢復,因此每個執行緒都需要獨立的程式計數器。
4 直接記憶體
JDK1.4中新加入了NIO(New Input/Output)類,它可以使用Native函式庫直接分配堆外記憶體。
可以使用儲存在Java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作(直接操作堆外記憶體)。
這樣避免了直接在Java堆和Native堆中來回複製資料,在某些場景中能顯著提高效率。