類文件的結(jié)構(gòu)
一比勉、魔數(shù)(Magic Number)
每個(gè) Class 文件的頭 4 個(gè)字節(jié)稱為魔數(shù)(Magic Number),它的唯一作用是確定這個(gè)文件是否為一個(gè)能被虛擬機(jī)接收的 Class 文件劳较。
二、Class 文件版本號(hào)(Minor&Major Version)
緊接著魔數(shù)的四個(gè)字節(jié)存儲(chǔ)的是 Class 文件的版本號(hào):前兩個(gè)字節(jié)是次版本號(hào)浩聋,后兩個(gè)字節(jié)是主版本號(hào)观蜗。
每當(dāng) Java 發(fā)布大版本(比如 Java 7,Java8)的時(shí)候衣洁,主版本號(hào)都會(huì)加 1墓捻。
高版本的 Java 虛擬機(jī)可以執(zhí)行低版本編譯器生成的 Class 文件,但是低版本的 Java 虛擬機(jī)不能執(zhí)行高版本編譯器生成的 Class 文件坊夫。
三砖第、常量池(Constant Pool)
緊接著主次版本號(hào)之后的是常量池,常量池的數(shù)量是兩個(gè)字節(jié)的 constant_pool_count-1
(常量池計(jì)數(shù)器是從 1 開始計(jì)數(shù)的环凿,第 0 項(xiàng)常量有特殊考慮梧兼,當(dāng)值為 0 代表“不引用任何一個(gè)常量池項(xiàng)”)。
常量池主要存放:字面量和符號(hào)引用智听,字面量是比如字符串羽杰、final常量。
符號(hào)引用包含三類:
- 類和接口的全限定名到推。
- 字段的名稱和描述。
- 方法的名稱和描述莉测。
常量池中的每一項(xiàng)都是一個(gè)表捣卤,這個(gè)表的第一位都是用來(lái)標(biāo)識(shí)常量類型的腌零。
四、訪問(wèn)標(biāo)識(shí)(Access Flags)
標(biāo)識(shí)類或接口的層次的訪問(wèn)信息,包括這個(gè)Class是類還是接口闲询,是否為public或者abstract類型,如果是類的話阎姥,是否被申明為final等呼巴。
五御蒲、當(dāng)前類(This Class)厚满、父類(Super Class)、接口(Interfaces)索引集合
u2 this_class;//當(dāng)前類全限定名
u2 super_class;//父類全限定名
u2 interfaces_count;//實(shí)現(xiàn)的接口數(shù)量
u2 interfaces[interfaces_count];//一個(gè)類可以實(shí)現(xiàn)多個(gè)接口
六遵馆、字段表集合(Fields)
字段數(shù)量:兩個(gè)字節(jié)描述字段數(shù)量货邓。
字段表:字段表中的項(xiàng)包括訪問(wèn)標(biāo)識(shí)换况、名稱、對(duì)常量池的引用复隆、一些額外的屬性挽拂。
字段表用于描述接口或類中聲明的變量亏栈。字段包括類級(jí)變量以及實(shí)例變量宏赘,但不包括在方法內(nèi)部聲明的局部變量察署。
七、方法集合(Methods)
方法數(shù)量:兩個(gè)字節(jié)標(biāo)識(shí)方法數(shù)量
方法表:方法表與字段表類似休吠,表中項(xiàng)包括訪問(wèn)標(biāo)識(shí)业簿、名稱梅尤、對(duì)常量池的引用巷燥、一些額外的屬性。
八亡脑、屬性表(Attributes)
與字段和方法類似霉咨,Class文件也有自己的屬性表集合途戒。
類加載
類的生命周期僵驰?
加載 -> 連接 -> 初始化 -> 使用 -> 卸載
連接又分為三步:驗(yàn)證 -> 準(zhǔn)備 -> 解析
一蒜茴、類加載
類加載主要完成三件事:
- 通過(guò)全類名獲取定義此類的二進(jìn)制字節(jié)流
- 將字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個(gè)代表該類的
Class
對(duì)象粉私,作為方法區(qū)這些數(shù)據(jù)的訪問(wèn)入口
二诺核、驗(yàn)證
- 文件格式驗(yàn)證:如魔數(shù)是否正確,主版本號(hào)是否能被當(dāng)前虛擬機(jī)處理漓摩,常量池的類型是否被支持管毙。
- 元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,例如:類是否有父類酥诽,是否繼承了不該被繼承的類等
- 字節(jié)碼驗(yàn)證:通過(guò)數(shù)據(jù)流和控制流分析皱埠,確定程序語(yǔ)義是否合法边器、符合邏輯托修。比如保證任意時(shí)刻操作數(shù)棧和指令代碼序列都能配合工作睦刃。
- 符號(hào)引用驗(yàn)證:確保解析動(dòng)作能正確執(zhí)行。
三际长、準(zhǔn)備
準(zhǔn)備階段正式為類變量分配內(nèi)存并設(shè)置類變量初始值工育。注意點(diǎn):
- 這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量如绸,即靜態(tài)變量怔接,而不包括實(shí)例變量扼脐。實(shí)例變量會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一塊分配在 Java 堆中靶端。
- 從概念上講脏榆,類變量所使用的內(nèi)存都應(yīng)當(dāng)在 方法區(qū) 中進(jìn)行分配台谍。JDK 7 之后,HotSpot 已經(jīng)把原本放在永久代的字符串常量池仔役、靜態(tài)變量等移動(dòng)到堆中又兵,這個(gè)時(shí)候類變量則會(huì)隨著 Class 對(duì)象一起存放在 Java 堆中沛厨。
- 里所設(shè)置的初始值"通常情況"下是數(shù)據(jù)類型默認(rèn)的零值(如 0逆皮、0L电谣、null剿牺、false 等)况鸣,只有final才在準(zhǔn)備階段賦值。
四潜索、解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程竹习。解析動(dòng)作主要針對(duì)類或接口整陌、字段、類方法九默、接口方法驼修、方法類型、方法句柄和調(diào)用限定符 7 類符號(hào)引用進(jìn)行幢竹。
五焕毫、初始化
初始化階段是執(zhí)行初始化方法方法的過(guò)程咬荷,是類加載的最后一步,同時(shí)也是對(duì)象創(chuàng)建的第一步唇牧,這一步 JVM 才開始真正執(zhí)行類中定義的 Java 程序代碼(字節(jié)碼)聚唐。
對(duì)于初始化方法的調(diào)用丐重,虛擬機(jī)會(huì)自己確保其在多線程環(huán)境中的安全性。因?yàn)榉椒ㄊ菐фi線程安全杆查,所以在多線程環(huán)境下進(jìn)行類初始化的話可能會(huì)引起多個(gè)進(jìn)程阻塞扮惦,并且這種阻塞很難被發(fā)現(xiàn)。
虛擬機(jī)對(duì)類進(jìn)行初始化的五種情況亲桦?
- 當(dāng)遇到 new 新對(duì)象崖蜜,訪問(wèn)靜態(tài)變量(不是靜態(tài)常量,靜態(tài)常量在常量池中)客峭,給靜態(tài)變量賦值豫领、調(diào)用類的靜態(tài)方法,會(huì)初始化類舔琅。
- 使用 java 反射對(duì)類進(jìn)行反射調(diào)用等恐,比如 newInstance(), Class.forname("...")等郊尝,如果類沒有初始化样傍,會(huì)觸發(fā)類初始化襟锐。
- 初始化一個(gè)類,如果其父類還未初始化,則先觸發(fā)該父類的初始化。
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要定義一個(gè)要執(zhí)行的主類 (包含
main
方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)類。 -
MethodHandle
和VarHandle
可以看作是輕量級(jí)的反射調(diào)用機(jī)制,而要想使用這 2 個(gè)調(diào)用, 就必須先使用findStaticVarHandle
來(lái)初始化要調(diào)用的類。 - 當(dāng)一個(gè)接口中定義了 JDK8 新加入的默認(rèn)方法時(shí),如果有這個(gè)接口的實(shí)現(xiàn)類發(fā)生了初始化,那該接口要在其之前被初始化高镐。
六算行、卸載
- 該類的所有的實(shí)例對(duì)象都已被 GC鲸阔,也就是說(shuō)堆不存在該類的實(shí)例對(duì)象叙身。
- 該類沒有在其他任何地方被引用(不被反射調(diào)用财忽,class.forname(""))
- 該類的類加載器的實(shí)例已被 GC
所以隶校,在 JVM 生命周期內(nèi)舞终,由 jvm 自帶的類加載器加載的類是不會(huì)被卸載的。但是由我們自定義的類加載器加載的類是可能被卸載的转捕。
類加載器總結(jié)枢步?
JVM內(nèi)置了三個(gè)重要的類加載器殴穴。
-
BootstrapClassLoader(啟動(dòng)類加載器):最頂層類加載器,由 C++實(shí)現(xiàn)淤堵。負(fù)責(zé)加載
%JAVA_HOME%/lib
目錄下的 jar 包和類扎阶,或者被-Xbootclasspath
參數(shù)指定的路徑中的所有類。 -
ExtensionClassLoader(擴(kuò)展類加載器) :主要負(fù)責(zé)加載
%JRE_HOME%/lib/ext
目錄下的 jar 包和類孟害,或被java.ext.dirs
系統(tǒng)變量所指定的路徑下的 jar 包。 - AppClassLoader(應(yīng)用程序類加載器) :面向我們用戶的加載器惯雳,負(fù)責(zé)加載當(dāng)前應(yīng)用 classpath 下的所有 jar 包和類捍歪。
類加載的雙親委派模式恩商?
- 自底向上檢查類是否被加載损拢,如果加載過(guò)或舞,直接返回。
- 自頂向下嘗試加載類丑蛤,如果頂層類加載器加載失敗叠聋,才會(huì)由底層類加載器加載。
- 首先檢查用戶自定義類是否加載了此類受裹,然后檢查應(yīng)用程序類加載器碌补,擴(kuò)展類加載器,啟動(dòng)類加載器棉饶。最后依次由上到下嘗試加載厦章。
雙親委派模式的好處?
雙親委派模型可以避免類的重復(fù)加載(JVM 區(qū)分不同類的方式不僅僅根據(jù)類名照藻,相同的類文件被不同的類加載器加載產(chǎn)生的是兩個(gè)不同的類)袜啃。
也保證了 Java 的核心 API 不被篡改。如果沒有使用雙親委派模型幸缕,而是每個(gè)類加載器加載自己的話就會(huì)出現(xiàn)一些問(wèn)題群发,比如我們編寫一個(gè)稱為
java.lang.Object
類的話,那么程序運(yùn)行的時(shí)候发乔,系統(tǒng)就會(huì)出現(xiàn)多個(gè)不同的Object
類熟妓。
如果我們不想用雙親委派模型怎么辦?
繼承ClassLoader類栏尚,重寫 loadClass() 方法起愈。
如何自定義類加載器?
除了 BootstrapClassLoader 其他類加載器均由 Java 實(shí)現(xiàn)且全部繼承自 java.lang.ClassLoader。如果我們要自定義自己的類加載器告材,很明顯需要繼承 ClassLoader坤次,并重寫 findClass() 方法。
jvm常見參數(shù)總結(jié)斥赋?
// 堆內(nèi)存 -Xms<heap size>[unit]
-Xms2G 堆初始化內(nèi)存2G
-Xmx5G 最大內(nèi)存5G
// 新生代內(nèi)存 -XX:NewSize=<young size>[unit]
-XX:NewSize=256m 新生代最小內(nèi)存256m
-XX:MaxNewSize=1024m 新生代最大內(nèi)存1024m
// 通過(guò)-Xmn<young size>[unit]指定新生代內(nèi)存
-Xmn256m 新生代最大內(nèi)存和最小內(nèi)存都是256m
// 通過(guò) XX:NewRatio=<int> 設(shè)置新生代與老年代的比值
-XX:NewRatio=1 新生代:老年代 的比值為1:1
// 顯示的指定元空間的大小
-XX:MetaspaceSize=N //設(shè)置 Metaspace 的初始(和最小大戌趾铩)
-XX:MaxMetaspaceSize=N //設(shè)置 Metaspace 的最大大小,如果不指定大小的話疤剑,隨著更多類的創(chuàng)建滑绒,虛擬機(jī)會(huì)耗盡所有可用的系統(tǒng)內(nèi)存
//顯示指定選擇垃圾收集器
-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+UseParNewGC
-XX:+UseG1GC
JVM 調(diào)優(yōu)
JVM 調(diào)優(yōu)常見工具?
JDK命令行工具:
- jps:
- jstat:
- jmap:
- jhat:
- jhat:
JDK 可視化分析工具:
JConsole:
Visual VM: