開篇問題:
-
一句話描述類加載過程锈死?
類加載過程實際是將Java文件編譯為class文件并裝載到JVM中最終解析為01機器代碼供服務(wù)器進行的過程午阵,涉及到的過程包括:編譯、裝載鸳址、鏈接戳葵、初始化就乓、使用、卸載6個過程拱烁,其中各個過程的作用分別是:
- 編譯:通過javac命令將Java文件轉(zhuǎn)換成class文件生蚁。
- 裝載:查找并加載class文件。
- 鏈接:包括:驗證戏自、準(zhǔn)備邦投、解析。
- 驗證:通過對二進制流的內(nèi)容進行校驗來檢查是否符合JVM的要求規(guī)范以及是否會對程序運行時是否會對JVM造成危害浦妄。其中包括:文件格式驗證 --> 元數(shù)據(jù)驗證 --> 字節(jié)碼驗證 --> 符號引用驗證尼摹。
- 準(zhǔn)備:在方法區(qū)中為類變量(靜態(tài)變量)分配內(nèi)存并設(shè)置系統(tǒng)默認(rèn)初始值。
- 解析:將方法區(qū)中的符號引用轉(zhuǎn)變成直接或引用剂娄,并對解析結(jié)構(gòu)進行緩存蠢涝。
- 初始化:調(diào)用構(gòu)造方法對類變量賦予程序中設(shè)置的值。
- 使用:
- 卸載:類卸載的三個條件必須都滿足才能進行卸載阅懦,一般情況下
詳細(xì)描述對象的內(nèi)存布局每一個部分干了什么和二,用到了什么技術(shù)?
對象內(nèi)存布局包括:對象頭耳胎、實例數(shù)據(jù)惯吕、對其填充三部分。其中
對象頭包含:Mark Word怕午、Class Pointer废登、Length。
- Mark Word:一系列標(biāo)志位郁惜,包括:哈希碼堡距、分代年齡、鎖狀態(tài)標(biāo)志等,其中哈希碼用到了大端存儲技術(shù)羽戒,便于數(shù)據(jù)類型的符號判斷)
- Class Pointer:指向?qū)ο髮?yīng)類的內(nèi)存地址缤沦,其中引用定位到對象的方式包括:句柄池訪問、直接訪問易稠。
- 句柄池訪問:使用句柄訪問對象缸废,會在堆中開辟一塊內(nèi)存作為句柄池,句柄中儲存了對象實例數(shù)據(jù) 的內(nèi)存地址驶社,訪問類型數(shù)據(jù)的內(nèi)存地址企量,優(yōu)點:reference存儲的是穩(wěn)定的句柄地址,在對象被移動時只會改變句柄中的實例數(shù)據(jù)指針衬吆,而reference本身不需要改變梁钾;缺點:增加了一次指針定位的時間開銷绳泉。
- 直接訪問:指reference中直接儲存對象在heap中的內(nèi)存地址逊抡,但對應(yīng)的類型數(shù)據(jù)訪問地址需要 在實例中存儲。優(yōu)點:節(jié)省了一次指針定位的開銷零酪;缺點:在對象被移動時冒嫡,reference本身需要被修改。
- Length:數(shù)據(jù)對象特有四苇,用于記錄數(shù)組長度孝凌。
實例數(shù)據(jù)包含:包含了對象的所有成員變量,大小由變量本身的類型決定月腋,用到的技術(shù)-- 指針壓縮技術(shù)作用包括:減少GC次數(shù)蟀架,提供CPU的OOP緩存。
對其填充的作用:為了保證對象的大小為8字節(jié)的整數(shù)倍榆骚,對其填充技術(shù)的作用是提高CPU訪問數(shù)據(jù)的效率片拍。
運行時數(shù)據(jù)區(qū)
通過CPU與主存的關(guān)系可以推斷出JVM是如何跟服務(wù)器的CPU和內(nèi)存進行交互的 – java是多線程機制,當(dāng)多個任務(wù)執(zhí)行(類比我們的CPU運算核心)對同一塊內(nèi)存(類比:主存)數(shù)據(jù)進行操作時(PS:線程共享)必然會發(fā)生數(shù)據(jù)不一致的情況,這個時候需要有一塊區(qū)域或者一種操作(類比:協(xié)議)保證數(shù)據(jù)的一致性妓肢。而每個線程又有自己單獨的工作內(nèi)存(類比高速緩沖區(qū)),當(dāng)我們線程進行運作時,數(shù)據(jù)肯定會從JVM主存拷貝到線程自己的工作內(nèi)存,然后再進行操作捌省。
方法區(qū)
方法區(qū)是被線程共享的Non-Heap(非堆) 內(nèi)存區(qū)域,用于存儲已被虛擬機加載的類信息碉钠、常量纲缓、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)喊废,在虛擬機啟動時創(chuàng)建祝高。
注意:
JVM運行時數(shù)據(jù)區(qū)是一種規(guī)范,真正的實現(xiàn)
方法區(qū)在JDK 8中就是Metaspace污筷,在JDK6或7中就是Perm Space
堆
堆是Java虛擬機所管理內(nèi)存中最大的一塊工闺,在虛擬機啟動時創(chuàng)建,被所有線程共享。其中 Java對象實例以及數(shù)組都在堆上分配斤寂。
虛擬機棧
問題:那一個線程執(zhí)行的狀態(tài)如何維護?一個線程可以執(zhí)行多 少個方法?這樣的關(guān)系怎么維護呢?
- ?虛擬機棧是一個線程執(zhí)行的區(qū)域耿焊,保存著一個線程中方法的調(diào)用狀態(tài)。換句話說遍搞,一個Java線程的運行狀態(tài)罗侯,由一個虛擬機棧來保存,所以虛擬機椣常肯定是線程私有的钩杰,獨有的,隨著線程的創(chuàng)建而創(chuàng)建诊县。
- 每一個被線程執(zhí)行的方法讲弄,為該棧中的棧幀,即每個方法對應(yīng)一個棧幀依痊。 調(diào)用一個方法避除,就會向棧中壓入一個棧幀;一個方法調(diào)用完成,就會把該棧幀從棧中彈出胸嘁。
棧幀:
棧幀:每個棧幀對應(yīng)一個被調(diào)用的方法瓶摆,可以理解為一個方法的運行空間。
棧幀中包括局部變量 表性宏、操作數(shù)棧群井、動態(tài)鏈接、方法返回地址和即時信息毫胜。
?局部變量 表:方法中定義的局部變量以及方法的參數(shù)存放在這張表中书斜, 局部變量表中的變量不可直接使用,如需要使用的話酵使,必須通過相關(guān)指令將其加載至操作數(shù)棧中作為操作數(shù)使用荐吉。
操作數(shù) 棧:以壓棧和出棧的方式存儲操作數(shù)的。如 1+1 兩個1存儲在局部變量 表中凝化, 1+1這個操作在操作數(shù)棧中完成稍坯。
動態(tài)鏈接:每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接(符號引用編程直接引用)搓劫。因為類加載機制 僅僅是將本文件的符號引用變成直接引用 瞧哟,當(dāng)遇到多態(tài)的調(diào)用時,只能通過運行來確定子類對接的引用是哪一個枪向。
方法返回地址:當(dāng)一個方法開始執(zhí)行后,只有兩種方式可以退出勤揩,一種是遇到方法返回的字節(jié)碼指令;一種是遇 見異常,并且這個異常沒有在方法體內(nèi)得到處理秘蛔。
本地方法棧
當(dāng)前線程執(zhí)行的方法是Native類型的陨亡,這些方法就會在本地方法棧中執(zhí)行
思考:在Java方法執(zhí)行的時候如何調(diào)用native的方法呢?
通過動態(tài)鏈接來進行調(diào)用傍衡。
動態(tài)鏈接.png
程序計數(shù)器
作用:是記錄當(dāng)前線程的執(zhí)行位置,線程私有负蠕。
如果線程正在執(zhí)行Java方法蛙埂,則計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;
如果正在執(zhí)行的是Native方法,則這個計數(shù)器為空遮糖。
除了上面五塊內(nèi)存之外,其實我們的JVM還會使用到其他兩塊內(nèi)存
直接內(nèi)存(Direct Memory)
并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分绣的,也不是JVM規(guī)范中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存也被頻 繁地使用欲账,而且也可能導(dǎo)致OutOfMemoryError 異常出現(xiàn)屡江,所以我們放到這里一起講解。在JDK 1.4 中新加入了NIO(New Input/Output)類赛不,引入了一種基于通道(Channel)與緩沖區(qū) (Buffer)的I/O 方式惩嘉,它可以使用Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在Java 堆 里面的DirectByteBuffer 對象作為這塊內(nèi)存的引用進行操作踢故。這樣能在一些場景中顯著提高性能文黎, 因為避免了在Java 堆和Native 堆中來回復(fù)制數(shù)據(jù)。
本機直接內(nèi)存的分配不會受到Java 堆大小的限制畴椰,但是臊诊,既然是內(nèi)存,則肯定還是會受到本機總 內(nèi)存的大小及處理器尋址空間的限制斜脂。因此在分配JVM空間的時候應(yīng)該考慮直接內(nèi)存所帶來的影 響,特別是應(yīng)用到NIO的場景触机。
其他內(nèi)存:
Code Cache:**JVM本身是個本地程序帚戳,還需要其他的內(nèi)存去完成各種基本任務(wù),比如儡首,JIT 編譯器在運行時對熱點方法進行編譯片任,就會將編譯后的方法儲存在Code Cache里面;GC等 功能。需要運行在本地線程之中蔬胯,類似部分都需要占用內(nèi)存空間对供。這些是實現(xiàn)JVM JIT等功能 的需要,但規(guī)范中并不涉及
其中椃毡簦可以指向堆 case: Object obj=new Object()
方法區(qū)指向堆 case: private static Object obj=new Object();
堆指向方法區(qū) case: Class類對象指向它的元數(shù)據(jù)产场。 new Object().getClass();
Java對象內(nèi)存模型
一個Java對象在內(nèi)存中包括3個部分:對象頭、實例數(shù)據(jù)和對齊填充舞竿。
Java對象內(nèi)存布局.png
內(nèi)存模型設(shè)計之–大小端存儲
小端存儲:便于數(shù)據(jù)之間的類型轉(zhuǎn)換京景,例如:long類型轉(zhuǎn)換為int類型時,高地址部分的數(shù)據(jù)可以 直接截掉骗奖。
大端存儲:便于數(shù)據(jù)類型的符號判斷确徙,因為最低地址位數(shù)據(jù)即為符號位醒串,可以直接判斷數(shù)據(jù)的正 負(fù)號。
內(nèi)存模型設(shè)計之–Class Pointer
引用定位到對象的方式有兩種鄙皇,一種叫句柄池訪問芜赌,一種叫直接訪問
句柄池訪問對象.png
區(qū)別:
句柄池:
使用句柄訪問對象,會在堆中開辟一塊內(nèi)存作為句柄池伴逸,句柄中儲存了對象實例數(shù)據(jù)(屬性值結(jié)構(gòu)體) 的內(nèi)存地址较鼓,訪問類型數(shù)據(jù)的內(nèi)存地址(類信息,方法類型信息)违柏,對象實例數(shù)據(jù)一般也在heap中開 辟博烂,類型數(shù)據(jù)一般儲存在方法區(qū)中。
優(yōu)點:reference存儲的是穩(wěn)定的句柄地址漱竖,在對象被移動(垃圾收集時移動對象是非常普遍的行為) 時只會改變句柄中的實例數(shù)據(jù)指針禽篱,而reference本身不需要改變。
缺點:增加了一次指針定位的時間開銷馍惹。 直接訪問:
直接指針訪問方式指reference中直接儲存對象在heap中的內(nèi)存地址躺率,但對應(yīng)的類型數(shù)據(jù)訪問地址需要 在實例中存儲。
優(yōu)點:節(jié)省了一次指針定位的開銷万矾。 缺點:在對象被移動時(如進行GC后的內(nèi)存重新排列)悼吱,reference本身需要被修改
內(nèi)存模型設(shè)計之–指針壓縮
指針壓縮的目的:
- 為了保證CPU普通對象指針(oop)緩存
- 為了減少GC的發(fā)生,因為指針不壓縮是8字節(jié)良狈,這樣在64位操作系統(tǒng)的堆上其他資源空間就少了后添。
64位操作系統(tǒng)中 內(nèi)存 > 4G 默認(rèn)開啟指針壓縮技術(shù),內(nèi)存< 4G薪丁,默認(rèn)是32位系統(tǒng)默認(rèn)不開啟遇西。內(nèi)存 > 32G 指針壓縮失效。所以我們通常在部署服務(wù)時严嗜,JVM內(nèi)存不要超過32G粱檀,因為超過32G就無法開啟 指針壓縮了。
內(nèi)存 > 32G指針壓縮失效的原因是:4G*8 = 32G
32位系統(tǒng)的CPU 最大支持2^32 = 4G ,如果是64位系統(tǒng)漫玄,最大支持 2^64茄蚯, 但是對其填充是按照8字節(jié)進行填充,指針壓縮可以理解為在32位系統(tǒng)在64位上面使用睦优,因為32位系統(tǒng)的CPU尋址空間最大支持4G渗常,對其填充*8 = 32G,這就是內(nèi)存>32G指針壓縮失效的原因刨秆。
關(guān)閉指針壓縮 : -XX:+UseCompressedOops
內(nèi)存模型設(shè)計之–對齊填充
對齊填充的意義是提高CPU訪問數(shù)據(jù)的效率凳谦,主要針對會存在該實例對象數(shù)據(jù)跨內(nèi)存地址區(qū)域存儲的情況。
例如:在沒有對齊填充的情況下衡未,內(nèi)存地址存放情況如下:
因為處理器只能0x00-0x07尸执,0x08-0x0F這樣讀取數(shù)據(jù)家凯,所以當(dāng)我們想獲取這個long型的數(shù)據(jù)時,處理 器必須要讀兩次內(nèi)存如失,第一次(0x00-0x07)绊诲,第二次(0x08-0x0F),然后將兩次的結(jié)果才能獲得真正的數(shù)值褪贵。
那么在有對齊填充的情況下掂之,內(nèi)存地址存放情況是這樣的:
現(xiàn)在處理器只需要直接一次讀取(0x08-0x0F)的內(nèi)存地址就可以獲得我們想要的數(shù)據(jù)了。