JVM 虛擬機與對象創(chuàng)建過程

問題:

Q1:什么是 JVM?

JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一種用于計算設(shè)備的規(guī)范,它是一個虛構(gòu)出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現(xiàn)的。

Q2:JVM、Dalvik 和 ART

android5.0之前使用的虛擬機是Dalvik虛擬機,這個虛擬機在JVM的基礎(chǔ)上做了一定的優(yōu)化。android5.0之后,android采用了新的虛擬機ART。

Q3:JVM 內(nèi)存模型,也就是說 JVM 包含什么?

Q4:虛擬機中對象的創(chuàng)建過程

這些問題在下文會有敘述。

一、Java 技術(shù)體系

Java 的技術(shù)體系主要包含下列內(nèi)容:

  • Java 程序設(shè)計語言:也就是 Java 語言,包括各種定義、規(guī)范等;
  • Java 虛擬機:各種硬件平臺上的虛擬機;
  • Class 文件格式:簡單的說就是把 Java 代碼轉(zhuǎn)換為二進制、格式為 Class 的文件,方便在各個平臺被虛擬機讀取;
  • Java API 類庫:Java 提供的 API,方便開發(fā)者日常使用。比如日歷 Calendar,數(shù)學計算 Math 等;
  • 來自商業(yè)機構(gòu)和開源社區(qū)的第三方 Java 類庫:也就是第三方庫。
各部件關(guān)系

關(guān)于 Java 運行流程,先看前半部分。Java 源代碼經(jīng)過編譯器處理過,生成 .class 文件。這是一種二進制文件,在虛擬機運行后,再通過 ClassLoader.class 文件加載到虛擬機中運行。虛擬機加載文件流程后面會記錄。

二、JVM 虛擬機

Java 虛擬機是一種抽象化的計算機,在實際上的計算機模擬各種計算機功能來實現(xiàn)。

正是因為模擬了一臺計算機,所以可以方便地在其它計算機環(huán)境下運行。
就比如市面上的 GBA、NES 游戲模擬器,模擬了游戲運行環(huán)境,可以在手機、電腦等設(shè)備上運行,實現(xiàn)執(zhí)行游戲文件的功能。Java 也是類似的過程實現(xiàn)了跨平臺運行。

2.1 JVM 啟動流程
  1. 為 JVM 分配內(nèi)存空間;
    這部分由運行平臺實現(xiàn),不同平臺會根據(jù)環(huán)境以及 JVM 配置為其分配空間。一旦運行成功,JVM 把內(nèi)存劃分為若干個不同的區(qū)域。
JVM 內(nèi)存模型
  1. 創(chuàng)建引導(dǎo)類加載器,加載系統(tǒng)類到內(nèi)存空間;
    JVM 創(chuàng)建成功之后,會實例化一個引導(dǎo)類加載器(Bootstrap Classloader),它會會讀取 {JRE_HOME}/lib 下的 jar 包和配置,并將一些系統(tǒng)類加載到方法區(qū)中。比如 java.lang.String, java.lang.Object 都是這時加載的。

一、JVM 虛擬機

JVM 虛擬機運行時包含以下五大區(qū)域:


JVM 內(nèi)存模型圖
  • 程序計數(shù)器
  • Java虛擬機棧
  • 本地方法棧
  • 方法區(qū)
1.1 程序計數(shù)器

Java 文件通過編譯器編譯成 java 字節(jié)碼文件(也就是 .class 文件),而 java 虛擬機執(zhí)行的就是字節(jié)碼文件。如果想了解什么是字節(jié)碼,可以閱讀下面文章:

JVM:這次一定要搞懂字節(jié)碼

那么程序計數(shù)器就是記錄當前線程執(zhí)行到哪個字節(jié)碼指令的地址,關(guān)于程序計數(shù)器的資料記錄:

程序計數(shù)器是一塊較小的內(nèi)存空間,它可以看作是當前線程執(zhí)行的字節(jié)碼的行號指示器。在虛擬機的概念模型當中,字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選擇下一條需要執(zhí)行的字節(jié)碼指令。

程序計數(shù)器有兩個作用:
  1. 字節(jié)碼解釋器通過改變程序計數(shù)器來依次讀取指令,從而實現(xiàn)代碼的流程控制,如:順序執(zhí)行、選擇、循環(huán)、異常處理。
  2. 在多線程的情況下,程序計數(shù)器用于記錄當前線程執(zhí)行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。

注:如果當前線程正在執(zhí)行的是一個本地方法,那么此時程序計數(shù)器為空。 因為本地方法是操作計算機的語言,和字節(jié)碼無關(guān)。

程序計數(shù)器的特點:
  1. 是一塊較小的存儲空間。
  2. 線程私有。每條線程都有一個程序計數(shù)器。
  3. 是唯一一個不會出現(xiàn)OutOfMemoryError的內(nèi)存區(qū)域。
  4. 生命周期隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的結(jié)束而死亡。

通俗的總結(jié)一下:程序計數(shù)器用一塊很小的內(nèi)存空間來記錄某線程當前執(zhí)行到的字節(jié)碼行號(第xxx行)。

1.2 Java 虛擬機棧(JVM Stack)

Java虛擬機棧是描述Java方法運行過程的內(nèi)存模型。

當一個方法即將運行時,Java 虛擬機棧首先會在該區(qū)域為該方法創(chuàng)建一個“棧幀”。

棧幀中包括 局部變量表(用于儲存要創(chuàng)建的局部變量)、操作數(shù)棧(虛擬機的工作區(qū)--彈出數(shù)據(jù),執(zhí)行運算,再把結(jié)果壓回操作數(shù)棧)、動態(tài)鏈接(需要時執(zhí)向所需要的資源地址)、方法出口信息(方法要結(jié)束時的信息)、等。

當這個方法執(zhí)行完畢以后,該方法所對應(yīng)的棧幀將會出棧,并釋放內(nèi)存空間。而Java虛擬機棧管理著這些棧幀。

Java 虛擬機棧的特點:
  1. 局部變量表的創(chuàng)建是隨著棧幀的創(chuàng)建而創(chuàng)建,而且局部變量表的大小是在編譯時期就確定了,在方法運行過程中該表大小不會改變。
  2. Java 虛擬機棧會出現(xiàn)兩種異常:StackOverFlowError 和 OutOfMemoryError。
    2.1 StackOverFlowError:Java 虛擬機棧不允許動態(tài)擴展內(nèi)存的情況下線程請求棧的深度超過當前 Java 虛擬機棧的最大深度。Java 虛擬機棧創(chuàng)建時有一定的深度,當棧幀越大、數(shù)量越多的時候棧深度就越小,但內(nèi)存并不一定就用完了。
    2.2 OutOfMemoryError:允許動態(tài)擴展 Java 虛擬機棧內(nèi)存時,且當線程請求棧時內(nèi)存用完了無法再進行擴展,會拋出該異常。
  3. Java 虛擬機棧也是線程私有,每個線程持有各自的虛擬機棧,跟隨線程的生命周期。
1.3 本地方法棧

本地方法棧和 Java 虛擬機棧功能類似,它是本地方法運行的內(nèi)存模型,執(zhí)行的是 Native 方法。

本地方法執(zhí)行的時候也會在本地方法棧創(chuàng)建棧幀,用于存放該本地方法的局部變量表、操作數(shù)棧、動態(tài)鏈接、接口信息等。

方法執(zhí)行完畢也會釋放內(nèi)存空間,出現(xiàn)異常也會拋出 Stack Overflow和 OutOfMemoryError 異常。

1.4 堆(heap)

幾乎所有對象的實例都在堆中分配內(nèi)存。也就是說堆中保存了大多數(shù)對象的實例。

堆的特點:
  1. 線程共享,上圖也有注明。
  2. JVM 啟動時創(chuàng)建。
  3. 垃圾回收的主要場所。
  4. 可以進一步細分為:新生代、老年代。
    新生代又可以細分為:Eden、From Survior、To Survior。
    這樣劃分的目的是為了使 Jvm 更好地管理堆內(nèi)存中的對象,包括內(nèi)存的分配和回收。
  5. 堆的大小既可以固定也可以動態(tài)擴展。在 Android 運行環(huán)境下,一般虛擬機堆的大小是可以擴展的,不同的設(shè)備為每個 App 分配的內(nèi)存是不確定的但有一個最大值。當某應(yīng)用可用內(nèi)存被使用完,又去請求內(nèi)存分配時就會拋出 OutOfMemoryError。
1.5 方法區(qū)

Jvm 棧規(guī)范定義方法區(qū)是堆的一個邏輯部分。

方法區(qū)中存放的是已經(jīng)被 JVM 加載的類信息(class)、常量(final static,enum等)、靜態(tài)變量(static),即經(jīng)過編譯器編譯后的代碼(static{})等。

方法區(qū)的特點:
  1. 線程共享:方法區(qū)是堆的一個邏輯部分,都是線程共享的。整個虛擬機只有一個方法區(qū)。
  2. 持久代:方法區(qū)中的信息一般需要長期存在,因此根據(jù)堆的邏輯來劃分,把方法區(qū)稱為持久代。
  3. 內(nèi)存回收效率低:因為方法區(qū)的信息需要長期存在,回收可能會丟失數(shù)據(jù)。
    方法區(qū)內(nèi)存回收的主要目標是:對常量池的回收和對類型的卸載。
  4. JVM 對方法區(qū)要求寬松:你回不回收內(nèi)存都可以,想要大內(nèi)存可以申請。
運行時常量池:

方法區(qū)中存放三種數(shù)據(jù):類信息、常量、靜態(tài)變量。其中常量儲存在運行時常量池中。

當某個類被 JVM 加載后,class 文件中的常量就存放在方法區(qū)中的運行時常量池中。并且在運行期間可以向常量池中添加新的常量。比如:String.intern() 方法可以向運行時常量池中添加字符串常量。

二、HotSpot 虛擬機

HotSpot 虛擬機是虛擬機的一個實現(xiàn),上面所說虛擬機更像是概念上的,而 HotSpot 虛擬機是依據(jù)理論創(chuàng)造出來的虛擬機。

HotSpot的正式發(fā)布名稱為"Java HotSpot Performance Engine",是Java虛擬機的一個實現(xiàn),包含了服務(wù)器版和桌面應(yīng)用程序版,現(xiàn)時由Oracle維護并發(fā)布。

接下來記錄 HotSpot 虛擬機對于對象的創(chuàng)建、內(nèi)存分配等過程。

2.1 對象的創(chuàng)建

Object object = new Object();
  1. 當虛擬機遇到一條 new 指令時,首先檢查常量池中是否有該對象所屬類的符號引用,并且檢查該類是否被加載、解析和初始化:
  • 如果常量池沒有該類的符號引用,拋出 ClassNotFoundException;
  • 如果存在并經(jīng)過 JVM 的執(zhí)行、解析和初始化等一系列工作,執(zhí)行下一步工作;
  1. 類加載完成后,虛擬機將為新生對象分配內(nèi)存。
    一個對象所需內(nèi)存,在 JVM 把該類加載進入方法區(qū)的時候就已經(jīng)確定了,且一個類所產(chǎn)生的對象所占內(nèi)存大小是一樣的。
  2. 從堆中劃分一塊相應(yīng)大小的內(nèi)存給新的對象:
    給對象分配內(nèi)存有兩種方式:
  • 指針碰撞(Bump the Pointer)
    如果堆中的內(nèi)存是規(guī)整的,也就是說使用中的內(nèi)存在一邊,空閑內(nèi)存在另一邊,中間有一個指針作為分界點指示器。那么只需要把指針向空閑區(qū)域挪動一段與新對象大小相等的距離。
    什么樣的情況下堆內(nèi)存是規(guī)整的呢?當然是經(jīng)過整理的,比如 JVM 的垃圾收集器采用復(fù)制算法或標記-整理算法,那么堆內(nèi)存是相對規(guī)整的。
  • 空閑列表(Free List)
    如果堆中的內(nèi)存不是規(guī)整的,而是已使用內(nèi)存和未使用內(nèi)存交錯的,那么就需要虛擬機維護一個列表并記錄哪些內(nèi)存是可用的。在可用內(nèi)存中找到一塊足夠大的空間劃分給新對象。
    JVM 的垃圾收集器采用標記-清除算法,就會使用這種方式分配內(nèi)存。

選擇哪種分配方式由 Java 堆是否規(guī)整決定,而 Java 堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。

  1. 為新對象中的成員變量附上初始值;
  2. 設(shè)置并保存對象頭信息(Object Header),對象頭信息包括該對象是哪個類實例、對象的哈希碼、對象的 GC 分代年齡等信息。
  3. 一般來說,執(zhí)行 new 指令之后會接著執(zhí)行 <init> 方法,把對象按照程序員規(guī)定的構(gòu)造函數(shù)進行初始化。

經(jīng)過以上步驟,對象的創(chuàng)建過程就完成了。

2.2 對象的內(nèi)存模型

一般情況下,一個對象包括成員變量,構(gòu)造函數(shù),成員方法。那么在內(nèi)存中分為三個部分:對象頭、實例數(shù)據(jù)、對齊填充。

  • 對象頭(Header),HotSpot 虛擬機的對象包括兩部分信息:
    (1) 第一部分用于存儲對象自身運行時數(shù)據(jù),包括哈希碼、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖。
    (2) 第二部分是類型指針,虛擬器通過這個指針來確定該對象是哪個類的實例。
  • 實例數(shù)據(jù)(Instance Data):
    對象存儲的真正有效信息,也就是成員變量的值,包括本類和父類的成員變量的值。
  • 對齊填充(Padding):
    不是必然存在,僅僅起著占位符的作用。HotSpot 要求對象的總長度必須是 8 字節(jié)的整數(shù)倍,當對象實例數(shù)據(jù)部分沒有對齊時,就需要通過對齊補充來補全。

2.3 對象的訪問

我們知道引用類型的變量存儲為一個地址,對象的訪問方式取決于虛擬機的實現(xiàn)。主流的訪問方式有兩種,句柄式訪問和直接指針訪問:

  1. 句柄式訪問
    堆中有一塊內(nèi)存空間叫 "句柄池",用于存放所有對象的地址和對象所屬類信息。
    引用類型變量存放的地址是該對象在句柄池中的地址,訪問對象時首先通過該對象的句柄,然后根據(jù)句柄再訪問該對象。
  2. 直接指針訪問
    通過引用直接訪問該對象的地址,但對象所在內(nèi)存空間需要額外的內(nèi)存策略來記錄該對象在方法區(qū)中的類信息的地址。

這兩種訪問方式各有利弊,句柄式訪問的好處是實際引用對象改變時只需更改該句柄的指針;而使用直接指針訪問速度較快,但需要額外的策略儲存被引用對象的類信息。
對于 HotSpot,使用的是直接指針訪問的方式。

參考資料:

深入理解JVM(一)——JVM內(nèi)存模型

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容