概述
我們常說的JDK(Java Development Kit)包含了Java語言、Java虛擬機(jī)和Java API類庫這三部分映之,是Java程序開發(fā)的最小環(huán)境。而JRE(Java Runtime Environment)包含了Java API中的Java SE API子集和Java虛擬機(jī)這兩部分蜡坊,是Java程序運(yùn)行的標(biāo)準(zhǔn)環(huán)境杠输。可以看出Java虛擬機(jī)的重要性秕衙,它是整個(gè)Java平臺的基石蠢甲,是Java語言編譯代碼的運(yùn)行平臺。你可以把Java虛擬機(jī)看做一個(gè)抽象的計(jì)算機(jī)据忘,它有各種指令集和運(yùn)行時(shí)數(shù)據(jù)區(qū)域鹦牛。
雖然叫Java虛擬機(jī),但其實(shí)它能運(yùn)行的語言不僅僅是Java勇吊,還包括Kotlin曼追、Groovy等。同時(shí)需要注意的是萧福,Android中的Dalvik和ART虛擬機(jī)并不屬于Java虛擬機(jī)拉鹃。
Java虛擬機(jī)的執(zhí)行流程
當(dāng)我們執(zhí)行一個(gè)Java程序時(shí),它的執(zhí)行流程如下圖所示:
從圖中可以發(fā)現(xiàn)鲫忍,java程序的執(zhí)行流程可以分為四個(gè)步驟:編輯源代碼膏燕、編譯生成Class文件、加載Class文件悟民、執(zhí)行Class文件里的字節(jié)碼指令坝辫。當(dāng)一個(gè)Java'文件經(jīng)過Java編譯器編譯后會(huì)生成Class文件,這個(gè)Class文件會(huì)由Java虛擬機(jī)處理射亏。Java虛擬機(jī)與Java語言并沒有什么必然的聯(lián)系近忙,它只與特定的二進(jìn)制文件:Class文件有關(guān)竭业。因此無論任何語言,只要能編譯成Class文件及舍,就可以被Java虛擬機(jī)識別并執(zhí)行未辆。
Java虛擬機(jī)結(jié)構(gòu)
Java虛擬機(jī)結(jié)構(gòu)包含運(yùn)行時(shí)數(shù)據(jù)區(qū)域、執(zhí)行引擎锯玛、本地庫接口和本地方法庫咐柜。但上圖中所示的類加載子系統(tǒng)并不屬于Java虛擬機(jī)的內(nèi)部結(jié)構(gòu)。圖中還標(biāo)出了線程共享區(qū)域和線程私有的區(qū)域攘残,比如方法區(qū)和Java堆就是所有線程共享的數(shù)據(jù)區(qū)域拙友。
Class文件格式
Java(.java)文件被編譯后會(huì)生成Class(.class)文件,這種二進(jìn)制格式文件不依賴于特定的硬件和操作系統(tǒng)歼郭。每個(gè)Class文件中都對應(yīng)著唯一的類或者接口的定義信息(內(nèi)部類和匿名內(nèi)部類編譯后也會(huì)生成單獨(dú)的Class文件)遗契。但是類或者接口并不一定定義在文件中,比如類和接口可以通過類加載器直接生成病曾。Class文件的格式如下:
ClassFile具有很強(qiáng)的描述能力牍蜂,包含了很多關(guān)鍵的信息,其中部分字段前面的u4知态,u2表示"基本數(shù)據(jù)類型"捷兰,class文件的基本數(shù)據(jù)類型如下:
- u1:1字節(jié),無符號類型
- u2:2字節(jié)负敏,無符號類型
- u4:4字節(jié),無符號類型
- u8:8字節(jié)秘蛇,無符號類型
類的生命周期
一個(gè)Java類的Class文件被加載到Java虛擬機(jī)內(nèi)存中到從內(nèi)存中卸載的過程被稱為類的生命周期其做。包括以下幾個(gè)階段:加載、鏈接赁还、初始化妖泄、使用和卸載。其中鏈接又可以分為三個(gè)階段:驗(yàn)證艘策、準(zhǔn)備和解析蹈胡。
廣義上來說,也是經(jīng)常被面試問到的朋蔫,類的加載機(jī)制是指:加載罚渐、驗(yàn)證、準(zhǔn)備驯妄、解析和初始化這5個(gè)階段荷并。這幾個(gè)階段分別完成以下工作:
(1)加載:查找并加載Class文件。加載階段主要做了三件事:
- 根據(jù)特定名稱查找類或接口類型的二進(jìn)制字節(jié)流青扔,這件事是由Java虛擬機(jī)外部的類加載子系統(tǒng)來完成的源织。
- 將這個(gè)二進(jìn)制字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對象翩伪,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口。
(2)鏈接:包括驗(yàn)證谈息、準(zhǔn)備和解析缘屹。 - 驗(yàn)證:驗(yàn)證載入的Class文件數(shù)據(jù)的正確性。
- 準(zhǔn)備:為類的靜態(tài)變量分配內(nèi)存侠仇,并用數(shù)據(jù)類型默認(rèn)值初始化這些字段轻姿。
- 解析:虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用。(符號引用是用一組符號描述所引用的目標(biāo)傅瞻;直接引用是指向目標(biāo)的指針)
(3)初始化:將類變量初始化為正確的初始值踢代。這個(gè)階段主要包括兩個(gè)過程:
- 如果類存在直接的父類并且這個(gè)類還沒有被初始化,那么就先初始化父類;
- 如果類中存在初始化語句嗅骄,就依次執(zhí)行這些初始化語句胳挎。
類加載子系統(tǒng)
類加載子系統(tǒng)通過多種類型的類加載器來完成查找和加載Class文件到Java虛擬機(jī)中。Java中有三種默認(rèn)實(shí)現(xiàn)的類加載器:
(1)Bootstrap ClassLoader(引導(dǎo)類加載器)
用C/C++代碼實(shí)現(xiàn)的加載器溺森,用于加載指定的JDK核心庫慕爬,比如java.lang、java.util等這些系統(tǒng)類屏积。它用來加載以下路徑下的類庫:
- $JAVA_HOME/jre/lib目錄
- -Xbootclasspath參數(shù)指定的目錄
Java虛擬機(jī)的啟動(dòng)就是通過引導(dǎo)類加載器創(chuàng)建一個(gè)初始類來完成的医窿。由于類加載器是使用平臺相關(guān)的C/C++語言實(shí)現(xiàn)的。所以該加載器不能被java代碼訪問到炊林,但是我們可以查詢某個(gè)類是否被引導(dǎo)類加載器加載過姥卢。
(2)Extensions ClassLoader(擴(kuò)展類加載器)
用于加載Java的擴(kuò)展庫,提供除了系統(tǒng)類之外的額外功能渣聚。它用來加載以下路徑下的類庫:
- $JAVA_HOME/jre/lib/ext目錄
- 系統(tǒng)屬性java.ext.dir所指定的目錄
(3)Application ClassLoader(應(yīng)用類加載器)
又稱作System ClassLoader独榴,因?yàn)檫@個(gè)類加載器可以通過ClassLoader.getSystemClassLoader()方法獲取到。它由來加載以下路徑下的類庫:
- 當(dāng)前應(yīng)用程序Classpath目錄
- 系統(tǒng)屬性java.class.path指定的目錄
除了系統(tǒng)自帶的類加載器奕枝,用戶還可以實(shí)現(xiàn)自定義加載器棺榔,它是通過繼承java.lang.ClassLoader類的方式來實(shí)現(xiàn)自己的類加載器。
運(yùn)行時(shí)數(shù)據(jù)區(qū)域
把Java的內(nèi)存簡單分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack)隘道,這種說法不夠準(zhǔn)確症歇。Java的內(nèi)存區(qū)域劃分實(shí)際上遠(yuǎn)比這復(fù)雜。Java虛擬機(jī)在執(zhí)行Java程序的過程中谭梗,會(huì)對它所管理的內(nèi)存劃分為不同的數(shù)據(jù)區(qū)域忘晤。
程序計(jì)數(shù)器
為了保證程序能夠連續(xù)地執(zhí)行下去,處理器必須具有某些手段來確定下一條執(zhí)行的地址默辨,這就是程序計(jì)數(shù)器的作用德频。它是一塊很小的內(nèi)存空間。在虛擬機(jī)概念模型中缩幸,字節(jié)碼解釋器工作時(shí)(執(zhí)行字節(jié)碼指令)就是通過改變程序計(jì)數(shù)器里存儲的地址壹置,來選取下一條需要的字節(jié)碼指令竞思。Java虛擬機(jī)的多線程是通過輪流切換并分配處理器執(zhí)行時(shí)間的方式實(shí)現(xiàn)的。在一個(gè)確定的時(shí)刻钞护,只有一個(gè)處理器執(zhí)行一個(gè)線程中的指令盖喷,為了在線程來回切換后能恢復(fù)到正確的執(zhí)行位置,每個(gè)線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器难咕,也就是說课梳,程序計(jì)數(shù)器是每個(gè)線程私有的。如果線程執(zhí)行的方法不是Native方法余佃,則程序計(jì)數(shù)器保存正在執(zhí)行的字節(jié)碼指令地址暮刃,如果是Native方法,則程序計(jì)數(shù)器的值為空(Undefined)爆土。程序計(jì)數(shù)器是Java虛擬機(jī)規(guī)范中唯一沒有規(guī)定任何OutOfMemoryError情況的數(shù)據(jù)區(qū)域椭懊。
實(shí)際上除了恢復(fù)線程執(zhí)行時(shí)會(huì)用到程序計(jì)數(shù)器之外,其它一些我們熟悉的判斷分支操作步势、循環(huán)操作氧猬、跳轉(zhuǎn)和異常處理也都需要依賴程序計(jì)數(shù)器來完成。
Java虛擬機(jī)棧
每個(gè)Java虛擬機(jī)線程都有一個(gè)線程私有的Java虛擬機(jī)棧坏瘩。它的生命周期和線程相同盅抚,與線程同時(shí)創(chuàng)建。Java虛擬機(jī)棧存儲的是線程中Java方法的調(diào)用狀態(tài)倔矾,包括局部變量妄均、參數(shù)、返回值以及運(yùn)算的中間結(jié)果等哪自。一個(gè)Java虛擬機(jī)棧包含了多個(gè)棧幀丛晦,一個(gè)棧幀用來存儲一個(gè)方法的局部變量表、操作數(shù)棧提陶、動(dòng)態(tài)鏈接、方法出口等信息匹层。當(dāng)線程執(zhí)行一個(gè)Java方法時(shí)隙笆,虛擬機(jī)會(huì)壓入一個(gè)新的棧幀到該線程的Java虛擬機(jī)棧中,該方法執(zhí)行完成后升筏,這個(gè)棧幀就會(huì)從Java虛擬機(jī)棧中彈出撑柔。具體過程可參考此處。我們平時(shí)所說的棧內(nèi)存指的就是Java虛擬機(jī)棧您访,Java虛擬機(jī)規(guī)范中铅忿,定義了兩種異常情況:
- 如果線程請求分配的棧容量超過Java虛擬機(jī)所允許的最大容量,Java虛擬機(jī)就會(huì)拋出StackOverflowError灵汪。實(shí)際開發(fā)中檀训,沒有結(jié)束條件的遞歸方法調(diào)用柑潦,是常見的產(chǎn)生StackOverflowError的場景。
- 如果Java虛擬機(jī)椌欤可以動(dòng)態(tài)擴(kuò)展(大部分Java虛擬機(jī)的虛擬機(jī)棧都可以動(dòng)態(tài)擴(kuò)展)渗鬼,但是擴(kuò)展時(shí)無法申請到足夠的內(nèi)存,或者在創(chuàng)建新的線程時(shí)沒有足夠的內(nèi)存來創(chuàng)建對應(yīng)的虛擬機(jī)棧荧琼,則會(huì)拋出OutOfMemoryError譬胎。
在學(xué)習(xí)JVM的過程中,經(jīng)常會(huì)看到一句話:
JVM是基于棧的解釋器執(zhí)行的命锄,DVM是基于寄存器解釋器執(zhí)行的
這里說道的“基于椦咔牵”,指的就是虛擬機(jī)棧脐恩。虛擬機(jī)棧的初衷是用來描述Java方法執(zhí)行的內(nèi)存模型镐侯。
關(guān)于棧幀
棧幀是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),每一個(gè)線程在執(zhí)行一個(gè)方法時(shí)被盈,都會(huì)為這個(gè)方法創(chuàng)建一個(gè)棧幀析孽。每個(gè)棧幀內(nèi)部包含局部變量表,操作數(shù)棧只怎,動(dòng)態(tài)鏈接和返回地址等袜瞬。
局部變量表——是變量值的存儲空間,我們調(diào)用方法時(shí)傳遞的參數(shù)身堡,以及在方法內(nèi)部創(chuàng)建的局部變量都保存在局部變量表中邓尤。在 Java 編譯成 class 文件的時(shí)候,就會(huì)在方法的 Code 屬性表中的 max_locals 數(shù)據(jù)項(xiàng)中贴谎,確定該方法需要分配的最大局部變量表的容量汞扎。注意:系統(tǒng)不會(huì)為局部變量賦予初始值(實(shí)例變量和類變量都會(huì)被賦予初始值),也就是說不存在靜態(tài)變量那樣的準(zhǔn)備階段擅这。
操作數(shù)棾浩牵——同局部變量表一樣,操作數(shù)棧的最大深度也在編譯的時(shí)候?qū)懭敕椒ǖ腃ode屬性表中的max_stacks數(shù)據(jù)項(xiàng)中仲翎。棧中的元素可以是任意Java數(shù)據(jù)類型痹扇,包括long和double。當(dāng)一個(gè)方法剛剛開始執(zhí)行的時(shí)候溯香,這個(gè)方法的操作數(shù)棧是空的鲫构。在方法執(zhí)行的過程中,會(huì)有各種字節(jié)碼指令被壓入和彈出操作數(shù)棧玫坛。
動(dòng)態(tài)鏈接——它是為了支持方法調(diào)用過程中的動(dòng)態(tài)鏈接(Dynamic Linking)结笨。在一個(gè)class文件中,一個(gè)方法要調(diào)用其他方法,需要將這些方法的符號引用(方法名)轉(zhuǎn)化為其所在內(nèi)存地址中的直接引用炕吸,而符號引用存在于方法區(qū)中伐憾。Java 虛擬機(jī)棧中,每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧所屬方法的符號引用算途,持有這個(gè)引用的目的就是為了支持方法調(diào)用過程中的動(dòng)態(tài)連接(Dynamic Linking)塞耕。
返回地址——在一個(gè)方法的執(zhí)行過程中,無論是執(zhí)行完畢正常退出還是中途異常退出后嘴瓤,都需要返回到方法被調(diào)用的位置扫外,程序才能繼續(xù)執(zhí)行,虛擬機(jī)棧中的“返回地址”就是用來幫助當(dāng)前方法恢復(fù)它的上層方法執(zhí)行狀態(tài)廓脆。一般來說筛谚,方法正常退出時(shí),調(diào)用者的 PC 計(jì)數(shù)值可以作為返回地址停忿,棧幀中可能保存此計(jì)數(shù)值驾讲。而方法異常退出時(shí),返回地址是通過異常處理器表確定的席赂,棧幀中一般不會(huì)保存此部分信息吮铭。
本地方法棧
Java虛擬機(jī)實(shí)現(xiàn)可能要用到C Stacks來支持Native語言,這個(gè)C Stacks就是本地方法棧(Native Method Stack)颅停。它與Java虛擬機(jī)棧類似谓晌,只不過本地方法棧是用來支持Native方法的。如果Java虛擬機(jī)不支持Native方法癞揉,并且也不依賴于C Stacks纸肉,可以無須支持本地方法棧。在Java虛擬機(jī)規(guī)范中對本地方法棧的語言和數(shù)據(jù)結(jié)構(gòu)等沒有強(qiáng)制規(guī)定喊熟,因此具體的Java虛擬機(jī)可以自由實(shí)現(xiàn)它柏肪。與Java虛擬機(jī)棧類似,本地方法棧也會(huì)拋出StackOverflowError和OutOfMemoryError異常芥牌。在實(shí)際開發(fā)中如果涉及JNI烦味,可能接觸本地方法棧多一些。
Java堆
Java堆(Java Heap)是被所有線程共享的運(yùn)行時(shí)內(nèi)存區(qū)域壁拉,Java堆用來存放對象實(shí)例拐叉,幾乎所有的對象實(shí)例都在這里分配內(nèi)存。Java堆內(nèi)存的對象被垃圾回收器管理扇商,這些受管理的對象無法顯式地銷毀。從內(nèi)存回收的角度來分宿礁,Java堆可以粗略的分為兩部分:新生代和老年代案铺,其中新生代又被分為Eden和Survivor空間。從內(nèi)存分配的角度梆靖,Java堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)控汉。不管如何劃分笔诵,Java對存儲的內(nèi)容是不變的,進(jìn)行劃分是為了能更快地回收或者分配內(nèi)存姑子。Java堆的容量可以是固定的乎婿,也可以動(dòng)態(tài)擴(kuò)展。Java堆所使用的內(nèi)存在物理上不需要連續(xù)街佑,邏輯上連續(xù)即可谢翎。Java虛擬機(jī)規(guī)范中定義了一種異常情況:如果在堆中沒有足夠的內(nèi)存來完成實(shí)例分配,并且堆也無法進(jìn)行擴(kuò)展時(shí)沐旨,則會(huì)拋出OutOfMemoryError異常森逮。
方法區(qū)
方法區(qū)(Method Area)是被所有線程共享的運(yùn)行時(shí)內(nèi)存區(qū)域,用來存儲已經(jīng)被Java虛擬機(jī)加載過的類的結(jié)構(gòu)信息磁携,包含運(yùn)行時(shí)常量池褒侧、字段和方法信息、靜態(tài)變量谊迄、即時(shí)編譯器編譯后的代碼和數(shù)據(jù)等闷供。方法區(qū)是Java堆的邏輯組成部分,他一樣在物理上不需要連續(xù)统诺,并且可以選擇在方法區(qū)中不實(shí)現(xiàn)垃圾回收歪脏。方法區(qū)并不等于永久代。只是因?yàn)镠otSpot VM使用永久代來實(shí)現(xiàn)方法區(qū)篙议,對于其他的Java虛擬機(jī)唾糯,比如J9和JRockit等,并不存在永久代概念鬼贱。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池(Runtime Constant Pool)并不是運(yùn)行時(shí)數(shù)據(jù)區(qū)域的其中一份子移怯,而是方法區(qū)的一部分。Class文件不僅包含類的版本这难、接口舟误、字段和方法等信息,還包含常量池姻乓,它用來存放編譯時(shí)期生成的字面量(int i= 1嵌溢;就是把1賦值給變量i,這個(gè)1就是字面量)和符號引用蹋岩,這些內(nèi)容會(huì)在類加載后存放在方法區(qū)的運(yùn)行時(shí)常量池中赖草。運(yùn)行時(shí)常量池可以理解為是類或接口的常量池運(yùn)行時(shí)的表現(xiàn)形式。在Java虛擬機(jī)規(guī)范中轉(zhuǎn)定義了一種異常情況:當(dāng)創(chuàng)建類或接口時(shí)剪个,如果構(gòu)造運(yùn)行時(shí)常量池所需要的內(nèi)存超過方法區(qū)所能提供的最大值秧骑,Java虛擬機(jī)機(jī)會(huì)拋出OutOfMemoryError。
對象的創(chuàng)建
通常是通過new指令來完成一個(gè)對象的創(chuàng)建。當(dāng)虛擬機(jī)收到一個(gè)new指令時(shí)乎折,它會(huì)做如下操作:
(1)判斷對象所屬的類是否被加載绒疗、鏈接和初始化
虛擬機(jī)接收到一條new指令時(shí),首先會(huì)檢查這個(gè)指定的參數(shù)(類名)是否能在常量池中定位到一個(gè)類的符號引用骂澄,并且檢查這個(gè)符號引用代表的類是否已被類加載器加載吓蘑、鏈接和初始化。
(2)為對象分配內(nèi)存
類加載完成后坟冲,接著JVM會(huì)在Java堆中劃分一塊內(nèi)存給對象磨镶。內(nèi)存分配根據(jù)Java堆是否規(guī)整,有兩種方式樱衷。
- 指針碰撞:如果Java堆的內(nèi)存是規(guī)整的棋嘲,即所有正在使用的內(nèi)存放在一邊,而空閑的內(nèi)存放在另一邊矩桂。分配內(nèi)存時(shí)將位于中間的指針指示器向空閑的內(nèi)存移動(dòng)一段與對象大小相等的距離沸移,這樣便完成了分配內(nèi)存工作。
- 空閑列表:如果Java堆內(nèi)存不是規(guī)整的侄榴,則需要由虛擬機(jī)維護(hù)一個(gè)列表來記錄哪些內(nèi)存是空閑可用的雹锣,這樣在分配的時(shí)候可以從列表中查詢到足夠大的內(nèi)存分配給對象,并在分配后更新列表記錄癞蚕。
Java堆內(nèi)存是否規(guī)整與JVM鎖采用的垃圾回收器是否帶有壓縮整理功能有關(guān)蕊爵。
(3)處理并發(fā)安全問題
創(chuàng)建對象是一個(gè)非常頻繁的操作,所以需要解決并發(fā)的問題桦山,有兩種方式:
- 對分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理攒射,比如在虛擬機(jī)采用CAS算法并配上失敗重試的方式保證更新操作的原子性。
- 每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存恒水,這塊內(nèi)存成為本地線程分配緩沖(Therad Local Allocation Buffer会放,TLAB),線程需要為對象分配內(nèi)存時(shí)钉凌,就在對應(yīng)線程的TLAB上分配內(nèi)存咧最,當(dāng)線程的TLAB用完并且被分配到了新的TLAB時(shí),這時(shí)候才需要同步鎖定御雕。通過-XX:+/-UserTLAB參數(shù)來設(shè)定虛擬機(jī)是否使用TLAB矢沿。
(4)初始化分配到的內(nèi)存空間
將分配到的內(nèi)存,除了對象頭外都初始化為零值酸纲。
(5)設(shè)置對象的對象頭
將對象的所屬類捣鲸,對象的HashCode和對象的GC分代年齡等數(shù)據(jù)存儲在對象的對象頭中。
(6)執(zhí)行init方法進(jìn)行初始化
執(zhí)行init方法闽坡,初始化對象的成員變量摄狱、調(diào)用類的構(gòu)造方法脓诡,這樣一個(gè)對象就創(chuàng)建出來了。
對象在堆內(nèi)存的布局
以HotSpot虛擬機(jī)為例媒役,一個(gè)對象在堆內(nèi)存的分布分為三個(gè)區(qū)域:對象頭、實(shí)例數(shù)據(jù)宪迟、對齊填充酣衷。
- 對象頭:對象頭包括兩部分信息,分別是Mark Word和元數(shù)據(jù)指針次泽。Mark Word用于存放對象運(yùn)行時(shí)的數(shù)據(jù)穿仪、比如HashCode、鎖狀態(tài)標(biāo)志意荤、GC分代年齡啊片、線程持有的鎖(monitor)等。而元數(shù)據(jù)指針用于指向方法區(qū)中的目標(biāo)類的元數(shù)據(jù)玖像,通過元數(shù)據(jù)可以確定對象的具體類型紫谷。
- 實(shí)例數(shù)據(jù):用于存放對象中的各種類型的字段信息,包括從父類繼承來的捐寥。
- 對齊填充:對齊填充不一定存在笤昨,起到了占位符的作用,沒有特別的含義握恳。
oop-Klass模型
oop-Klass模型是用來描述Java對象實(shí)例的一種模型瞒窒,它分為兩個(gè)部分,OOP(Ordinary Object Pointer)指的是普通對象指針乡洼,用來表示對象的實(shí)例信息崇裁。klass用來描述元數(shù)據(jù)。在Java虛擬機(jī)內(nèi)部會(huì)分別定義很多oop類型和Klass類型束昵,它們可以看做是oop家族和klass家族其中oopDesc是所有oop的頂級父類拔稳,arraryOopDesc是objArrayOopDesc和typeArrayOopDesc的父類。instanceOopdesc和arrayOopDesc都可以用來描述對象頭妻怎。
其中Klass是klass家族的父類(不是頂級父類)壳炎,可以發(fā)現(xiàn)oop家族中的成員和klass家族的成員有著對應(yīng)的關(guān)系,比如instanceOopDesc對應(yīng)著instanceKlass逼侦。
當(dāng)我們使用new創(chuàng)建一個(gè)對象的時(shí)候匿辩,JVM會(huì)在對重創(chuàng)建一個(gè)instanceOopDesc對象,這個(gè)對象中包含對象頭以及實(shí)例數(shù)據(jù)榛丢。而我們從oop家族的關(guān)系可以看到铲球,instanceOopDesc的父類是oopDesc。它的結(jié)構(gòu)如下:
oopDesc中包含兩個(gè)數(shù)據(jù)成員:_mark和_metadata晰赞。其中markOop類型的_mark對象指的就是對象的對象頭中的Mark Word稼病。metadata是一個(gè)共用體选侨,其中_klass是普通指針,_compressed_klass是壓縮類指針然走,他們就是對象頭中的元數(shù)據(jù)指針援制。這兩個(gè)指針數(shù)據(jù)根據(jù)對應(yīng)關(guān)系都會(huì)指向instanceKlass,instanceKlass可以用來描述元數(shù)據(jù)芍瑞。
instanceKlass繼承自Klass晨仑,枚舉ClassState用來標(biāo)識對象的加載進(jìn)度,Klass中的定義的部分字段如下:
可以看到Klass描述了Java類的元數(shù)據(jù)拆檬,具體來說就是Java類在JVM中對等的C++類型描述洪己,這樣繼承自Klass的instanceKlass同樣可以用來描述元數(shù)據(jù)。了解了oop-klass模型竟贯,我們就可以分析JVM是如何通過棧幀中的對象引用找到對應(yīng)的對象實(shí)例的答捕。
從圖中可以看出,JVM通過棧幀中的對象引用找到Java堆中的instanceOopDesc對象,這樣就可以訪問到Java對象的實(shí)例信息,當(dāng)需要訪問對象的具體類型等信息時(shí)慢显,可以通過instanceOopDesc的元數(shù)據(jù)指針來找到方法區(qū)中對應(yīng)的instanceKlass。
垃圾標(biāo)記算法
垃圾回收器痢站,簡稱GC,主要做了兩個(gè)工作选酗,一個(gè)是內(nèi)存的劃分和分配阵难,另一個(gè)是對垃圾進(jìn)行回收。關(guān)于內(nèi)存的劃分和分配芒填,目前JVM的內(nèi)存劃分是依賴于GC設(shè)計(jì)的呜叫,比如現(xiàn)在GC都是采用了分代收集算法來回收垃圾的,Java堆作為GC主要管理的區(qū)域被劃分為新生代和老年代殿衰。而新生代又可以細(xì)分為Eden空間朱庆、FromSurvivor空間和ToSurvivor空間等。這樣劃分是為了更快地進(jìn)行內(nèi)存分配和回收闷祥∮榧眨空間劃分后,GC就可以為新對象分配內(nèi)存空間凯砍。關(guān)于垃圾回收箱硕,被引用的對象是存活的對象,沒有被引用的對象就是死亡的對象悟衩,也就是垃圾剧罩。GC要區(qū)分出存活的對象和死亡的對象(也就是垃圾標(biāo)記),并對垃圾進(jìn)行回收座泳。目前垃圾標(biāo)記有兩種算法分別是引用計(jì)數(shù)算法和根搜索算法惠昔。這兩種算法都和引用有些關(guān)聯(lián)幕与,所以我們可以先回顧下引用的相關(guān)知識點(diǎn)。
Java中的引用
JDK1.2之后镇防,Java將引用分為強(qiáng)引用啦鸣,軟引用,弱引用来氧,虛引用赏陵。
- 強(qiáng)引用:當(dāng)我們新建一個(gè)對象時(shí)就創(chuàng)建了一個(gè)具有強(qiáng)引用的對象,如果一個(gè)對象具有強(qiáng)引用饲漾,GC就絕不會(huì)回收它。JVM寧愿拋出OOM異常缕溉,使程序異常終止考传,也不會(huì)回收具有強(qiáng)引用的對象來解決內(nèi)存不足的問題。
- 軟引用:如果一個(gè)對象只具有軟引用证鸥,當(dāng)內(nèi)存不夠時(shí)僚楞,GC會(huì)回收這些對象的內(nèi)存,回收后如果還沒有足夠的內(nèi)存枉层,就會(huì)拋出OOM異常泉褐。Java提供了SoftReference類來實(shí)現(xiàn)軟引用。
軟引用隱藏問題 Java虛擬機(jī)究竟是如何處理SoftReference的
- 弱引用:弱引用比軟引用的生命周期更短鸟蜡,GC一旦發(fā)現(xiàn)對象只具有弱引用膜赃,不管當(dāng)前是否足夠,都會(huì)回收它的內(nèi)存揉忘。Java提供了WeakReference類來實(shí)現(xiàn)弱引用跳座。
- 虛引用:虛引用并不會(huì)決定對象的生命周期,如果一個(gè)對象僅持有虛引用泣矛,這就和沒有任何引用一樣疲眷,在任何時(shí)候都有可能被GC回收。一個(gè)只具有虛引用的對象您朽,被GC回收時(shí)會(huì)發(fā)出一個(gè)系統(tǒng)通知狂丝,這也是虛引用的主要作用。Java提供了PhantomReference類來實(shí)現(xiàn)虛引用哗总。
引用計(jì)數(shù)算法
引用計(jì)數(shù)算法的基本思想是几颜,每個(gè)對象都有一個(gè)引用計(jì)數(shù)器,當(dāng)對象在某處被引用的時(shí)候魂奥,它的引用計(jì)數(shù)器就加1菠剩,引用失效時(shí)就減1,當(dāng)引用計(jì)數(shù)器中的值為0耻煤,則該對象就不能被使用具壮,變成了垃圾准颓。
目前主流的JVM沒有選擇引用計(jì)數(shù)算法來為垃圾標(biāo)記的,主要原因是引用計(jì)數(shù)算法沒有解決對象之間的相互循環(huán)引用的問題棺妓。
根搜索算法
這個(gè)算法的基本思想是選定一些對象作GC Roots攘已,并組成根對象集合,然后以這些GC Roots的對象作為起始點(diǎn)怜跑,向下探索样勃,如果目標(biāo)對象到GC Roots是連接著的,我們則稱該對象是可達(dá)的性芬,如果目標(biāo)對象不可達(dá)則說明目標(biāo)對象是可以回收的對象峡眶。該算法可以解決引用計(jì)數(shù)算法無法解決的問題:已經(jīng)死亡的對象因?yàn)橄嗷ヒ枚荒鼙换厥铡T贘ava中植锉,可以作為GC Roots的對象主要有這么幾種:
- 虛擬機(jī)棧中引用的對象
- 本地方法棧中JNI引用的對象
- 方法區(qū)靜態(tài)變量引用的對象
- 方法區(qū)運(yùn)行時(shí)常量池引用的對象
- 正在運(yùn)行的線程
- 有Boorstrap ClassLoader加載的對象
- GC控制的對象
還有一個(gè)問題是被標(biāo)記為不可達(dá)的對象會(huì)立即被GC回收嗎辫樱?要回答這個(gè)問題我們需要了解Java對象在JVM中的生命周期。
Java對象在JVM中的生命周期
- 創(chuàng)建階段
創(chuàng)建階段的具體步驟為
(1)為對象分配內(nèi)存空間
(2)構(gòu)造對象
(3)從超類到子類對static成員進(jìn)行初始化
(4)超類成員變量按順序初始化俊庇,遞歸調(diào)用超類的構(gòu)造方法
(5)子類成員按順序初始化狮暑,子類構(gòu)造方法調(diào)用 - 應(yīng)用階段
當(dāng)對象被創(chuàng)建,并分配給變量賦值時(shí)辉饱,狀態(tài)就切換到了應(yīng)用狀態(tài)搬男。這個(gè)階段的對象至少要具有一個(gè)強(qiáng)引用,或者顯式地使用軟引用彭沼,弱引用或者虛引用缔逛。 - 不可見階段
在程序中找不到對象的任何強(qiáng)引用,比如程序的執(zhí)行已經(jīng)超過了該對象的作用域溜腐。在不可見狀態(tài)译株,對象仍可能被特殊的強(qiáng)應(yīng)用GC Roots持有者,比如對象被本地方法棧中JNI引用或被運(yùn)行中的線程引用等挺益。 - 不可達(dá)階段
在程序中找不到對象的任何強(qiáng)引用歉糜,并且GC發(fā)現(xiàn)對象不可達(dá)。 - 收集階段
GC已經(jīng)發(fā)現(xiàn)對象不可達(dá)望众,并且GC已經(jīng)準(zhǔn)備好要對該對象的內(nèi)存空間進(jìn)行重新分配匪补,這個(gè)時(shí)候如果該對象重寫了finalize方法,則會(huì)調(diào)用該方法烂翰。 - 終結(jié)階段
對象執(zhí)行完finalize方法后仍然處于不可達(dá)狀態(tài)時(shí)夯缺,或者對象沒有重寫finalize方法,則對象會(huì)進(jìn)入終結(jié)階段甘耿,等待GC來回收該對象的空間踊兜。 - 對象空間重新分配階段
當(dāng)GC對對象的內(nèi)存空間進(jìn)行回收或者再分配時(shí),這個(gè)對象就會(huì)徹底消失了佳恬。
我們現(xiàn)在已經(jīng)了解了對象的生命周期捏境,再來思考下先前那個(gè)問題于游,被標(biāo)記為不可達(dá)的對象是否會(huì)立即被GC回收?很顯然是不會(huì)的垫言,被標(biāo)記為不可達(dá)的對象會(huì)進(jìn)入收集階段贰剥,此時(shí)會(huì)執(zhí)行該對象重寫的finalize方法,如果沒有重寫finalize方法或者finalize方法中沒有重新與一個(gè)可達(dá)的對象進(jìn)行關(guān)聯(lián)才會(huì)進(jìn)入終結(jié)階段筷频,并最終被回收蚌成。
垃圾收集算法
標(biāo)記-清除算法
標(biāo)記-清除算法(Mark-Sweep)是一種常見的基礎(chǔ)垃圾回收算法。它將垃圾回收分為兩個(gè)階段:
- 標(biāo)記階段:標(biāo)記出可以回收的對象凛捏。
-
清除階段:回收被標(biāo)記的對象所占的內(nèi)存空間
標(biāo)記-清除算法之所以是基礎(chǔ)的担忧,這是因?yàn)楹竺嬷v到的其他垃圾回收算法都是在此算法的基礎(chǔ)上進(jìn)行改進(jìn)的,下圖演示了標(biāo)記-清除算法的執(zhí)行過程:
標(biāo)記-清除算法主要有兩個(gè)缺點(diǎn):一個(gè)是標(biāo)記和清除的效率都不高坯癣,另一個(gè)就是容易產(chǎn)生大量不連續(xù)的內(nèi)存碎片涵妥,碎片太多可能會(huì)導(dǎo)致后續(xù)沒有足夠的連續(xù)內(nèi)存分配給較大的對象,從而提前觸發(fā)新一次的垃圾回收動(dòng)作坡锡。
復(fù)制算法
為了解決標(biāo)記-清除算法效率不高的問題,產(chǎn)生了復(fù)制算法窒所。它把內(nèi)存空間劃分了兩個(gè)相等的區(qū)域鹉勒,每次只使用其中一個(gè)區(qū)域。在垃圾收集時(shí)吵取,遍歷當(dāng)前使用的區(qū)域禽额,把存活的對象復(fù)制到另一個(gè)區(qū)域中,最后將當(dāng)前使用區(qū)域的可回收對象進(jìn)行回收皮官。執(zhí)行過程如下圖脯倒。這種算法每次都對整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,不需要考慮內(nèi)存碎片問題捺氢,代價(jià)就是使用內(nèi)存為原來的一半藻丢。復(fù)制算法的效率和存活對象的數(shù)量多少有很大關(guān)系,如果存活對象很少摄乒,復(fù)制算法的效率就會(huì)很高悠反。由于絕大多數(shù)對象的生命周期很短,并且這些生命周期很短的對象都存在于新生代中馍佑,所以復(fù)制算法被廣泛應(yīng)用于新生代中斋否。
標(biāo)記-壓縮算法
在新生代中可以使用復(fù)制算法,但是在老年代中就不能選擇復(fù)制算法了拭荤,因?yàn)槔夏甏膶ο蟠婊盥瘦^高茵臭,這樣會(huì)有較多的復(fù)制操作,導(dǎo)致效率較低舅世。因此就出現(xiàn)了一種標(biāo)記-清除算法的改進(jìn)版旦委,標(biāo)記-壓縮算法奇徒。與標(biāo)記-清除算法不同的是,在標(biāo)記可回收對象后社证,將所有存活的對象壓縮到內(nèi)存空間的一端逼龟,使它們緊湊地排列在一起,然后對邊界之外的內(nèi)存空間進(jìn)行回收追葡,回收后腺律,已使用和未使用的內(nèi)存就各自一邊,標(biāo)記-壓縮算法的執(zhí)行過程如下圖宜肉。標(biāo)記-壓縮算法解決了標(biāo)記-清除算法效率低和容易產(chǎn)生大量內(nèi)存碎片的問題匀钧,它被廣泛應(yīng)用于老年代的垃圾回收。
分代收集算法
分代收集算法結(jié)合不同的收集算法來處理不同的空間谬返。了解分代收集算法之前之斯,我們先要了解Java堆內(nèi)存的空間劃分。在java虛擬機(jī)中遣铝,各種對象的生命周期會(huì)有較大的差異佑刷,大部分對象生命周期很短暫,少部分對象生命周期很長酿炸,有的甚至和應(yīng)用程序以及JVM的運(yùn)行周期一樣長瘫絮。因此,應(yīng)該對不同生命周期的對象采用不同的回收策略填硕,根據(jù)生命周期長短將它們分別放在Java堆內(nèi)存的不同劃分區(qū)域麦萤,并且在不同的區(qū)域采用不同的垃圾回收算法,這就是分代的概念”饷校現(xiàn)在主流的Java虛擬機(jī)的GC都采用了分代收集算法壮莹。Java堆內(nèi)存基于分代的概念,分為新生代和老年代姻檀,其中新生代又細(xì)分為Eden空間命满、From Survivor空間和To Survivor空間。新創(chuàng)建的對象通常先進(jìn)入Eden空間绣版,因?yàn)镋den中大多數(shù)對象生命周期很短周荐,所以新生代的空間劃分不是均分的,比如HotSpot虛擬機(jī)默認(rèn)Eden空間和兩個(gè)Survivor空間所在的比例是8:1:1僵娃。
根據(jù)Java堆內(nèi)存區(qū)域的空間劃分概作,垃圾回收的類型有兩種:
- Minor Collection:新生代垃圾回收。
- Full Collection:又稱作Major Collection默怨,對老年代進(jìn)行垃圾回收讯榕。Full Collection通常情況下會(huì)伴隨至少一次的Minor Collection,它的收集頻率較低,耗時(shí)較長愚屁。
這個(gè)時(shí)候GC執(zhí)行Minor Collection报辱,Eden空間和FromSurvivor空間都會(huì)被清空与殃,新生代中存活的對象都存放在To Survivor空間或晉升到老年代,接下來將From Survivor和To Survivor空間互換位置碍现,也就是此前的From Survivor空間成為了現(xiàn)在的To Survivor空間幅疼,每次Minor Collection的執(zhí)行,Survivor空間互換都保證了To Survivor空間是空的昼接,這就是復(fù)制算法在新生代中的應(yīng)用衣屏。而在老年代則會(huì)采用標(biāo)記-壓縮算法或者標(biāo)記-清除算法。
java對象內(nèi)存申請過程
絕大多數(shù)剛創(chuàng)建的對象會(huì)存放在Eden空間辩棒。如圖所示,S0和S1分別指上文中提到的膨疏,會(huì)相互切換的To Survivor和From Survivor空間一睁。一個(gè)對象如果在新生代存活了足夠長的時(shí)間還沒有被回收,則會(huì)被復(fù)制到老年代签则。老年代的空間一般比新生代大须床,能存放更多的對象,如果對象比較大(比如長字符串和大容量數(shù)組)渐裂,并且新生代的剩余空間不足豺旬,則這個(gè)大對象會(huì)直接被分配到老年代。
可以使用 -XX:PretenureSizeThreshold 來控制直接升入老年代的對象大小芯义,大于這個(gè)值的對象會(huì)直接分配在老年代上哈垢。老年代因?yàn)閷ο蟮纳芷谳^長,不需要過多的復(fù)制操作扛拨,所以一般采用標(biāo)記壓縮的回收算法耘分。
注意:對于老年代可能存在這么一種情況,老年代中的對象有時(shí)候會(huì)引用到新生代對象绑警。這時(shí)如果要執(zhí)行新生代 GC求泰,則可能需要查詢整個(gè)老年代上可能存在引用新生代的情況,這顯然是低效的计盒。所以渴频,老年代中維護(hù)了一個(gè) 512 byte 的 card table,所有老年代對象引用新生代對象的信息都記錄在這里北启。每當(dāng)新生代發(fā)生 GC 時(shí)卜朗,只需要檢查這個(gè) card table 即可,大大提高了性能咕村。
關(guān)于對象如何從年輕代到老年代
觸發(fā)Major GC的條件
JVM進(jìn)行Minor GC的頻率很高场钉,而且Minor GC執(zhí)行的時(shí)間很短,所以對系統(tǒng)產(chǎn)生的影響不大懈涛。而Major GC的執(zhí)行逛万,對系統(tǒng)的影響就很明顯,值得我們關(guān)注批钠,總的來說宇植,有兩個(gè)條件會(huì)觸發(fā)Major GC:
- 當(dāng)應(yīng)用程序空閑時(shí),即沒有應(yīng)用線程在執(zhí)行時(shí)埋心,Major GC會(huì)被調(diào)用指郁。因?yàn)镚C是在優(yōu)先級最低的線程中進(jìn)行,所以當(dāng)應(yīng)用程序任務(wù)繁忙時(shí)拷呆,GC線程就不會(huì)被調(diào)用坡氯,當(dāng)下列條件除外。
- Java堆內(nèi)存不足時(shí),GC會(huì)被調(diào)用箫柳。當(dāng)應(yīng)用線程運(yùn)行過程需要?jiǎng)?chuàng)建新的對象手形,若此時(shí)內(nèi)存空間不夠,JVM就會(huì)強(qiáng)制地調(diào)用GC線程悯恍,以便回收內(nèi)存用于新的分配库糠。若一次GC之后仍不能滿足內(nèi)存分配的需求,JVM會(huì)再進(jìn)行兩次GC作進(jìn)一步的嘗試涮毫,如果還是不能滿足要求瞬欧,沒有足夠的內(nèi)存創(chuàng)建新對象,則JVM會(huì)拋出OOM異常罢防,應(yīng)用程序?qū)⒔K止艘虎。
減少GC開銷的措施
基本思想就是盡量減少M(fèi)ajor GC的觸發(fā),原則就是盡可能減少垃圾產(chǎn)生和減少GC過程中的開銷咒吐。
- 不要顯示地調(diào)用System.gc()
- 盡量減少臨時(shí)對象的創(chuàng)建和使用
- 將不再使用的對象顯式地設(shè)為null
- 盡量使用StringBUffer和StringBuilder野建,而不用String來累加字符串
- 能用基本類型如int、long恬叹,就不要用Integer候生、Long對象
- 盡量少用靜態(tài)變量。類的靜態(tài)變量保存在方法區(qū)绽昼,而在很多java虛擬機(jī)中唯鸭,不會(huì)對方法區(qū)內(nèi)存進(jìn)行GC操作
- 不要短時(shí)間集中創(chuàng)建和刪除對象
垃圾回收機(jī)制和調(diào)用System.gc()的區(qū)別
調(diào)用System.gc()方法其實(shí)是建議JVM進(jìn)行一次Major GC:程序員希望進(jìn)行一次Major GC。但是它不保證Major GC一定會(huì)進(jìn)行硅确,而且具體什么時(shí)候進(jìn)行取決于JVM的實(shí)現(xiàn)目溉,不同的JVM有不同的對策。
雖然只是建議而非一定菱农,但是多數(shù)情況下缭付,它都會(huì)觸發(fā)Major GC,從而增加了Major GC的頻率大莫,也即增加了應(yīng)用程序間歇性停頓的次數(shù)。
本文參考
《Android進(jìn)階解密》
http://www.reibang.com/p/d686e108d15f
https://blog.csdn.net/peerslee/article/details/79166477
https://blog.csdn.net/github_37130188/article/details/102731811
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1856
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1855
https://mp.weixin.qq.com/s/XRCq3IDdGJt3Nq9Mu23U5g