Java內存區域理解

前言

最近的經驗告訴我,大家在挑選技術書籍時候一定要慎重,若是買到不適合自己情況,真的是既浪費金錢,更重要的是浪費了時間。寧願花費幾天甚至一週的時間來好好選擇自己所需要的書籍,這樣做是非常值得的,原因有二:

  • 好書值得一讀再讀,每讀一遍都有新的體驗,買來總是不會錯的
  • 不會浪費金錢,也不會浪費時間

作爲搞技術的,書籍的花費是絕不能吝嗇的,網上找的大部分文章包括我這篇,都大概只能解決當前問題,因爲我也是爲了總結學習內容,對你的技術提高應該是沒有什麼幫助的。只有閱讀優秀書籍,站在巨人肩膀上,才能系統地學習,並提高自己的能力與技術。

以上是我的淺見,不認同的朋友不妨當我是胡說,因爲本質上來說我是說給自己聽的(笑)。

Java 內存概述

Java 虛擬機在執行Java 程序的時候,將它所管理的內存給劃分成了若干個不同的數據區域,這些區域,可以使用下圖來簡單概括:

這裏寫圖片描述

下面來一一解析這些內存區域:

程序計數器

程序計數器是很小的一塊內存空間,它的作用主要是起到一個線程中字節碼執行行號的作用,換句話說,它標識了下一句應該直接的字節碼,舉個例子,常用的分支,循環,跳轉,異常處理等基礎功能都是由這個程序計數器實現的。

在Java虛擬機中,多線程是通過線程輪流切換並分配處理器執行時間來實現的,使用這一種cpu調度方式意味着任何一個確定的時候,一個處理器都只會處理一條線程中的指令。所以爲了線程切換後,能恢復到正確的位置,就用到了程序計數器,由於是多線程,每個線程間還需要獨立的程序計數器,存儲這個東西的程序計數器的內存區域,叫做“線程私有”內存。這很好理解,因爲每個線程的實際執行位置幾乎是不一樣的。

需要說明的是,在執行Java方法的時候,程序計數器就如同它的功能一樣,標識字節碼的下一行執行代碼,然而,若是執行的Native方法,那麼,這個計數器就爲空。而且此區域是唯一一個JVM沒有規定任何OutOfMemoryError的區域。

Java虛擬機棧

Java虛擬機棧可以類比C語言中的棧,每個方法執行都會創建一個棧幀用於存儲局部變量表,操作數棧,動態鏈接,方法出口信息等跟方法執行相關的信息,一個方法的執行到結束就對應着一個棧幀的入棧到出棧的過程。同樣地,很容易得,棧也是線程私有的,它的生命週期與線程相同。

這裏有個需要注意的地方:

當線程請求的棧深度大於虛擬機所允許的最大值時,那麼,其會拋出 StackOverflowError,若是虛擬機棧時允許拓展的,那麼,在這種情況下,深度還是不夠,則會拋出OutOfMemoryError。

本地方法棧

這塊區域很好理解,與虛擬機棧所有的功能非常類似,但是是執行的本地方法。與虛擬機棧一樣,也會拋出一樣的異常。

Java堆

這一塊是比較重點的內容,也是大部分程序員比較關心的一塊。Java堆是被所有線程所共享的一塊區域,在虛擬機啓動時就創建完成。此區域的唯一目的就是存放對象實例,幾乎所有的對象實例都是在該區域進行分配內存的,注意是幾乎,隨着技術的發展,以及一些比較激進的優化策略,這都不是絕對的。

關於Java堆的內容,我不準備在這裏展開,之後會另外寫Blog來看看。

方法區

和Java堆一樣,方法區也是所有線程所共享的內存區域,它主要是用來存儲已被虛擬機加載的類信息,常量,靜態變量等數據,注意,這裏說到裏一些靜態變量等數據,這可以說明GC在這個區域是比較少發生的,或者說在這裏GC很難有所收穫,在這裏GC主要是對常量池以及類型卸載。

運行時常量

運行時常量屬於方法區的一部分,它用於存放編譯期生成的各種字面量和符號引用。運行時常量區具有動態性,換句話說,Java並不要求,常量一定只有編譯期才能夠產生,也就是並非預置入class文件中常量池的內容才能進入,運行期間也可以將新的常量放入池裏。

直接內存

就如同它的名字一樣,這部分內存並不屬於虛擬機所管轄的內存區域,Java提供了NIO API來讓用戶操作這部分內存,以此來獲取更高的效率,因爲減少了複製次數。

Java 對象

Java 對象的創建

虛擬機在遇到一條New 指令時,首先會去檢查這個指令的參數是否能在常量池中定位到一個類的引用,並且檢查這個符號引用代表的class,是否以i紀念館被加載,解析,初始化。這時候,若是沒有以上流程的話,則去執行相應的類加載流程。這段話其實不用想太多,這樣去理解;

在執行New之前,虛擬機會去檢查代表這個對象的模版class是否已經加載到內存中了,若是沒有加載的話,它不知道這個對象如何創建對不對?所以會根據之前提到的,從運行時常量池中,獲取到該class的符號引用,並將它初始化。

上一步之後,需要先給新生對象分配內容,對象所需要的內存在類加載完成後就可以完全確定了,這很好理解,有了模版,創建的東西都是在它的規劃之類,這裏分配對象內存等同於把一塊確定大小的內存從Java堆中劃出來。虛擬機有兩種分配內存的方式:

  • 空閒列表
  • 指針碰撞

先不用糾結這兩種方法,一句話來說,就是根據內存是否是連續規整來使用不同的內存分配劃分。

在內存分配完畢之後,虛擬機需要將分配的內存空間都初始化爲零值。這一步操作就保證了對象的實例字段在Java代碼中不用賦值就可以直接使用,因爲此時,程序能訪問到這些字段的數據類型所對應的零值。

到這一步,一個對象就已經誕生了,但是它現在還有實際的意義,需要繼續對它初始化,這一步是初始化用戶的內容,執行\方法,此方法執行完成後,按照用戶的思路,這時候才真正完成了對象的初始化。

Java對象的內存佈局

Java對象的內存佈局主要分爲以下三部分:

  • 對象頭
  • 實例數據
  • 對齊填充

對於對象頭,主要是存儲了一些對象自身的運行時數據,這展開就比較多,比較複雜了。可以這麼理解,類比,網絡協議Http的Http Header,它裏面存儲了很多用於客戶端,服務端信令交互的內容,都是爲了說明自身目前的狀態,以便別人瞭解而做出相應的處理。實際上,Java 對象頭也是如此的。

實例數據這是對象真正存儲有效信息的區域

對齊填充沒什麼好討論的,主要就是爲了方便處理數據。

Java內存錯誤

既然是Java內存區域,那有錯自然是內存相關錯誤,之前有說了,幾乎所有的Java內存區域都會拋出OutOfMemoryError,除了程序計數器在執行native方法的時候。

總結下可能出現的內存區域錯誤:

  • Java 堆溢出
  • 虛擬機棧溢出
  • 本地方法棧溢出
  • 方法區與運行時常量池溢出
  • 本機直接內存溢出
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章