問題:
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)于 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 啟動流程
- 為 JVM 分配內(nèi)存空間;
這部分由運行平臺實現(xiàn),不同平臺會根據(jù)環(huán)境以及 JVM 配置為其分配空間。一旦運行成功,JVM 把內(nèi)存劃分為若干個不同的區(qū)域。

- 創(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ū)域:
- 程序計數(shù)器
- Java虛擬機棧
- 本地方法棧
- 堆
- 方法區(qū)
1.1 程序計數(shù)器
Java 文件通過編譯器編譯成 java 字節(jié)碼文件(也就是 .class 文件),而 java 虛擬機執(zhí)行的就是字節(jié)碼文件。如果想了解什么是字節(jié)碼,可以閱讀下面文章:
那么程序計數(shù)器就是記錄當前線程執(zhí)行到哪個字節(jié)碼指令的地址,關(guān)于程序計數(shù)器的資料記錄:
程序計數(shù)器是一塊較小的內(nèi)存空間,它可以看作是當前線程執(zhí)行的字節(jié)碼的行號指示器。在虛擬機的概念模型當中,字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選擇下一條需要執(zhí)行的字節(jié)碼指令。
程序計數(shù)器有兩個作用:
- 字節(jié)碼解釋器通過改變程序計數(shù)器來依次讀取指令,從而實現(xiàn)代碼的流程控制,如:順序執(zhí)行、選擇、循環(huán)、異常處理。
- 在多線程的情況下,程序計數(shù)器用于記錄當前線程執(zhí)行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。
注:如果當前線程正在執(zhí)行的是一個本地方法,那么此時程序計數(shù)器為空。 因為本地方法是操作計算機的語言,和字節(jié)碼無關(guān)。
程序計數(shù)器的特點:
- 是一塊較小的存儲空間。
- 線程私有。每條線程都有一個程序計數(shù)器。
- 是唯一一個不會出現(xiàn)OutOfMemoryError的內(nèi)存區(qū)域。
- 生命周期隨著線程的創(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 虛擬機棧的特點:
- 局部變量表的創(chuàng)建是隨著棧幀的創(chuàng)建而創(chuàng)建,而且局部變量表的大小是在編譯時期就確定了,在方法運行過程中該表大小不會改變。
- 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)存用完了無法再進行擴展,會拋出該異常。 - 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ù)對象的實例。
堆的特點:
- 線程共享,上圖也有注明。
- JVM 啟動時創(chuàng)建。
- 垃圾回收的主要場所。
- 可以進一步細分為:新生代、老年代。
新生代又可以細分為:Eden、From Survior、To Survior。
這樣劃分的目的是為了使 Jvm 更好地管理堆內(nèi)存中的對象,包括內(nèi)存的分配和回收。 - 堆的大小既可以固定也可以動態(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ū)的特點:
- 線程共享:方法區(qū)是堆的一個邏輯部分,都是線程共享的。整個虛擬機只有一個方法區(qū)。
- 持久代:方法區(qū)中的信息一般需要長期存在,因此根據(jù)堆的邏輯來劃分,把方法區(qū)稱為持久代。
- 內(nèi)存回收效率低:因為方法區(qū)的信息需要長期存在,回收可能會丟失數(shù)據(jù)。
方法區(qū)內(nèi)存回收的主要目標是:對常量池的回收和對類型的卸載。 - 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();
- 當虛擬機遇到一條 new 指令時,首先檢查常量池中是否有該對象所屬類的符號引用,并且檢查該類是否被加載、解析和初始化:
- 如果常量池沒有該類的符號引用,拋出 ClassNotFoundException;
- 如果存在并經(jīng)過 JVM 的執(zhí)行、解析和初始化等一系列工作,執(zhí)行下一步工作;
- 類加載完成后,虛擬機將為新生對象分配內(nèi)存。
一個對象所需內(nèi)存,在 JVM 把該類加載進入方法區(qū)的時候就已經(jīng)確定了,且一個類所產(chǎn)生的對象所占內(nèi)存大小是一樣的。 - 從堆中劃分一塊相應(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ī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
- 為新對象中的成員變量附上初始值;
- 設(shè)置并保存對象頭信息(Object Header),對象頭信息包括該對象是哪個類實例、對象的哈希碼、對象的 GC 分代年齡等信息。
- 一般來說,執(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)。主流的訪問方式有兩種,句柄式訪問和直接指針訪問:
- 句柄式訪問
堆中有一塊內(nèi)存空間叫 "句柄池",用于存放所有對象的地址和對象所屬類信息。
引用類型變量存放的地址是該對象在句柄池中的地址,訪問對象時首先通過該對象的句柄,然后根據(jù)句柄再訪問該對象。 - 直接指針訪問
通過引用直接訪問該對象的地址,但對象所在內(nèi)存空間需要額外的內(nèi)存策略來記錄該對象在方法區(qū)中的類信息的地址。
這兩種訪問方式各有利弊,句柄式訪問的好處是實際引用對象改變時只需更改該句柄的指針;而使用直接指針訪問速度較快,但需要額外的策略儲存被引用對象的類信息。
對于 HotSpot,使用的是直接指針訪問的方式。
參考資料: