Java虛擬機(jī)(JVM)淺入深出
Java虛擬機(jī)(英語:Java Virtual Machine盐碱,縮寫為JVM)潘拨,一種能夠運(yùn)行Java bytecode的虛擬機(jī),以堆棧結(jié)構(gòu)機(jī)器來進(jìn)行實(shí)做惯裕。最早由太陽微系統(tǒng)所研發(fā)并實(shí)現(xiàn)第一個實(shí)現(xiàn)版本,是Java平臺的一部分悲没,能夠運(yùn)行以Java語言寫作的軟件程序。
Java虛擬機(jī)有自己完善的硬體架構(gòu)示姿,如處理器甜橱、堆棧子檀、寄存器等,還具有相應(yīng)的指令系統(tǒng)。JVM屏蔽了與具體操作系統(tǒng)平臺相關(guān)的信息崇猫,使得Java程序只需生成在Java虛擬機(jī)上運(yùn)行的目標(biāo)代碼(字節(jié)碼),就可以在多種平臺上不加修改地運(yùn)行瞻凤。通過對中央處理器(CPU)所執(zhí)行的軟件實(shí)現(xiàn),實(shí)現(xiàn)能執(zhí)行編譯過的Java程序碼(Applet與應(yīng)用程序)伐憾。作為一種編程語言的虛擬機(jī),實(shí)際上不只是專用于Java語言摧玫,只要生成的編譯文件匹配JVM對加載編譯文件格式要求,任何語言都可以由JVM編譯運(yùn)行绑青。
引入Java語言虛擬機(jī)后诬像,Java語言在不同平臺上運(yùn)行時不需要重新編譯。
Java語言編譯程序只需生成在Java虛擬機(jī)上運(yùn)行的目標(biāo)代碼(字節(jié)碼)闸婴,可以在多種平臺上不加修改地運(yùn)行坏挠。
Java虛擬機(jī)在執(zhí)行字節(jié)碼時,把字節(jié)碼解釋成具體平臺上的機(jī)器指令執(zhí)行邪乍。
-
Java的能夠“一次編譯降狠,到處運(yùn)行”。
下面具體講解JVM
1. JVM生命周期
啟動庇楞。啟動一個Java程序時榜配,一個JVM實(shí)例就產(chǎn)生了,任何一個擁有public static void main(String[] args)函數(shù)的class都可以作為JVM實(shí)例運(yùn)行的起點(diǎn)吕晌。
運(yùn)行蛋褥。main()作為該程序初始線程的起點(diǎn),任何其他線程均由該線程啟動聂使。
-
消亡壁拉。當(dāng)程序中的所有非守護(hù)線程都終止時谬俄,JVM才退出;若安全管理器允許弃理,程序也可以使用Runtime類或者System.exit()來退出溃论。
一個運(yùn)行中的Java虛擬機(jī)有著一個清晰的任務(wù):執(zhí)行Java程序。程序開始執(zhí)行時他才運(yùn)行痘昌,程序結(jié)束時他就停止钥勋。你在同一臺機(jī)器上運(yùn)行三個程序,就會有三個運(yùn)行中的Java虛擬機(jī)辆苔。 Java虛擬機(jī)總是開始于一個main()方法算灸,這個方法必須是公有、返回void驻啤、直接受一個字符串?dāng)?shù)組菲驴。在程序執(zhí)行時,你必須給Java虛擬機(jī)指明這個包換main()方法的類名骑冗。main()方法是程序的起點(diǎn)赊瞬,他被執(zhí)行的線程初始化為程序的初始線程。程序中其他的線程都由他來啟動贼涩。
Java中的線程分為兩種:守護(hù)線程 (daemon)和普通線程(non-daemon)巧涧。守護(hù)線程是Java虛擬機(jī)自己使用的線程,比如負(fù)責(zé)垃圾收集的線程就是一個守護(hù)線程遥倦。當(dāng)然谤绳,你也可以把自己的程序設(shè)置為守護(hù)線程。包含main()方法的初始線程不是守護(hù)線程袒哥。
只要Java虛擬機(jī)中還有普通的線程在執(zhí)行缩筛,Java虛擬機(jī)就不會停止。如果有足夠的權(quán)限统诺,你可以調(diào)用exit()方法終止程序歪脏。
2. JVM體系結(jié)構(gòu)
1) 類裝載器(ClassLoader)(用來裝載.class文件)
2) 執(zhí)行引擎(執(zhí)行字節(jié)碼婿失,或者執(zhí)行本地方法)
3) 運(yùn)行時數(shù)據(jù)區(qū)(方法區(qū)啄寡、堆豪硅、java棧、PC寄存器懒浮、本地方法棧)
3. JVM運(yùn)行時數(shù)據(jù)區(qū)
方法區(qū)
線程間共享
用于存儲已被虛擬機(jī)加載的類信息、常量砚著、靜態(tài)變量次伶、即時編譯器編譯后的代碼等數(shù)據(jù)
OutOfMemoryError異常:當(dāng)方法區(qū)無法滿足內(nèi)存的分配需求時
-
運(yùn)行時常量池
方法區(qū)的一部分
用于存放編譯期生成的各種字面量與符號引用稽穆,如String類型常量就存放在常量池
-
OutOfMemoryError異常:當(dāng)常量池?zé)o法再申請到內(nèi)存時
Class文件中除了有關(guān)的版本、字段柱彻、方法餐胀、接口等描述信息外、還有一項(xiàng)信息是常量池卖擅,用于存放編輯期生成的各種字面量和符號引用磨镶,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時常量池中存放
除了Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外健提,還可以選擇不實(shí)現(xiàn)垃圾收集私痹。這個區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收和對類型的卸載统刮。
當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時侥蒙,將拋出OutOfMemoryErroy異常
Java 堆(Java Heap)
對于大多數(shù)應(yīng)用來說,Java 堆(Java Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊鞭衩。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域论衍,在虛擬機(jī)啟動的是創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實(shí)例炬丸,幾乎所有的對象實(shí)例以及數(shù)組都要在這里分配內(nèi)存蜒蕾。
Java堆是垃圾收集器管理的主要區(qū)域焕阿,因此很多時候也被稱為“GC堆”(Garbage Collected Heap)捣鲸。從內(nèi)存回收的角度來看闽坡,由于現(xiàn)在收集器基本都采用分代收集算法,所以Java堆還可以細(xì)分為:新生代和老年代外厂;新生代又可以分為:Eden 空間汁蝶、From Survivor空間论悴、To Survivor空間。
- 新生代幔亥。新建的對象都是用新生代分配內(nèi)存帕棉,Eden空間不足的時候香伴,會把存活的對象轉(zhuǎn)移到Survivor中具则,新生代大小可以由-Xmn來控制博肋,也可以用-XX:SurvivorRatio來控制Eden和Survivor的比例
- 舊生代。用于存放新生代中經(jīng)過多次垃圾回收仍然存活的對象
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定拔稳,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中巴比,只要邏輯上是連續(xù)的即可,就像我們的磁盤空間一樣采记。在實(shí)現(xiàn)時唧龄,既可以實(shí)現(xiàn)成固定大小的奸远,也可以是可擴(kuò)展的懒叛,不過當(dāng)前主流的虛擬機(jī)都是按照可擴(kuò)展來實(shí)現(xiàn)的(通過-Xms和-Xmx控制)。如果在堆中沒有內(nèi)存完成實(shí)例的分配胖烛,并且堆也無法再擴(kuò)展時佩番,將會拋出OutOfMemoryError異常罢杉。
程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間屑那,它可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器持际。在虛擬機(jī)的概念模型里(僅是概念模型哗咆,各種虛擬機(jī)可能會通過一些更高效的方式去實(shí)現(xiàn))晌柬,字節(jié)碼解釋器工作時就是通過改變這個計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令年碘、分支、循環(huán)埃难、跳轉(zhuǎn)、異常處理忍弛、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計(jì)數(shù)器來完成细疚。
- 當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器
- 當(dāng)前線程私有
- 不會出現(xiàn)OutOfMemoryError情況
由于Java虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實(shí)現(xiàn)的疯兼。在任何一個確定的時刻贫途,一個處理器都只會執(zhí)行一條線程中的指令潮饱。因此香拉,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個獨(dú)立的程序計(jì)數(shù)器扑毡,各個線程之間計(jì)數(shù)器互不影響瞄摊,獨(dú)立存儲苦掘。
如果線程正在執(zhí)行的是一個Java方法鹤啡,那這個計(jì)數(shù)器記錄的是正在執(zhí)行的字節(jié)碼指令的地址递瑰;如果正在執(zhí)行的是Native方法,這個計(jì)數(shù)器值則為空(undefined)说贝。
此內(nèi)存區(qū)域是唯一一個在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域乡恕。
程序計(jì)數(shù)器是線程私有的,它的生命周期與線程相同(隨線程而生倍试,隨線程而滅)县习。
Java虛擬機(jī)棧
- 線程私有躁愿,生命周期與線程相同
- 存儲方法的局部變量表(基本類型沪蓬、對象引用)跷叉、操作數(shù)棧云挟、動態(tài)鏈接、方法出口等信息帖世。
- java方法執(zhí)行的內(nèi)存模型日矫,每個方法執(zhí)行的同時都會創(chuàng)建一個棧幀绑榴,每一個方法被調(diào)用直至執(zhí)行完成的過程彭沼,就對應(yīng)著一個棧幀在虛擬機(jī)棧中從入棧到出棧的過程姓惑。
- StackOverflowError異常:當(dāng)線程請求的棧深度大于虛擬機(jī)所允許的深度
- OutOfMemoryError異常:如果棧的擴(kuò)展時無法申請到足夠的內(nèi)存
JVM棧是線程私有的按脚,每個線程創(chuàng)建的同時都會創(chuàng)建JVM棧辅搬,JVM棧中存放的為當(dāng)前線程中局部基本類型的變量、部分的返回結(jié)果以及Stack Frame萌庆。其他引用類型的對象在JVM棧上僅存放變量名和指向堆上對象實(shí)例的首地址践险。
虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀用于儲存局部變量表吹菱、操作數(shù)棧鳍刷、動態(tài)鏈接输瓜、方法出口等信息尤揣。每個方法從調(diào)用直至完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中入棧到出棧的過程坯癣。
棧內(nèi)存就是虛擬機(jī)棧示罗,或者說是虛擬機(jī)棧中局部變量表的部分
局部變量表存放了編輯期可知的各種基本數(shù)據(jù)類型(boolean蚜点、byte拌阴、char迟赃、short纤壁、int、float欠痴、long喇辽、double)菩咨、對象引用(refrence)類型和returnAddress類型(指向了一條字節(jié)碼指令的地址)
其中64位長度的long和double類型的數(shù)據(jù)會占用兩個局部變量空間旦委,其余的數(shù)據(jù)類型只占用1個。
局部變量表所需的內(nèi)存空間在編譯器間完成分配摩钙,當(dāng)進(jìn)入一個方法時胖笛,這個方法需要在幀中分配多大的局部變量空間是完全確定的长踊,在方法運(yùn)行期間不會改變局部變量表的大小
本地方法棧
本地方法棧(Native Method Stack)與虛擬機(jī)棧所發(fā)揮的作用是非常類似身弊,它們之間的區(qū)別在于虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法服務(wù)列敲,而本地方法棧則是為虛擬機(jī)使用到的Native方法服務(wù)戴而。在虛擬機(jī)規(guī)范中對本地方法棧中方法使用的語言所意、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定扶踊,因此具體的虛擬機(jī)可以自由的實(shí)現(xiàn)它秧耗。
與虛擬機(jī)棧一樣,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常胶台。
與虛擬機(jī)棧一樣诈唬,本地方法棧也是線程私有的铸磅。
直接內(nèi)存(Direct Memory)
- 直接內(nèi)存并不是虛擬機(jī)運(yùn)行的一部分阅仔,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域弧械,但是這部分內(nèi)存也被頻繁使用
- NIO可以使用Native函數(shù)庫直接分配堆外內(nèi)存刃唐,堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作
- 大小不受Java堆大小的限制画饥,受本機(jī)(服務(wù)器)內(nèi)存限制
- OutOfMemoryError異常:系統(tǒng)內(nèi)存不足時
總結(jié):Java對象實(shí)例存放在堆中抖甘;常量存放在方法區(qū)的常量池衔彻;虛擬機(jī)加載的類信息米奸、常量、靜態(tài)變量慢睡、即時編譯器編譯后的代碼等數(shù)據(jù)放在方法區(qū)漂辐;以上區(qū)域是所有線程共享的髓涯。棧是線程私有的纬纪,存放該方法的局部變量表(基本類型包各、對象引用)、操作數(shù)棧娃属、動態(tài)鏈接矾端、方法出口等信息秩铆。
一個Java程序?qū)?yīng)一個JVM豺旬,一個方法(線程)對應(yīng)一個Java棧族阅。
Java代碼的編譯和執(zhí)行過程
Java代碼的編譯和執(zhí)行包括了三個重要機(jī)制:
(1)Java源碼編譯機(jī)制(.java源代碼文件 -> .class字節(jié)碼文件)
(2)類加載機(jī)制(ClassLoader)
(3)類執(zhí)行機(jī)制(JVM執(zhí)行引擎)
Java源碼編譯機(jī)制
Java源代碼是不能被機(jī)器識別的坦刀,需要先經(jīng)過編譯器編譯成JVM可以執(zhí)行的.class字節(jié)碼文件鲤遥,再由解釋器解釋運(yùn)行盖奈。即:Java源文件(.java) -- Java編譯器 --> Java字節(jié)碼文件 (.class) -- Java解釋器 --> 執(zhí)行钢坦。流程圖如下:
Java字節(jié)碼的執(zhí)行是由JVM執(zhí)行引擎來完成爹凹,流程圖如下所示:
Java源碼編譯機(jī)制
Java 源碼編譯由以下三個過程組成:
分析和輸入到符號表
注解處理
語義分析和生成class文件
流程圖如下所示:
最后生成的class文件由以下部分組成:
- 結(jié)構(gòu)信息。包括class文件格式版本號及各部分的數(shù)量與大小的信息
- 元數(shù)據(jù)颤陶。對應(yīng)于Java源碼中聲明與常量的信息指郁。包含類/繼承的超類/實(shí)現(xiàn)的接口的聲明信息闲坎、域與方法聲明信息和常量池
- 方法信息茬斧。對應(yīng)Java源碼中語句和表達(dá)式對應(yīng)的信息项秉。包含字節(jié)碼娄蔼、異常處理器表岁诉、求值棧與局部變量區(qū)大小涕癣、求值棧的類型記錄坠韩、調(diào)試符號信息只搁。
類加載機(jī)制
JVM的類加載是通過ClassLoader及其子類來完成的氢惋,類的層次關(guān)系和加載順序可以由下圖來描述:
(1)Bootstrap ClassLoader
- JVM的根ClassLoader明肮,由C++實(shí)現(xiàn)
- 加載Java的核心API:$JAVA_HOME中jre/lib/rt.jar中所有class文件的加載柿估,這個jar中包含了java規(guī)范定義的所有接口以及實(shí)現(xiàn)循未。
- JVM啟動時即初始化此ClassLoader
(2)Extension ClassLoader
- 加載Java擴(kuò)展API(lib/ext中的類)
(3)App ClassLoader
- 加載Classpath目錄下定義的class
(4)Custom ClassLoader
- 屬于應(yīng)用程序根據(jù)自身需要自定義的ClassLoader,如tomcat、jboss都會根據(jù)J2EE規(guī)范自行實(shí)現(xiàn)ClassLoader
類被加載到虛擬機(jī)內(nèi)存中開始的妖,到卸載為止绣檬,整個生命周期包括:加載、驗(yàn)證嫂粟、準(zhǔn)備娇未、解析星虹、初始化零抬、使用和卸載7個階段。
加載過程中會先檢查類是否被已加載宽涌,檢查順序是自底向上平夜,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視為已加載此類卸亮,保證此類只所有ClassLoader加載一次忽妒。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類兼贸。
雙親委派機(jī)制
JVM在加載類時默認(rèn)采用的是雙親委派機(jī)制段直。通俗的講,就是某個特定的類加載器在接到加載類的請求時溶诞,首先將加載任務(wù)委托給父類加載器鸯檬,依次遞歸。如果父類加載器可以完成類加載任務(wù)很澄,就成功返回京闰;只有父類加載器無法完成此加載任務(wù)時,才自己去加載甩苛。
作用:1)避免重復(fù)加載蹂楣;2)更安全。如果不是雙親委派讯蒲,那么用戶在自己的classpath編寫了一個java.lang.Object的類痊土,那就無法保證Object的唯一性。所以使用雙親委派墨林,即使自己編寫了赁酝,但是永遠(yuǎn)都不會被加載運(yùn)行。
破壞雙親委派機(jī)制
雙親委派機(jī)制并不是一種強(qiáng)制性的約束模型旭等,而是Java設(shè)計(jì)者推薦給開發(fā)者的類加載器實(shí)現(xiàn)方式酌呆。
線程上下文類加載器,這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進(jìn)行設(shè)置搔耕,如果創(chuàng)建線程時還未設(shè)置隙袁,它將會從父線程中繼承一個,如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過的話,那么這個類加載器就是應(yīng)用程序類加載器菩收。像JDBC就是采用了這種方式损搬。這種行為就是逆向使用了加載器颠区,違背了雙親委派模型的一般性原則洛巢。
類執(zhí)行機(jī)制
JVM是基于棧的體系結(jié)構(gòu)來執(zhí)行class字節(jié)碼的小染。線程創(chuàng)建后,都會產(chǎn)生程序計(jì)數(shù)器(PC)和棧(Stack)箱舞,程序計(jì)數(shù)器存放下一條要執(zhí)行的指令在方法內(nèi)的偏移量遍坟,棧中存放一個個棧幀,每個棧幀對應(yīng)著每個方法的每次調(diào)用褐缠,而棧幀又是有局部變量區(qū)和操作數(shù)棧兩部分組成政鼠,局部變量區(qū)用于存放方法中的局部變量和參數(shù),操作數(shù)棧中用于存放方法執(zhí)行過程中產(chǎn)生的中間結(jié)果队魏。棧的結(jié)構(gòu)如下圖所示:
主要的執(zhí)行技術(shù):解釋,即時編譯万搔,自適應(yīng)優(yōu)化胡桨、芯片級直接執(zhí)行
- 解釋屬于第一代JVM,
- 即時編譯JIT屬于第二代JVM瞬雹,
- 自適應(yīng)優(yōu)化(目前Sun的HotspotJVM采用這種技術(shù))則吸取第一代JVM和第二代JVM的經(jīng)驗(yàn)昧谊,采用兩者結(jié)合的方式
開始對所有的代碼都采取解釋執(zhí)行的方式,并監(jiān)視代碼執(zhí)行情況酗捌。對那些經(jīng)常調(diào)用的方法啟動一個后臺線程呢诬,將其編譯為本地代碼,并進(jìn)行優(yōu)化胖缤。若方法不再頻繁使用尚镰,則取消編譯過的代碼,仍對其進(jìn)行解釋執(zhí)行哪廓。
HotSpot
HotSpot的正式發(fā)布名稱為"Java HotSpot Performance Engine"狗唉,是Java虛擬機(jī)的一個實(shí)現(xiàn),包含了服務(wù)器版和桌面應(yīng)用程序版涡真,現(xiàn)時由Oracle維護(hù)并發(fā)布分俯。它利用JIT及自適應(yīng)優(yōu)化技術(shù)(自動查找性能熱點(diǎn)并進(jìn)行動態(tài)優(yōu)化,這也是HotSpot名字的由來)來提高性能哆料。
提起HotSpot VM缸剪,相信所有Java程序員都知道,它是Sun JDK和OpenJDK中所帶的虛擬機(jī)东亦,也是目前使用范圍最廣的Java虛擬機(jī)杏节。
hotspot虛擬機(jī)對象
對象的創(chuàng)建
1.檢查
類加載、解析、初始化:虛擬機(jī)遇到一條new指令時拢锹,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用谣妻,并且檢查這個符號引用代表的類是否已經(jīng)被加載、解析和初始化過卒稳。如果沒有蹋半,那必須先執(zhí)行相應(yīng)的類加載過程
2.分配內(nèi)存
接下來將為新生對象分配內(nèi)存,對象所需內(nèi)存在類加載完畢之后就可以完全確定充坑,為對象分配內(nèi)存空間的任務(wù)等同于把一塊確定的大小的內(nèi)存從Java堆中劃分出來减江。
假設(shè)Java堆中內(nèi)存是絕對規(guī)整的,所有用過的內(nèi)存放在一邊捻爷,空閑的內(nèi)存放在另一邊辈灼,中間放著一個指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個指針指向空閑空間那邊挪動一段與對象大小相等的距離也榄,這個分配方式叫做“指針碰撞”
如果Java堆中的內(nèi)存并不是規(guī)整的巡莹,已使用的內(nèi)存和空閑的內(nèi)存相互交錯,那就沒辦法簡單地進(jìn)行指針碰撞了甜紫,虛擬機(jī)就必須維護(hù)一個列表降宅,記錄上哪些內(nèi)存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實(shí)例囚霸,并更新列表上的記錄腰根,這種分配方式成為“空閑列表”
選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定拓型。
在分配內(nèi)存的時候會出現(xiàn)并發(fā)的問題额嘿,比如在給A對象分配內(nèi)存的時候,指針還沒有來得及修改劣挫,對象B又同時使用了原來的指針進(jìn)行了內(nèi)存的分片册养。
有兩個解決方案:
1、對分配的內(nèi)存進(jìn)行同步處理:CAS配上失敗重試的方式保證更新操作的原子性
2揣云、把內(nèi)存分配的動作按照線程劃分在不同的空間之中進(jìn)行,即每個線程在java堆中分配一塊小內(nèi)存捕儒,稱為本地緩沖區(qū),那個線程需要分配內(nèi)存邓夕,就需要在本地緩沖區(qū)上進(jìn)行刘莹,只有當(dāng)緩沖區(qū)用完并分配新的緩沖區(qū)的時候,才需要同步鎖定焚刚。
3.Init
執(zhí)行new指令之后會接著執(zhí)行Init方法点弯,進(jìn)行對象頭和對象實(shí)例數(shù)據(jù)的初始化,這樣一個對象才算產(chǎn)生出來
對象的內(nèi)存布局
在HotSpot虛擬機(jī)中矿咕,對象在內(nèi)存中儲存的布局可以分為3塊區(qū)域:對象頭抢肛、實(shí)例數(shù)據(jù)和對齊填充
- 對象頭:標(biāo)記字(32位虛擬機(jī)4B狼钮,64位虛擬機(jī)8B) + 類型指針(32位虛擬機(jī)4B,64位虛擬機(jī)8B)+ [數(shù)組長(對于數(shù)組對象才需要此部分信息)]
- 實(shí)例數(shù)據(jù):是對象正常儲存的有效信息捡絮,也是程序代碼中所定義的各種類型的字段內(nèi)容熬芜。無論是從父類繼承下來的,還是在子類中定義的福稳,都需要記錄下來涎拉。
- 對齊填充:對于64位虛擬機(jī)來說,對象大小必須是8B的整數(shù)倍的圆,當(dāng)實(shí)例數(shù)據(jù)沒有對齊的時候鼓拧,就需要通過對齊填充來補(bǔ)全
對象頭包括兩部分:
a) 儲存對象自身的運(yùn)行時數(shù)據(jù),如哈希碼越妈、GC分帶年齡季俩、鎖狀態(tài)標(biāo)志、線程持有的鎖梅掠、偏向線程ID酌住、偏向時間戳
b) 另一部分是指類型指針,即對象指向它的類元數(shù)據(jù)的指針阎抒,虛擬機(jī)通過這個指針來確定這個對象是那個類的實(shí)例
示例(以HashMap<Long,Long>為例):
其只有Key和Value是有效數(shù)據(jù)赂韵,共2 8B=16B,包裝成Long對象后分別具有了8B標(biāo)記字和8B的類型指針挠蛉,共24B2=48B;兩個對象組成Map.Entry后多了16B對象頭肄满、一個8B的next字段谴古、4B的int類型的hash字段,還必須添加4B的空白填充稠歉。共32B掰担;最后還有對HashMap中對此Entry的8B的引用。所以空間利用率為 16B / (48B+32B+8B) ≈ 18%
對象的訪問定位
使用句柄訪問
Java堆中將會劃分出一塊內(nèi)存來作為句柄池怒炸,reference中存儲的就是對象的句柄地址带饱,而句柄中包含了對象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址,相當(dāng)于二級指針阅羹。
優(yōu)勢:reference中存儲的是穩(wěn)定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實(shí)例數(shù)據(jù)指針勺疼,而reference本身不需要修改
使用直接指針訪問
Java堆對象的布局就必須考慮如何訪問類型數(shù)據(jù)的相關(guān)信息,而refreence中存儲的直接就是對象的地址,相當(dāng)于一級指針捏鱼。
優(yōu)勢:速度更快执庐,節(jié)省了一次指針定位的時間開銷,由于對象的訪問在Java中非常頻繁导梆,因此這類開銷積少成多后也是一項(xiàng)非彻焯剩可觀的執(zhí)行成本
兩種方式有各自的優(yōu)缺點(diǎn)迂烁。當(dāng)垃圾回收移動對象時,對于方式一而言递鹉,reference中存儲的地址是穩(wěn)定的地址盟步,不需要修改,僅需要修改對象句柄的地址躏结;而對于方式二却盘,則需要修改reference中存儲的地址。從訪問效率上看窜觉,方式二優(yōu)于方式一谷炸,因?yàn)榉绞蕉贿M(jìn)行了一次指針定位,節(jié)省了時間開銷禀挫,而這也是HotSpot采用的實(shí)現(xiàn)方式旬陡。下圖是句柄訪問與指針訪問的示意圖。