成都創(chuàng)新互聯(lián)成立10多年來(lái),這條路我們正越走越好,積累了技術(shù)與客戶資源,形成了良好的口碑。為客戶提供成都網(wǎng)站設(shè)計(jì)、成都做網(wǎng)站、網(wǎng)站策劃、網(wǎng)頁(yè)設(shè)計(jì)、空間域名、網(wǎng)絡(luò)營(yíng)銷、VI設(shè)計(jì)、網(wǎng)站改版、漏洞修補(bǔ)等服務(wù)。網(wǎng)站是否美觀、功能強(qiáng)大、用戶體驗(yàn)好、性價(jià)比高、打開快等等,這些對(duì)于網(wǎng)站建設(shè)都非常重要,成都創(chuàng)新互聯(lián)通過(guò)對(duì)建站技術(shù)性的掌握、對(duì)創(chuàng)意設(shè)計(jì)的研究為客戶提供一站式互聯(lián)網(wǎng)解決方案,攜手廣大客戶,共同發(fā)展進(jìn)步。
前言:
今天要給大家分享的是Java虛擬機(jī)的一些硬貨知識(shí),文章不錯(cuò)的話記得給我點(diǎn)給個(gè)關(guān)注哦,私信我可以獲取更多的java資料。
第一章 JVM 內(nèi)存模型
Java 虛擬機(jī)(Java Virtual Machine=JVM)的內(nèi)存空間分為五個(gè)部分,分別是:
程序計(jì)數(shù)器
Java 虛擬機(jī)棧
本地方法棧
堆
方法區(qū)。
下面對(duì)這五個(gè)區(qū)域展開深入的介紹。
1.1 程序計(jì)數(shù)器
1.1.1 什么是程序計(jì)數(shù)器?
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,可以把它看作當(dāng)前線程正在執(zhí)行的字節(jié)碼的行號(hào)指示器。也就是說(shuō),程序計(jì)數(shù)器里面記錄的是當(dāng)前線程正在執(zhí)行的那一條字節(jié)碼指令的地址。
注:但是,如果當(dāng)前線程正在執(zhí)行的是一個(gè)本地方法,那么此時(shí)程序計(jì)數(shù)器為空。
1.1.2 程序計(jì)數(shù)器的作用
程序計(jì)數(shù)器有兩個(gè)作用:
字節(jié)碼解釋器通過(guò)改變程序計(jì)數(shù)器來(lái)依次讀取指令,從而實(shí)現(xiàn)代碼的流程控制,如:順序執(zhí)行、選擇、循環(huán)、異常處理。
在多線程的情況下,程序計(jì)數(shù)器用于記錄當(dāng)前線程執(zhí)行的位置,從而當(dāng)線程被切換回來(lái)的時(shí)候能夠知道該線程上次運(yùn)行到哪兒了。
1.1.3 程序計(jì)數(shù)器的特點(diǎn)
是一塊較小的存儲(chǔ)空間
線程私有。每條線程都有一個(gè)程序計(jì)數(shù)器。
是唯一一個(gè)不會(huì)出現(xiàn)OutOfMemoryError的內(nèi)存區(qū)域。
生命周期隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的結(jié)束而死亡。
1.2 Java虛擬機(jī)棧(JVM Stack)
1.2.1 什么是Java虛擬機(jī)棧?
Java虛擬機(jī)棧是描述Java方法運(yùn)行過(guò)程的內(nèi)存模型。
Java虛擬機(jī)棧會(huì)為每一個(gè)即將運(yùn)行的Java方法創(chuàng)建一塊叫做“棧幀”的區(qū)域,這塊區(qū)域用于存儲(chǔ)該方法在運(yùn)行過(guò)程中所需要的一些信息,這些信息包括:
局部變量表
存放基本數(shù)據(jù)類型變量、引用類型的變量、returnAddress類型的變量。
操作數(shù)棧
動(dòng)態(tài)鏈接
方法出口信息
等
當(dāng)一個(gè)方法即將被運(yùn)行時(shí),Java虛擬機(jī)棧首先會(huì)在Java虛擬機(jī)棧中為該方法創(chuàng)建一塊“棧幀”,棧幀中包含局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口信息等。當(dāng)方法在運(yùn)行過(guò)程中需要?jiǎng)?chuàng)建局部變量時(shí),就將局部變量的值存入棧幀的局部變量表中。
當(dāng)這個(gè)方法執(zhí)行完畢后,這個(gè)方法所對(duì)應(yīng)的棧幀將會(huì)出棧,并釋放內(nèi)存空間。
注意:人們常說(shuō),Java的內(nèi)存空間分為“棧”和“堆”,棧中存放局部變量,堆中存放對(duì)象。
這句話不完全正確!這里的“堆”可以這么理解,但這里的“棧”只代表了Java虛擬機(jī)棧中的局部變量表部分。真正的Java虛擬機(jī)棧是由一個(gè)個(gè)棧幀組成,而每個(gè)棧幀中都擁有:局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口信息。
1.2.2 Java 虛擬機(jī)棧的特點(diǎn)
(1)局部變量表的創(chuàng)建是在方法被執(zhí)行的時(shí)候,隨著棧幀的創(chuàng)建而創(chuàng)建。而且,局部變量表的大小在編譯時(shí)期就確定下來(lái)了,在創(chuàng)建的時(shí)候只需分配事先規(guī)定好的大小即可。此外,在方法運(yùn)行的過(guò)程中局部變量表的大小是不會(huì)發(fā)生改變的。
(2)Java 虛擬機(jī)棧會(huì)出現(xiàn)兩種異常:StackOverFlowError 和 OutOfMemoryError。
a) StackOverFlowError:
若Java虛擬機(jī)棧的內(nèi)存大小不允許動(dòng)態(tài)擴(kuò)展,那么當(dāng)線程請(qǐng)求棧的深度超過(guò)當(dāng)前Java虛擬機(jī)棧的最大深度的時(shí)候,就拋出StackOverFlowError異常。
b) OutOfMemoryError:
若Java虛擬機(jī)棧的內(nèi)存大小允許動(dòng)態(tài)擴(kuò)展,且當(dāng)線程請(qǐng)求棧時(shí)內(nèi)存用完了,無(wú)法再動(dòng)態(tài)擴(kuò)展了,此時(shí)拋出OutOfMemoryError異常。
(3)Java虛擬機(jī)棧也是線程私有的,每個(gè)線程都有各自的Java虛擬機(jī)棧,而且隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的死亡而死亡。
注:StackOverFlowError和OutOfMemoryError的異同?StackOverFlowError表示當(dāng)前線程申請(qǐng)的棧超過(guò)了事先定好的棧的最大深度,但內(nèi)存空間可能還有很多。而OutOfMemoryError是指當(dāng)線程申請(qǐng)棧時(shí)發(fā)現(xiàn)棧已經(jīng)滿了,而且內(nèi)存也全都用光了。
1.3 本地方法棧
1.3.1 什么是本地方法棧?
本地方法棧和Java虛擬機(jī)棧實(shí)現(xiàn)的功能類似,只不過(guò)本地方法區(qū)是本地方法運(yùn)行的內(nèi)存模型。
本地方法被執(zhí)行的時(shí)候,在本地方法棧也會(huì)創(chuàng)建一個(gè)棧幀,用于存放該本地方法的局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、出口信息。
方法執(zhí)行完畢后相應(yīng)的棧幀也會(huì)出棧并釋放內(nèi)存空間。
也會(huì)拋出StackOverFlowError和OutOfMemoryError異常。
1.4 堆
1.4.1 什么是堆?
堆是用來(lái)存放對(duì)象的內(nèi)存空間。
幾乎所有的對(duì)象都存儲(chǔ)在堆中。
1.4.2 堆的特點(diǎn)
(1)線程共享
整個(gè) Java 虛擬機(jī)只有一個(gè)堆,所有的線程都訪問(wèn)同一個(gè)堆。而程序計(jì)數(shù)器、Java 虛擬機(jī)棧、本地方法棧都是一個(gè)線程對(duì)應(yīng)一個(gè)的。
(2)在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。
(3)垃圾回收的主要場(chǎng)所。
(4)可以進(jìn)一步細(xì)分為:新生代、老年代。
新生代又可被分為:Eden、From Survior、To Survior。不同的區(qū)域存放具有不同生命周期的對(duì)象。這樣可以根據(jù)不同的區(qū)域使用不同的垃圾回收算法,從而更具有針對(duì)性,從而更高效。
(5)堆的大小既可以固定也可以擴(kuò)展,但主流的虛擬機(jī)堆的大小是可擴(kuò)展的,因此當(dāng)線程請(qǐng)求分配內(nèi)存,但堆已滿,且內(nèi)存已滿無(wú)法再擴(kuò)展時(shí),就拋出 OutOfMemoryError。
1.5 方法區(qū)
1.5.1 什么是方法區(qū)?
Java 虛擬機(jī)規(guī)范中定義方法區(qū)是堆的一個(gè)邏輯部分。方法區(qū)中存放已經(jīng)被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等。
1.5.2 方法區(qū)的特點(diǎn)
線程共享
方法區(qū)是堆的一個(gè)邏輯部分,因此和堆一樣,都是線程共享的。整個(gè)虛擬機(jī)中只有一個(gè)方法區(qū)。
永久代
方法區(qū)中的信息一般需要長(zhǎng)期存在,而且它又是堆的邏輯分區(qū),因此用堆的劃分方法,我們把方法區(qū)稱為老年代。
內(nèi)存回收效率低
方法區(qū)中的信息一般需要長(zhǎng)期存在,回收一遍內(nèi)存之后可能只有少量信息無(wú)效。
對(duì)方法區(qū)的內(nèi)存回收的主要目標(biāo)是:對(duì)常量池的回收 和 對(duì)類型的卸載。
Java虛擬機(jī)規(guī)范對(duì)方法區(qū)的要求比較寬松。
和堆一樣,允許固定大小,也允許可擴(kuò)展的大小,還允許不實(shí)現(xiàn)垃圾回收。
1.5.3 什么是運(yùn)行時(shí)常量池?
方法區(qū)中存放三種數(shù)據(jù):類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼。其中常量存儲(chǔ)在運(yùn)行時(shí)常量池中。
我們一般在一個(gè)類中通過(guò)public static final來(lái)聲明一個(gè)常量。這個(gè)類被編譯后便生成Class文件,這個(gè)類的所有信息都存儲(chǔ)在這個(gè)class文件中。
當(dāng)這個(gè)類被Java虛擬機(jī)加載后,class文件中的常量就存放在方法區(qū)的運(yùn)行時(shí)常量池中。而且在運(yùn)行期間,可以向常量池中添加新的常量。如:String類的intern()方法就能在運(yùn)行期間向常量池中添加字符串常量。
當(dāng)運(yùn)行時(shí)常量池中的某些常量沒有被對(duì)象引用,同時(shí)也沒有被變量引用,那么就需要垃圾收集器回收。
1.6 直接內(nèi)存
直接內(nèi)存是除Java虛擬機(jī)之外的內(nèi)存,但也有可能被Java使用。
在NIO中引入了一種基于通道和緩沖的IO方式。它可以通過(guò)調(diào)用本地方法直接分配Java虛擬機(jī)之外的內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer對(duì)象直接操作該內(nèi)存,而無(wú)需先將外面內(nèi)存中的數(shù)據(jù)復(fù)制到堆中再操作,從而提升了數(shù)據(jù)操作的效率。
直接內(nèi)存的大小不受Java虛擬機(jī)控制,但既然是內(nèi)存,當(dāng)內(nèi)存不足時(shí)就會(huì)拋出OOM異常。
1.7 綜上所述
Java虛擬機(jī)的內(nèi)存模型中一共有兩個(gè)“?!?,分別是:Java虛擬機(jī)棧和本地方法棧。
兩個(gè)“?!钡墓δ茴愃?,都是方法運(yùn)行過(guò)程的內(nèi)存模型。并且兩個(gè)“?!眱?nèi)部構(gòu)造相同,都是線程私有。
只不過(guò)Java虛擬機(jī)棧描述的是Java方法運(yùn)行過(guò)程的內(nèi)存模型,而本地方法棧是描述Java本地方法運(yùn)行過(guò)程的內(nèi)存模型。
Java虛擬機(jī)的內(nèi)存模型中一共有兩個(gè)“堆”,一個(gè)是原本的堆,一個(gè)是方法區(qū)。方法區(qū)本質(zhì)上是屬于堆的一個(gè)邏輯部分。堆中存放對(duì)象,方法區(qū)中存放類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯的代碼。
堆是Java虛擬機(jī)中最大的一塊內(nèi)存區(qū)域,也是垃圾收集器主要的工作區(qū)域。
程序計(jì)數(shù)器、Java虛擬機(jī)棧、本地方法棧是線程私有的,即每個(gè)線程都擁有各自的程序計(jì)數(shù)器、Java虛擬機(jī)棧、本地方法棧。并且他們的生命周期和所屬的線程一樣。
而堆、方法區(qū)是線程共享的,在Java虛擬機(jī)中只有一個(gè)堆、一個(gè)方法棧。并在JVM啟動(dòng)的時(shí)候就創(chuàng)建,JVM停止才銷毀。
第二章 揭開Java對(duì)象創(chuàng)建的奧秘
2.1 對(duì)象的創(chuàng)建過(guò)程
當(dāng)虛擬機(jī)遇到一條含有new的指令時(shí),會(huì)進(jìn)行一系列對(duì)象創(chuàng)建的操作:
(1)檢查常量池中是否有即將要?jiǎng)?chuàng)建的這個(gè)對(duì)象所屬的類的符號(hào)引用;
若常量池中沒有這個(gè)類的符號(hào)引用,說(shuō)明這個(gè)類還沒有被定義!拋出ClassNotFoundException;
若常量池中有這個(gè)類的符號(hào)引用,則進(jìn)行下一步工作;
(2)進(jìn)而檢查這個(gè)符號(hào)引用所代表的類是否已經(jīng)被JVM加載;
若該類還沒有被加載,就找該類的class文件,并加載進(jìn)方法區(qū);
若該類已經(jīng)被JVM加載,則準(zhǔn)備為對(duì)象分配內(nèi)存;
(3)根據(jù)方法區(qū)中該類的信息確定該類所需的內(nèi)存大?。?/p>
一個(gè)對(duì)象所需的內(nèi)存大小是在這個(gè)對(duì)象所屬類被定義完就能確定的!且一個(gè)類所生產(chǎn)的所有對(duì)象的內(nèi)存大小是一樣的!JVM在一個(gè)類被加載進(jìn)方法區(qū)的時(shí)候就知道該類生產(chǎn)的每一個(gè)對(duì)象所需要的內(nèi)存大小。
(4)從堆中劃分一塊對(duì)應(yīng)大小的內(nèi)存空間給新的對(duì)象;分配堆中內(nèi)存有兩種方式:
指針碰撞
如果JVM的垃圾收集器采用復(fù)制算法或標(biāo)記-整理算法,那么堆中空閑內(nèi)存是完整的區(qū)域,并且空閑內(nèi)存和已使用內(nèi)存之間由一個(gè)指針標(biāo)記。那么當(dāng)為一個(gè)對(duì)象分配內(nèi)存時(shí),只需移動(dòng)指針即可。因此,這種在完整空閑區(qū)域上通過(guò)移動(dòng)指針來(lái)分配內(nèi)存的方式就叫做“指針碰撞”。
空閑列表
如果JVM的垃圾收集器采用標(biāo)記-清除算法,那么堆中空閑區(qū)域和已使用區(qū)域交錯(cuò),因此需要用一張“空閑列表”來(lái)記錄堆中哪些區(qū)域是空閑區(qū)域,從而在創(chuàng)建對(duì)象的時(shí)候根據(jù)這張“空閑列表”找到空閑區(qū)域,并分配內(nèi)存。
綜上所述:JVM究竟采用哪種內(nèi)存分配方法,取決于它使用了何種垃圾收集器。
(5)為對(duì)象中的成員變量賦上初始值(默認(rèn)初始化);
(6)設(shè)置對(duì)象頭中的信息;
(7)調(diào)用對(duì)象的構(gòu)造函數(shù)進(jìn)行初始化;
此時(shí),整個(gè)對(duì)象的創(chuàng)建過(guò)程就完成了。
2.2 對(duì)象的內(nèi)存模型
一個(gè)對(duì)象從邏輯角度看,它由成員變量和成員函數(shù)構(gòu)成,從物理角度來(lái)看,對(duì)象是存儲(chǔ)在堆中的一串二進(jìn)制數(shù),這串二進(jìn)制數(shù)的組織結(jié)構(gòu)如下。
對(duì)象在內(nèi)存中分為三個(gè)部分:
對(duì)象頭
實(shí)例數(shù)據(jù)
對(duì)齊補(bǔ)充
2.2.1 對(duì)象頭
對(duì)象頭中記錄了對(duì)象在運(yùn)行過(guò)程中所需要使用的一些數(shù)據(jù):哈希碼、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等。
此外,對(duì)象頭中可能還包含類型指針。通過(guò)該指針能確定這個(gè)對(duì)象所屬哪個(gè)類。
此外,如果對(duì)象是一個(gè)數(shù)組,那么對(duì)象頭中還要包含數(shù)組長(zhǎng)度。
2.2.2 實(shí)例數(shù)據(jù)
實(shí)力數(shù)據(jù)部分就是成員變量的值,其中包含父類的成員變量和本類的成員變量。
2.2.3 對(duì)齊補(bǔ)充
用于確保對(duì)象的總長(zhǎng)度為8字節(jié)的整數(shù)倍。
HotSpot要求對(duì)象的總長(zhǎng)度必須是8字節(jié)的整數(shù)倍。由于對(duì)象頭一定是8字節(jié)的整數(shù)倍,但實(shí)例數(shù)據(jù)部分的長(zhǎng)度是任意的,因此需要對(duì)齊補(bǔ)充字段確保整個(gè)對(duì)象的總長(zhǎng)度為8的整數(shù)倍。
2.3 訪問(wèn)對(duì)象的過(guò)程
我們知道,引用類型的變量中存放的是一個(gè)地址,那么根據(jù)地址類型的不同,對(duì)象有不同的訪問(wèn)方式:
句柄訪問(wèn)方式
堆中需要有一塊叫做“句柄池”的內(nèi)存空間,用于存放所有對(duì)象的地址和所有對(duì)象所屬類的類信息。
引用類型的變量存放的是該對(duì)象在句柄池中的地址。訪問(wèn)對(duì)象時(shí),首先需要通過(guò)引用類型的變量找到該對(duì)象的句柄,然后根據(jù)句柄中對(duì)象的地址再訪問(wèn)對(duì)象。
直接指針訪問(wèn)方式
引用類型的變量直接存放對(duì)象的地址,從而不需要句柄池,通過(guò)引用能夠直接訪問(wèn)對(duì)象。
但對(duì)象所在的內(nèi)存空間中需要額外的策略存儲(chǔ)對(duì)象所屬的類信息的地址。
比較
HotSpot采用直接指針方式訪問(wèn)對(duì)象,因?yàn)樗恍枰淮螌ぶ凡僮?,從而性能比句柄訪問(wèn)方式快一倍。但它需要額外的策略存儲(chǔ)對(duì)象在方法區(qū)中類信息的地址。
第三章 揭開 Java 對(duì)象內(nèi)存分配的秘密
Java所承諾的自動(dòng)內(nèi)存管理主要是針對(duì)對(duì)象內(nèi)存的回收和對(duì)象內(nèi)存的分配。
在Java虛擬機(jī)的五塊內(nèi)存空間中,程序計(jì)數(shù)器、Java虛擬機(jī)棧、本地方法棧內(nèi)存的分配和回收都具有確定性,一半都在編譯階段就能確定下來(lái)需要分配的內(nèi)存大小,并且由于都是線程私有,因此它們的內(nèi)存空間都隨著線程的創(chuàng)建而創(chuàng)建,線程的結(jié)束而回收。也就是這三個(gè)區(qū)域的內(nèi)存分配和回收都具有確定性。
而Java虛擬機(jī)中的方法區(qū)因?yàn)槭怯脕?lái)存儲(chǔ)類信息、常量
靜態(tài)變量,這些數(shù)據(jù)的變動(dòng)性較小,因此不是Java內(nèi)存管理重點(diǎn)需要關(guān)注的區(qū)域。
而對(duì)于堆,所有線程共享,所有的對(duì)象都需要在堆中創(chuàng)建和回收。雖然每個(gè)對(duì)象的大小在類加載的時(shí)候就能確定,但對(duì)象的數(shù)量只有在程序運(yùn)行期間才能確定,因此堆中內(nèi)存的分配具有較大的不確定性。此外,對(duì)象的生命周期長(zhǎng)短不一,因此需要針對(duì)不同生命周期的對(duì)象采用不同的內(nèi)存回收算法,增加了內(nèi)存回收的復(fù)雜性。
綜上所述:Java自動(dòng)內(nèi)存管理最核心的功能是堆內(nèi)存中對(duì)象的分配與回收。
3.1 對(duì)象優(yōu)先在 Eden 區(qū)中分配
目前主流的垃圾收集器都會(huì)采用分代回收算法,因此需要將堆內(nèi)存分為新生代和老年代。
在新生代中為了防止內(nèi)存碎片問(wèn)題,因此垃圾收集器一般都選用“復(fù)制”算法。因此,堆內(nèi)存的新生代被進(jìn)一步分為:Eden區(qū)+Survior1區(qū)+Survior2區(qū)。
每次創(chuàng)建對(duì)象時(shí),首先會(huì)在Eden區(qū)中分配。
若Eden區(qū)已滿,則在Survior1區(qū)中分配。
若Eden區(qū)+Survior1區(qū)剩余內(nèi)存太少,導(dǎo)致對(duì)象無(wú)法放入該區(qū)域時(shí),就會(huì)啟用“分配擔(dān)保”,將當(dāng)前Eden區(qū)+Survior1區(qū)中的對(duì)象轉(zhuǎn)移到老年代中,然后再將新對(duì)象存入Eden區(qū)。
3.2 大對(duì)象直接進(jìn)入老年代
所謂“大對(duì)象”就是指一個(gè)占用大量連續(xù)存儲(chǔ)空間的對(duì)象,如數(shù)組。
當(dāng)發(fā)現(xiàn)一個(gè)大對(duì)象在Eden區(qū)+Survior1區(qū)中存不下的時(shí)候就需要分配擔(dān)保機(jī)制把當(dāng)前Eden區(qū)+Survior1區(qū)的所有對(duì)象都復(fù)制到老年代中去。
我們知道,一個(gè)大對(duì)象能夠存入Eden區(qū)+Survior1區(qū)的概率比較小,發(fā)生分配擔(dān)保的概率比較大,而分配擔(dān)保需要涉及到大量的復(fù)制,就會(huì)造成效率低下。
因此,對(duì)于大對(duì)象我們直接把他放到老年代中去,從而就能避免大量的復(fù)制操作。
那么,什么樣的對(duì)象才是“大對(duì)象”呢?
通過(guò)-XX:PretrnureSizeThreshold參數(shù)設(shè)置大對(duì)象
該參數(shù)用于設(shè)置大小超過(guò)該參數(shù)的對(duì)象被認(rèn)為是“大對(duì)象”,直接進(jìn)入老年代。
注意:該參數(shù)只對(duì)Serial和ParNew收集器有效。
3.3 生命周期較長(zhǎng)的對(duì)象進(jìn)入老年代
老年代用于存儲(chǔ)生命周期較長(zhǎng)的對(duì)象,那么我們?nèi)绾闻袛嘁粋€(gè)對(duì)象的年齡呢?
新生代中的每個(gè)對(duì)象都有一個(gè)年齡計(jì)數(shù)器,當(dāng)新生代發(fā)生一次MinorGC后,存活下來(lái)的對(duì)象的年齡就加一,當(dāng)年齡超過(guò)一定值時(shí),就將超過(guò)該值的所有對(duì)象轉(zhuǎn)移到老年代中去。
使用-XXMaxTenuringThreshold設(shè)置新生代的最大年齡
設(shè)置該參數(shù)后,只要超過(guò)該參數(shù)的新生代對(duì)象都會(huì)被轉(zhuǎn)移到老年代中去。
3.4 相同年齡的對(duì)象內(nèi)存超過(guò)Survior內(nèi)存一半的對(duì)象進(jìn)入老年代
如果當(dāng)前新生代的Survior中,年齡相同的對(duì)象的內(nèi)存空間總和超過(guò)了Survior內(nèi)存空間的一半,那么所有年齡相同的對(duì)象和超過(guò)該年齡的對(duì)象都被轉(zhuǎn)移到老年代中去。無(wú)需等到對(duì)象的年齡超過(guò)MaxTenuringThreshold才被轉(zhuǎn)移到老年代中去。
3.5 “分配擔(dān)保”策略詳解
當(dāng)垃圾收集器準(zhǔn)備要在新生代發(fā)起一次MinorGC時(shí),首先會(huì)檢查“老年代中最大的連續(xù)空閑區(qū)域的大小 是否大于 新生代中所有對(duì)象的大???”,也就是老年代中目前能夠?qū)⑿律兴袑?duì)象全部裝下?
若老年代能夠裝下新生代中所有的對(duì)象,那么此時(shí)進(jìn)行MinorGC沒有任何風(fēng)險(xiǎn),然后就進(jìn)行MinorGC。
若老年代無(wú)法裝下新生代中所有的對(duì)象,那么此時(shí)進(jìn)行MinorGC是有風(fēng)險(xiǎn)的,垃圾收集器會(huì)進(jìn)行一次預(yù)測(cè):根據(jù)以往MinorGC過(guò)后存活對(duì)象的平均數(shù)來(lái)預(yù)測(cè)這次MinorGC后存活對(duì)象的平均數(shù)。
如果以往存活對(duì)象的平均數(shù)小于當(dāng)前老年代最大的連續(xù)空閑空間,那么就進(jìn)行MinorGC,雖然此次MinorGC是有風(fēng)險(xiǎn)的。
如果以往存活對(duì)象的平均數(shù)大于當(dāng)前老年代最大的連續(xù)空閑空間,那么就對(duì)老年代進(jìn)行一次Full GC,通過(guò)清除老年代中廢棄數(shù)據(jù)來(lái)擴(kuò)大老年代空閑空間,以便給新生代作擔(dān)保。
這個(gè)過(guò)程就是分配擔(dān)保。
注意:
分配擔(dān)保是老年代為新生代作擔(dān)保;
新生代中使用“復(fù)制”算法實(shí)現(xiàn)垃圾回收,老年代中使用“標(biāo)記-清除”或“標(biāo)記-整理”算法實(shí)現(xiàn)垃圾回收,只有使用“復(fù)制”算法的區(qū)域才需要分配擔(dān)保,因此新生代需要分配擔(dān)保,而老年代不需要分配擔(dān)保。
第四章 了解 Java 虛擬機(jī)的垃圾回收算法
Java虛擬機(jī)的內(nèi)存模型分為五個(gè)部分,分別是:程序計(jì)數(shù)器、Java虛擬機(jī)棧、本地方法棧、堆、方法區(qū)。
這五個(gè)區(qū)域既然是存儲(chǔ)空間,那么為了避免Java虛擬機(jī)在運(yùn)行期間內(nèi)存存滿的情況,就必須得有一個(gè)垃圾收集者的角色,不定期地回收一些無(wú)效內(nèi)存,以保障Java虛擬機(jī)能夠健康地持續(xù)運(yùn)行。
這個(gè)垃圾收集者就是平常我們所說(shuō)的“垃圾收集器”,那么垃圾收集器在何時(shí)清掃內(nèi)存?清掃哪些數(shù)據(jù)?這就是接下來(lái)我們要解決的問(wèn)題。
程序計(jì)數(shù)器、Java虛擬機(jī)棧、本地方法棧都是線程私有的,也就是每條線程都擁有這三塊區(qū)域,而且會(huì)隨著線程的創(chuàng)建而創(chuàng)建,線程的結(jié)束而銷毀。那么,垃圾收集器在何時(shí)清掃這三塊區(qū)域的問(wèn)題就解決了。
此外,Java虛擬機(jī)棧、本地方法棧中的棧幀會(huì)隨著方法的開始而入棧,方法的結(jié)束而出棧,并且每個(gè)棧幀中的本地變量表都是在類被加載的時(shí)候就確定的。因此以上三個(gè)區(qū)域的垃圾收集工作具有確定性,垃圾收集器能夠清楚地知道何時(shí)清掃這三塊區(qū)域中的哪些數(shù)據(jù)。
然而,堆和方法區(qū)中的內(nèi)存清理工作就沒那么容易了。
堆和方法區(qū)所有線程共享,并且都在JVM啟動(dòng)時(shí)創(chuàng)建,一直得運(yùn)行到JVM停止時(shí)。因此它們沒辦法根據(jù)線程的創(chuàng)建而創(chuàng)建、線程的結(jié)束而釋放。
堆中存放JVM運(yùn)行期間的所有對(duì)象,雖然每個(gè)對(duì)象的內(nèi)存大小在加載該對(duì)象所屬類的時(shí)候就確定了,但究竟創(chuàng)建多少個(gè)對(duì)象只有在程序運(yùn)行期間才能確定。
方法區(qū)中存放類信息、靜態(tài)成員變量、常量。類的加載是在程序運(yùn)行過(guò)程中,當(dāng)需要?jiǎng)?chuàng)建這個(gè)類的對(duì)象時(shí)才會(huì)加載這個(gè)類。因此,JVM究竟要加載多少個(gè)類也需要在程序運(yùn)行期間確定。
因此,堆和方法區(qū)的內(nèi)存回收具有不確定性,因此垃圾收集器在回收堆和方法區(qū)內(nèi)存的時(shí)候花了一些心思。
4.1 堆內(nèi)存的回收
4.1.1 如何判定哪些對(duì)象需要回收?
在對(duì)堆進(jìn)行對(duì)象回收之前,首先要判斷哪些是無(wú)效對(duì)象。我們知道,一個(gè)對(duì)象不被任何對(duì)象或變量引用,那么就是無(wú)效對(duì)象,需要被回收。一般有兩種判別方式:
引用計(jì)數(shù)法
每個(gè)對(duì)象都有一個(gè)計(jì)數(shù)器,當(dāng)這個(gè)對(duì)象被一個(gè)變量或另一個(gè)對(duì)象引用一次,該計(jì)數(shù)器加一;若該引用失效則計(jì)數(shù)器減一。當(dāng)計(jì)數(shù)器為0時(shí),就認(rèn)為該對(duì)象是無(wú)效對(duì)象。
可達(dá)性分析法
所有和GC Roots直接或間接關(guān)聯(lián)的對(duì)象都是有效對(duì)象,和GC Roots沒有關(guān)聯(lián)的對(duì)象就是無(wú)效對(duì)象。
GC Roots是指:
Java虛擬機(jī)棧所引用的對(duì)象(棧幀中局部變量表中引用類型的變量所引用的對(duì)象)
方法區(qū)中靜態(tài)屬性引用的對(duì)象
方法區(qū)中常量所引用的對(duì)象
本地方法棧所引用的對(duì)象
兩者對(duì)比:
引用計(jì)數(shù)法雖然簡(jiǎn)單,但存在一個(gè)嚴(yán)重的問(wèn)題,它無(wú)法解決循環(huán)引用的問(wèn)題。
因此,目前主流語(yǔ)言均使用可達(dá)性分析方法來(lái)判斷對(duì)象是否有效。
4.1.2 回收無(wú)效對(duì)象的過(guò)程
當(dāng)JVM篩選出失效的對(duì)象之后,并不是立即清除,而是再給對(duì)象一次重生的機(jī)會(huì),具體過(guò)程如下:
(1)判斷該對(duì)象是否覆蓋了finalize()方法
若已覆蓋該方法,并該對(duì)象的finalize()方法還沒有被執(zhí)行過(guò),那么就會(huì)將finalize()扔到F-Queue隊(duì)列中;
若未覆蓋該方法,則直接釋放對(duì)象內(nèi)存。
(2)執(zhí)行F-Queue隊(duì)列中的finalize()方法
虛擬機(jī)會(huì)以較低的優(yōu)先級(jí)執(zhí)行這些finalize()方法們,也不會(huì)確保所有的finalize()方法都會(huì)執(zhí)行結(jié)束。如果finalize()方法中出現(xiàn)耗時(shí)操作,虛擬機(jī)就直接停止執(zhí)行,將該對(duì)象清除。
(3)對(duì)象重生或死亡
如果在執(zhí)行finalize()方法時(shí),將this賦給了某一個(gè)引用,那么該對(duì)象就重生了。如果沒有,那么就會(huì)被垃圾收集器清除。
注意:強(qiáng)烈不建議使用finalize()函數(shù)進(jìn)行任何操作!如果需要釋放資源,請(qǐng)使用try-finally。因?yàn)閒inalize()不確定性大,開銷大,無(wú)法保證順利執(zhí)行。
4.2 方法區(qū)的內(nèi)存回收
我們知道,如果使用復(fù)制算法實(shí)現(xiàn)堆的內(nèi)存回收,堆就會(huì)被分為新生代和老年代,新生代中的對(duì)象“朝生夕死”,每次垃圾回收都會(huì)清除掉大量的對(duì)象;而老年代中的對(duì)象生命較長(zhǎng),每次垃圾回收只有少量的對(duì)象被清除掉。
由于方法區(qū)中存放生命周期較長(zhǎng)的類信息、常量、靜態(tài)變量,因此方法區(qū)就像是堆的老年代,每次垃圾收集的只有少量的垃圾被清除掉。
方法區(qū)中主要清除兩種垃圾:
廢棄常量
廢棄的類
4.2.1 如何判定廢棄常量?
清除廢棄的常量和清除對(duì)象類似,只要常量池中的常量不被任何變量或?qū)ο笠茫敲催@些常量就會(huì)被清除掉。
4.2.2 如何廢棄廢棄的類?
清除廢棄類的條件較為苛刻:
該類的所有對(duì)象都已被清除
該類的java.lang.Class對(duì)象沒有被任何對(duì)象或變量引用
只要一個(gè)類被虛擬機(jī)加載進(jìn)方法區(qū),那么在堆中就會(huì)有一個(gè)代表該類的對(duì)象:java.lang.Class。這個(gè)對(duì)象在類被加載進(jìn)方法區(qū)的時(shí)候創(chuàng)建,在方法區(qū)中該類被刪除時(shí)清除。
加載該類的ClassLoader已經(jīng)被回收
4.3 垃圾收集算法
現(xiàn)在我們知道了判定一個(gè)對(duì)象是無(wú)效對(duì)象、判定一個(gè)類是廢棄類、判定一個(gè)常量是廢棄常量的方法,也就是知道了垃圾收集器會(huì)清除哪些數(shù)據(jù),那么接下來(lái)介紹如何清除這些數(shù)據(jù)。
4.3.1 標(biāo)記-清除算法
首先利用剛才介紹的方法判斷需要清除哪些數(shù)據(jù),并給它們做上標(biāo)記;然后清除被標(biāo)記的數(shù)據(jù)。
分析:
這種算法標(biāo)記和清除過(guò)程效率都很低,而且清除完后存在大量碎片空間,導(dǎo)致無(wú)法存儲(chǔ)大對(duì)象,降低了空間利用率。
4.3.2 復(fù)制算法
將內(nèi)存分成兩份,只將數(shù)據(jù)存儲(chǔ)在其中一塊上。當(dāng)需要回收垃圾時(shí),也是首先標(biāo)記出廢棄的數(shù)據(jù),然后將有用的數(shù)據(jù)復(fù)制到另一塊內(nèi)存上,最后將第一塊內(nèi)存全部清除。
分析:
這種算法避免了碎片空間,但內(nèi)存被縮小了一半。
而且每次都需要將有用的數(shù)據(jù)全部復(fù)制到另一片內(nèi)存上去,效率不高。
解決空間利用率問(wèn)題:
在新生代中,由于大量的對(duì)象都是“朝生夕死”,也就是一次垃圾收集后只有少量對(duì)象存活,因此我們可以將內(nèi)存劃分成三塊:Eden、Survior1、Survior2,內(nèi)存大小分別是8:1:1。分配內(nèi)存時(shí),只使用Eden和一塊Survior1。當(dāng)發(fā)現(xiàn)Eden+Survior1的內(nèi)存即將滿時(shí),JVM會(huì)發(fā)起一次MinorGC,清除掉廢棄的對(duì)象,并將所有存活下來(lái)的對(duì)象復(fù)制到另一塊Survior2中。那么,接下來(lái)就使用Survior2+Eden進(jìn)行內(nèi)存分配。
通過(guò)這種方式,只需要浪費(fèi)10%的內(nèi)存空間即可實(shí)現(xiàn)帶有壓縮功能的垃圾收集方法,避免了內(nèi)存碎片的問(wèn)題。
但是,當(dāng)一個(gè)對(duì)象要申請(qǐng)內(nèi)存空間時(shí),發(fā)現(xiàn)Eden+Survior中剩下的空間無(wú)法放置該對(duì)象,此時(shí)需要進(jìn)行Minor GC,如果MinorGC過(guò)后空閑出來(lái)的內(nèi)存空間仍然無(wú)法放置該對(duì)象,那么此時(shí)就需要將對(duì)象轉(zhuǎn)移到老年代中,這種方式叫做“分配擔(dān)?!?。
什么是分配擔(dān)保?
當(dāng)JVM準(zhǔn)備為一個(gè)對(duì)象分配內(nèi)存空間時(shí),發(fā)現(xiàn)此時(shí)Eden+Survior中空閑的區(qū)域無(wú)法裝下該對(duì)象,那么就會(huì)觸發(fā)MinorGC,對(duì)該區(qū)域的廢棄對(duì)象進(jìn)行回收。但如果MinorGC過(guò)后只有少量對(duì)象被回收,仍然無(wú)法裝下新對(duì)象,那么此時(shí)需要將Eden+Survior中的所有對(duì)象都轉(zhuǎn)移到老年代中,然后再將新對(duì)象存入Eden區(qū)。這個(gè)過(guò)程就是“分配擔(dān)保”。
4.3.3 標(biāo)記-整理算法
在回收垃圾前,首先將所有廢棄的對(duì)象做上標(biāo)記,然后將所有未被標(biāo)記的對(duì)象移到一邊,最后清空另一邊區(qū)域即可。
分析:
它是一種老年代的垃圾收集算法。老年代中的對(duì)象一般壽命比較長(zhǎng),因此每次垃圾回收會(huì)有大量對(duì)象存活,因此如果選用“復(fù)制”算法,每次需要復(fù)制大量存活的對(duì)象,會(huì)導(dǎo)致效率很低。而且,在新生代中使用“復(fù)制”算法,當(dāng)Eden+Survior中都裝不下某個(gè)對(duì)象時(shí),可以使用老年代的內(nèi)存進(jìn)行“分配擔(dān)?!?,而如果在老年代使用該算法,那么在老年代中如果出現(xiàn)Eden+Survior裝不下某個(gè)對(duì)象時(shí),沒有其他區(qū)域給他作分配擔(dān)保。因此,老年代中一般使用“標(biāo)記-整理”算法。
4.3.4 分代收集算法
將內(nèi)存劃分為老年代和新生代。老年代中存放壽命較長(zhǎng)的對(duì)象,新生代中存放“朝生夕死”的對(duì)象。然后在不同的區(qū)域使用不同的垃圾收集算法。
4.4 Java中引用的種類
Java中根據(jù)生命周期的長(zhǎng)短,將引用分為4類。
4.4.1 強(qiáng)引用
我們平時(shí)所使用的引用就是強(qiáng)引用。
A a = new A();
也就是通過(guò)關(guān)鍵字new創(chuàng)建的對(duì)象所關(guān)聯(lián)的引用就是強(qiáng)引用。
只要強(qiáng)引用存在,該對(duì)象永遠(yuǎn)也不會(huì)被回收。
4.4.2 軟引用
只有當(dāng)堆即將發(fā)生OOM異常時(shí),JVM才會(huì)回收軟引用所指向的對(duì)象。
軟引用通過(guò)SoftReference類實(shí)現(xiàn)。
軟引用的生命周期比強(qiáng)引用短一些。
4.4.3 弱引用
只要垃圾收集器運(yùn)行,軟引用所指向的對(duì)象就會(huì)被回收。
弱引用通過(guò)WeakReference類實(shí)現(xiàn)。
弱引用的生命周期比軟引用短。
4.4.4 虛引用
虛引用也叫幽靈引用,它和沒有引用沒有區(qū)別,無(wú)法通過(guò)虛引用訪問(wèn)對(duì)象的任何屬性或函數(shù)。
一個(gè)對(duì)象關(guān)聯(lián)虛引用唯一的作用就是在該對(duì)象被垃圾收集器回收之前會(huì)受到一條系統(tǒng)通知。
虛引用通過(guò)PhantomReference類來(lái)實(shí)現(xiàn)。
第五章 class 文件結(jié)構(gòu)詳解
5.1 什么是JVM的“無(wú)關(guān)性”?
Java具有平臺(tái)無(wú)關(guān)性,也就是任何操作系統(tǒng)都能運(yùn)行Java代碼。之所以能實(shí)現(xiàn)這一點(diǎn),是因?yàn)镴ava運(yùn)行在虛擬機(jī)之上,不同的操作系統(tǒng)都擁有各自的Java虛擬機(jī),因此Java能實(shí)現(xiàn)“一次編寫,處處運(yùn)行”。
而JVM不僅具有平臺(tái)無(wú)關(guān)性,還具有語(yǔ)言無(wú)關(guān)性。
平臺(tái)無(wú)關(guān)性是指不同操作系統(tǒng)都有各自的JVM,而語(yǔ)言無(wú)關(guān)性是指Java虛擬機(jī)能運(yùn)行除Java以外的代碼!
這聽起來(lái)非常驚人,但JVM對(duì)能運(yùn)行的語(yǔ)言是有嚴(yán)格要求的。首先來(lái)了解下Java代碼的運(yùn)行過(guò)程。
Java源代碼首先需要使用Javac編譯器編譯成class文件,然后啟動(dòng)JVM執(zhí)行class文件,從而程序開始運(yùn)行。
也就是JVM只認(rèn)識(shí)class文件,它并不管何種語(yǔ)言生成了class文件,只要class文件符合JVM的規(guī)范就能運(yùn)行。
因此目前已經(jīng)有Scala、JRuby、Jython等語(yǔ)言能夠在JVM上運(yùn)行。它們有各自的語(yǔ)法規(guī)則,不過(guò)它們的編譯器都能將各自的源碼編譯成符合JVM規(guī)范的class文件,從而能夠借助JVM運(yùn)行它們。
5.2 縱觀Class文件結(jié)構(gòu)
class文件是二進(jìn)制文件,它的內(nèi)容具有嚴(yán)格的規(guī)范,文件中沒有任何空格,全是連續(xù)的0/1。class文件中的所有內(nèi)容被分為兩種類型:無(wú)符號(hào)數(shù) 和 表。
無(wú)符號(hào)數(shù):它表示class文件中的值,這些值沒有任何類型,但有不同的長(zhǎng)度。根據(jù)這些值長(zhǎng)度的不同分為:u1、u2、u4、u8,分別代表1字節(jié)的無(wú)符號(hào)數(shù)、2字節(jié)的無(wú)符號(hào)數(shù)、4字節(jié)的無(wú)符號(hào)數(shù)、8字節(jié)的無(wú)符號(hào)數(shù)。
表:class文件中所有數(shù)據(jù)(即無(wú)符號(hào)數(shù))要么單獨(dú)存在,要么由多個(gè)無(wú)符號(hào)數(shù)組成二維表。即class文件中的數(shù)據(jù)要么是單個(gè)值,要么是二維表。
5.2.1 class文件的組織結(jié)構(gòu)
魔數(shù)
本文件的版本信息
常量池
訪問(wèn)標(biāo)志
類索引
父類索引
接口索引集合
字段表集合
方法表集合
5.3 Class文件的構(gòu)成1:魔數(shù)
class文件的頭4個(gè)字節(jié)稱為魔數(shù),用來(lái)表示這個(gè)class文件的類型。
魔數(shù)的作用就相當(dāng)于文件后綴名,只不過(guò)后綴名容易被修改,不安全,因此在class文件中標(biāo)示文件類型比較合適。
class文件的魔數(shù)是用16進(jìn)制表示的“CAFEBABE”,非常具有浪漫主義色彩,誰(shuí)說(shuō)程序員的情商都很低!
5.4 Class文件的構(gòu)成2:版本信息
緊接著魔數(shù)的4個(gè)字節(jié)是版本號(hào)。它表示本class中使用的是哪個(gè)版本的JDK。
在高版本的JVM上能夠運(yùn)行低版本的class文件,但在低版本的JVM上無(wú)法運(yùn)行高版本的class文件,即使該class文件中沒有用到任何高版本JDK的特性也無(wú)法運(yùn)行!
5.5 Class文件的構(gòu)成3:常量池
5.5.1 什么是常量池?
緊接著版本號(hào)之后的就是常量池。常量池中存放兩種類型的常量:
字面值常量
字面值常量即我們?cè)诔绦蛑卸x的字符串、被final修飾的值。
符號(hào)引用
符號(hào)引用就是我們定義的各種名字:
類和接口的全限定名
字段的名字 和 描述符
方法的名字 和 描述符
5.5.2 常量池的特點(diǎn)
常量池長(zhǎng)度不固定
常量池的大小是不固定的,因此常量池開頭放置一個(gè)u2類型的無(wú)符號(hào)數(shù),用來(lái)存儲(chǔ)當(dāng)前常量池的容量。JVM根據(jù)這個(gè)值就知道常量池的頭尾來(lái)。
注:這個(gè)值是從1開始的,若為5表示池中有4個(gè)常量。
常量池中的常量由而為表來(lái)表示
常量池開頭有個(gè)常量池容量計(jì)數(shù)器,接下來(lái)就全是一個(gè)個(gè)常量了,只不過(guò)常量都是由一張張二維表構(gòu)成,除了記錄常量的值以外,還記錄當(dāng)前常量的相關(guān)信息。
常量池是class文件的資源倉(cāng)庫(kù)
常量池是與本class中其它部分關(guān)聯(lián)最多的部分
常量池是class文件中空間占用最大的部分之一
5.5.3 常量池中常量的類型
剛才介紹了,常量池中的常量大體上分為:字面值常量 和 符號(hào)引用。在此基礎(chǔ)上,根據(jù)常量的數(shù)據(jù)類型不同,又可以被細(xì)分為14種常量類型。這14種常量類型都有各自的二維表示結(jié)構(gòu)。每種常量類型的頭1個(gè)字節(jié)都是tag,用于表示當(dāng)前常量屬于14種類型中的哪一個(gè)。
以CONSTANT_Class_info常量為例,它的二維表示結(jié)構(gòu)如下:
CONSTANT_Class_info表:
類型名稱數(shù)量u1tag1u2name_index1
tag表示當(dāng)前常量的類型(當(dāng)前常量為CONSTANT_Class_info,因此tag的值應(yīng)為7,表示一個(gè)類或接口的全限定名);
name_index表示這個(gè)類或接口全限定名的位置。它的值表示指向常量池的第幾個(gè)常量。它會(huì)指向一個(gè)CONSTANT_Utf8_info類型的常量,它的二維表結(jié)構(gòu)如下:
CONSTANT_Utf8_info表:
類型名稱數(shù)量u1tag1u2length2u1byteslength
CONSTANT_Utf8_info表示字符串常量;
tag表示當(dāng)前常量的類型,這里應(yīng)該是1;
length表示這個(gè)字符串的長(zhǎng)度;
bytes為這個(gè)字符串的內(nèi)容(采用縮略的UTF8編碼)
問(wèn):為什么Java中定義的類、變量名字必須小于64K?
類、接口、變量等名字都屬于符號(hào)引用,它們都存儲(chǔ)在常量池中。而不管哪種符號(hào)引用,它們的名字都由CONSTANT_Utf8_info類型的常量表示,這種類型的常量使用u2存儲(chǔ)字符串的長(zhǎng)度。由于2字節(jié)最多能表示65535個(gè)數(shù),因此這些名字的最大長(zhǎng)度最多只能是64K。
問(wèn):什么是UTF-8編碼?什么是縮略UTF-8編碼?
前者每個(gè)字符使用3個(gè)字節(jié)表示,而后者把128個(gè)ASKII碼用1字節(jié)表示,某些字符用2字節(jié)表示,某些字符用3字節(jié)表示。
5.6 Class文件的構(gòu)成4:訪問(wèn)標(biāo)志
在常量池之后是2字節(jié)的訪問(wèn)標(biāo)志。訪問(wèn)標(biāo)志是用來(lái)表示這個(gè)class文件是類還是接口、是否被public修飾、是否被abstract修飾、是否被final修飾等。
由于這些標(biāo)志都由是/否表示,因此可以用0/1表示。
訪問(wèn)標(biāo)志為2字節(jié),可以表示16位標(biāo)志,但JVM目前只定義了8種,未定義的直接寫0.
5.7 Class文件的構(gòu)成5:類索引、父類索引、接口索引集合
類索引、父類索引、接口索引集合是用來(lái)表示當(dāng)前class文件所表示類的名字、父類名字、接口們的名字。
它們按照順序依次排列,類索引和父類索引各自使用一個(gè)u2類型的無(wú)符號(hào)常量,這個(gè)常量指向CONSTANT_Class_info類型的常量,該常量的bytes字段記錄了本類、父類的全限定名。
由于一個(gè)類的接口可能有好多個(gè),因此需要用一個(gè)集合來(lái)表示接口索引,它在類索引和父類索引之后。這個(gè)集合頭兩個(gè)字節(jié)表示接口索引集合的長(zhǎng)度,接下來(lái)就是接口的名字索引。
5.8 Class文件的構(gòu)成6:字段表的集合
5.8.1 什么是字段表集合?
接下來(lái)是字段表的集合。字段表集合用于存儲(chǔ)本類所涉及到的成員變量,包括實(shí)例變量和類變量,但不包括方法中的局部變量。
每一個(gè)字段表只表示一個(gè)成員變量,本類中所有的成員變量構(gòu)成了字段表集合。
5.8.2 字段表結(jié)構(gòu)的定義
類型名稱數(shù)量u2access_flags1u2name_index1u2descriptor_index1u2attributes_count1attribute_infoattributesattributes_count
access_flags:字段的訪問(wèn)標(biāo)志。在Java中,每個(gè)成員變量都有一系列的修飾符,和上述class文件的訪問(wèn)標(biāo)志的作用一樣,只不過(guò)成員變量的訪問(wèn)標(biāo)志與類的訪問(wèn)標(biāo)志稍有區(qū)別。
name_index:本字段名字的索引。指向一個(gè)CONSTANT_Class_info類型的常量,這里面存儲(chǔ)了本字段的名字等信息。
descriptor_index:描述符。用于描述本字段在Java中的數(shù)據(jù)類型等信息(下面詳細(xì)介紹)。
attributes_count:屬性表集合的長(zhǎng)度。
attributes:屬性表集合。到descriptor_index為止是字段表的固定信息,光有上述信息可能無(wú)法完整地描述一個(gè)字段,因此用屬性表集合來(lái)存放額外的信息,比如一個(gè)字段的值(下面會(huì)詳細(xì)介紹)。
5.8.3 什么是描述符?
成員變量(包括靜態(tài)成員變量和實(shí)例變量)和 方法都有各自的描述符。
對(duì)于字段而言,描述符用于描述字段的數(shù)據(jù)類型;
對(duì)于方法而言,描述符用于描述字段的數(shù)據(jù)類型、參數(shù)列表、返回值。
在描述符中,基本數(shù)據(jù)類型用大寫字母表示,對(duì)象類型用“L對(duì)象類型的全限定名”表示,數(shù)組用“[數(shù)組類型的全限定名”表示。
描述方法時(shí),將參數(shù)根據(jù)上述規(guī)則放在()中,()右側(cè)按照上述方法放置返回值。而且,參數(shù)之間無(wú)需任何符號(hào)。
5.8.4 字段表集合的注意點(diǎn)
一個(gè)class文件的字段表集合中不能出現(xiàn)從父類/接口繼承而來(lái)字段;
一個(gè)class文件的字段表集合中可能會(huì)出現(xiàn)程序猿沒有定義的字段
如編譯器會(huì)自動(dòng)地在內(nèi)部類的class文件的字段表集合中添加外部類對(duì)象的成員變量,供內(nèi)部類訪問(wèn)外部類。
Java中只要兩個(gè)字段名字相同就無(wú)法通過(guò)編譯。但在JVM規(guī)范中,允許兩個(gè)字段的名字相同但描述符不同的情況,并且認(rèn)為它們是兩個(gè)不同的字段。
5.9 Class文件的構(gòu)成7:方法表的集合
在class文件中,所有的方法以二維表的形式存儲(chǔ),每張表來(lái)表示一個(gè)函數(shù),一個(gè)類中的所有方法構(gòu)成方法表的集合。
方法表的結(jié)構(gòu)和字段表的結(jié)構(gòu)一致,只不過(guò)訪問(wèn)標(biāo)志和屬性表集合的可選項(xiàng)有所不同。
類型名稱數(shù)量u2access_flags1u2name_index1u2descriptor_index1u2attributes_count1attribute_infoattributesattributes_count
方法表的屬性表集合中有一張Code屬性表,用于存儲(chǔ)當(dāng)前方法經(jīng)編譯器編譯過(guò)后的字節(jié)碼指令。
方法表集合的注意點(diǎn)
如果本class沒有重寫父類的方法,那么本class文件的方法表集合中是不會(huì)出現(xiàn)父類/父接口的方法表;
本class的方法表集合可能出現(xiàn)程序猿沒有定義的方法
編譯器在編譯時(shí)會(huì)在class文件的方法表集合中加入類構(gòu)造器
和實(shí)例構(gòu)造器。
重載一個(gè)方法需要有相同的簡(jiǎn)單名稱和不同的特征簽名。JVM的特征簽名和Java的特征簽名有所不同:
Java特征簽名:方法參數(shù)在常量池中的字段符號(hào)引用的集合
JVM特征簽名:方法參數(shù)+返回值
第六章 詳解 Java 類的加載過(guò)程
6.1 類的生命周期
一個(gè)類從加載進(jìn)內(nèi)存到卸載出內(nèi)存為止,一共經(jīng)歷7個(gè)階段:
加載——>驗(yàn)證——>準(zhǔn)備——>解析——>初始化——>使用——>卸載
其中,類加載包括5個(gè)階段:
加載——>驗(yàn)證——>準(zhǔn)備——>解析——>初始化
在類加載的過(guò)程中,以下3個(gè)過(guò)程稱為連接:
驗(yàn)證——>準(zhǔn)備——>解析
因此,JVM的類加載過(guò)程也可以概括為3個(gè)過(guò)程:
加載——>連接——>初始化
C/C++在運(yùn)行前需要完成預(yù)處理、編譯、匯編、鏈接;而在Java中,類加載(加載、連接、初始化)是在程序運(yùn)行期間完成的。
在程序運(yùn)行期間進(jìn)行類加載會(huì)稍微增加程序的開銷,但隨之會(huì)帶來(lái)更大的好處——提高程序的靈活性。Java語(yǔ)言的靈活性體現(xiàn)在它可以在運(yùn)行期間動(dòng)態(tài)擴(kuò)展,所謂動(dòng)態(tài)擴(kuò)展就是在運(yùn)行期間動(dòng)態(tài)加載和動(dòng)態(tài)連接。
6.2 類加載的時(shí)機(jī)
6.2.1 類加載過(guò)程中每個(gè)步驟的順序
我們已經(jīng)知道,類加載的過(guò)程包括:加載、連接、初始化,連接又分為:驗(yàn)證、準(zhǔn)備、解析,所以說(shuō)類加載一共分為5步:加載、驗(yàn)證、準(zhǔn)備、解析、初始化。
其中加載、驗(yàn)證、準(zhǔn)備、初始化的開始順序是依次進(jìn)行的,這些步驟開始之后的過(guò)程可能會(huì)有重疊。
而解析過(guò)程會(huì)發(fā)生在初始化過(guò)程中。
6.2.2 類加載過(guò)程中“初始化”開始的時(shí)機(jī)
JVM規(guī)范中只定義了類加載過(guò)程中初始化過(guò)程開始的時(shí)機(jī),加載、連接過(guò)程都應(yīng)該在初始化之前開始(解析除外),這些過(guò)程具體在何時(shí)開始,JVM規(guī)范并沒有定義,不同的虛擬機(jī)可以根據(jù)具體的需求自定義。
初始化開始的時(shí)機(jī):
在運(yùn)行過(guò)程中遇到如下字節(jié)碼指令時(shí),如果類尚未初始化,那就要進(jìn)行初始化:new、getstatic、putstatic、invokestatic。這四個(gè)指令對(duì)應(yīng)的Java代碼場(chǎng)景是:
通過(guò)new創(chuàng)建對(duì)象;
讀取、設(shè)置一個(gè)類的靜態(tài)成員變量(不包括final修飾的靜態(tài)變量);
調(diào)用一個(gè)類的靜態(tài)成員函數(shù)。
使用java.lang.reflect進(jìn)行反射調(diào)用的時(shí)候,如果類沒有初始化,那就需要初始化;
當(dāng)初始化一個(gè)類的時(shí)候,若其父類尚未初始化,那就先要讓其父類初始化,然后再初始化本類;
當(dāng)虛擬機(jī)啟動(dòng)時(shí),虛擬機(jī)會(huì)首先初始化帶有main方法的類,即主類;
6.2.3 主動(dòng)引用 與 被動(dòng)引用
JVM規(guī)范中要求在程序運(yùn)行過(guò)程中,“當(dāng)且僅當(dāng)”出現(xiàn)上述4個(gè)條件之一的情況才會(huì)初始化一個(gè)類。如果間接滿足上述初始化條件是不會(huì)初始化類的。
其中,直接滿足上述初始化條件的情況叫做主動(dòng)引用;間接滿足上述初始化過(guò)程的情況叫做被動(dòng)引用。
那么,只有當(dāng)程序在運(yùn)行過(guò)程中滿足主動(dòng)引用的時(shí)候才會(huì)初始化一個(gè)類,若滿足被動(dòng)引用就不會(huì)初始化一個(gè)類。
6.2.4 被動(dòng)引用的場(chǎng)景示例
示例一
public?class?Fu{ ?public?static?String?name?=?"柴毛毛"; ?static{ ?System.out.println("父類被初始化!"); ?} } public?class?Zi{ ?static{ ?System.out.println("子類被初始化!"); ?} } public?static?void?main(String[]?args){ ?System.out.println(Zi.name); }
輸出結(jié)果:
父類被初始化!
柴毛毛
原因分析:
本示例看似滿足初始化時(shí)機(jī)的第一條:當(dāng)要獲取某一個(gè)類的靜態(tài)成員變量的時(shí)候如果該類尚未初始化,則對(duì)該類進(jìn)行初始化。
但由于這個(gè)靜態(tài)成員變量屬于Fu類,Zi類只是間接調(diào)用Fu類中的靜態(tài)成員變量,因此Zi類調(diào)用name屬性屬于間接引用,而Fu類調(diào)用name屬性屬于直接引用,由于JVM只初始化直接引用的類,因此只有Fu類被初始化。
示例二
public?class?A{ ?public?static?void?main(String[]?args){ ?Fu[]?arr?=?new?Fu[10]; ?} }
輸出結(jié)果:
并沒有輸出“父類被初始化!”
原因分析:
這個(gè)過(guò)程看似滿足初始化時(shí)機(jī)的第一條:遇到new創(chuàng)建對(duì)象時(shí)若類沒被初始化,則初始化該類。
但現(xiàn)在通過(guò)new要?jiǎng)?chuàng)建的是一個(gè)數(shù)組對(duì)象,而非Fu類對(duì)象,因此也屬于間接引用,不會(huì)初始化Fu類。
示例三
public?class?Fu{ ?public?static?final?String?name?=?"柴毛毛"; ?static{ ?System.out.println("父類被初始化!"); ?} } public?class?A{ ?public?static?void?main(String[]?args){ ?System.out.println(Fu.name); ?} }
輸出結(jié)果:
柴毛毛
原因分析:
本示例看似滿足類初始化時(shí)機(jī)的第一個(gè)條件:獲取一個(gè)類靜態(tài)成員變量的時(shí)候若類尚未初始化則初始化類。
但是,F(xiàn)u類的靜態(tài)成員變量被final修飾,它已經(jīng)是一個(gè)常量。被final修飾的常量在Java代碼編譯的過(guò)程中就會(huì)被放入它被引用的class文件的常量池中(這里是A的常量池)。所以程序在運(yùn)行期間如果需要調(diào)用這個(gè)常量,直接去當(dāng)前類的常量池中取,而不需要初始化這個(gè)類。
6.2.5 接口的初始化
接口和類都需要初始化,接口和類的初始化過(guò)程基本一樣,不同點(diǎn)在于:類初始化時(shí),如果發(fā)現(xiàn)父類尚未被初始化,則先要初始化父類,然后再初始化自己;但接口初始化時(shí),并不要求父接口已經(jīng)全部初始化,只有程序在運(yùn)行過(guò)程中用到當(dāng)父接口中的東西時(shí)才初始化父接口。
6.3 類加載的過(guò)程
通過(guò)之前的介紹可知,類加載過(guò)程共有5個(gè)步驟,分別是:加載、驗(yàn)證、準(zhǔn)備、解析、初始化。其中,驗(yàn)證、準(zhǔn)備、解析稱為連接。下面詳細(xì)介紹這5個(gè)過(guò)程JVM所做的工作。
6.3.1 加載
注意:“加載”是“類加載”過(guò)程的第一步,千萬(wàn)不要混淆。
在加載過(guò)程中,JVM主要做3件事情:
通過(guò)一個(gè)類的全限定名來(lái)獲取這個(gè)類的二進(jìn)制字節(jié)流,即class文件:
在程序運(yùn)行過(guò)程中,當(dāng)要訪問(wèn)一個(gè)類時(shí),若發(fā)現(xiàn)這個(gè)類尚未被加載,并滿足類初始化時(shí)機(jī)的條件時(shí),就根據(jù)要被初始化的這個(gè)類的全限定名找到該類的二進(jìn)制字節(jié)流,開始加載過(guò)程。
將二進(jìn)制字節(jié)流的存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為特定的數(shù)據(jù)結(jié)構(gòu),存儲(chǔ)在方法區(qū)中;
在內(nèi)存中創(chuàng)建一個(gè)java.lang.Class類型的對(duì)象:
接下來(lái)程序在運(yùn)行過(guò)程中所有對(duì)該類的訪問(wèn)都通過(guò)這個(gè)類對(duì)象,也就是這個(gè)Class類型的類對(duì)象是提供給外界訪問(wèn)該類的接口。
從哪里加載?
JVM規(guī)范對(duì)于加載過(guò)程給予了較大的寬松度。一般二進(jìn)制字節(jié)流都從已經(jīng)編譯好的本地class文件中讀取,此外還可以從以下地方讀?。?/p>
從壓縮包中讀取,如:Jar、War、Ear等。
從其它文件中動(dòng)態(tài)生成,如:從JSP文件中生成Class類。
從數(shù)據(jù)庫(kù)中讀取,將二進(jìn)制字節(jié)流存儲(chǔ)至數(shù)據(jù)庫(kù)中,然后在加載時(shí)從數(shù)據(jù)庫(kù)中讀取。有些中間件會(huì)這么做,用來(lái)實(shí)現(xiàn)代碼在集群間分發(fā)。
從網(wǎng)絡(luò)中獲取,從網(wǎng)絡(luò)中獲取二進(jìn)制字節(jié)流。典型就是Applet。
類 和 數(shù)組加載過(guò)程的區(qū)別?
數(shù)組也有類型,稱為“數(shù)組類型”。如:
String[]?str?=?new?String[10];
這個(gè)數(shù)組的數(shù)組類型是Ljava.lang.String,而String只是這個(gè)數(shù)組中元素的類型。
當(dāng)程序在運(yùn)行過(guò)程中遇到new關(guān)鍵字創(chuàng)建一個(gè)數(shù)組時(shí),由JVM直接創(chuàng)建數(shù)組類,再由類加載器創(chuàng)建數(shù)組中的元素類。
而普通類的加載由類加載器完成。既可以使用系統(tǒng)提供的引導(dǎo)類加載器,也可以使用用戶自定義的類加載器。
加載過(guò)程的注意點(diǎn)
JVM規(guī)范并未給出類在方法區(qū)中存放的數(shù)據(jù)結(jié)構(gòu)
類完成加載后,二進(jìn)制字節(jié)流就以特定的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)在方法區(qū)中,但存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)是由虛擬機(jī)自己定義的,JVM規(guī)范并沒有指定。
JVM規(guī)范并沒有指定Class對(duì)象存放的位置
在二進(jìn)制字節(jié)流以特定格式存儲(chǔ)在方法區(qū)后,JVM會(huì)創(chuàng)建一個(gè)java.lang.Class類型的對(duì)象,作為本類的外部接口。既然是對(duì)象就應(yīng)該存放在堆內(nèi)存中,不過(guò)JVM規(guī)范并沒有給出限制,不同的虛擬機(jī)根據(jù)自己的需求存放這個(gè)對(duì)象。HotSpot將Class對(duì)象存放在方法區(qū)。
加載階段和連接階段是交叉的
通過(guò)之前的介紹可知,類加載過(guò)程中每個(gè)步驟的開始順序都有嚴(yán)格限制,但每個(gè)步驟的結(jié)束順序沒有限制。也就是說(shuō),類加載過(guò)程中,必須按照如下順序開始:
加載、連接、初始化,但結(jié)束順序無(wú)所謂,因此由于每個(gè)步驟處理時(shí)間的長(zhǎng)短不一就會(huì)導(dǎo)致有些步驟會(huì)出現(xiàn)交叉。
6.3.2 驗(yàn)證
驗(yàn)證階段比較耗時(shí),它非常重要但不一定必要,如果所運(yùn)行的代碼已經(jīng)被反復(fù)使用和驗(yàn)證過(guò),那么可以使用-Xverify:none參數(shù)關(guān)閉,以縮短類加載時(shí)間。
驗(yàn)證的目的是什么?
驗(yàn)證是為了保證二進(jìn)制字節(jié)流中的信息符合虛擬機(jī)規(guī)范,并沒有安全問(wèn)題。
為什么需要驗(yàn)證?
雖然Java語(yǔ)言是一門安全的語(yǔ)言,它能確保程序猿無(wú)法訪問(wèn)數(shù)組邊界以外的內(nèi)存、避免讓一個(gè)對(duì)象轉(zhuǎn)換成任意類型、避免跳轉(zhuǎn)到不存在的代碼行,如果出現(xiàn)這些情況,編譯無(wú)法通過(guò)。也就是說(shuō),Java語(yǔ)言的安全性是通過(guò)編譯器來(lái)保證的。
但是我們知道,編譯器和虛擬機(jī)是兩個(gè)獨(dú)立的東西,虛擬機(jī)只認(rèn)二進(jìn)制字節(jié)流,它不會(huì)管所獲得的二進(jìn)制字節(jié)流是哪來(lái)的,當(dāng)然,如果是編譯器給它的,那么就相對(duì)安全,但如果是從其它途徑獲得的,那么無(wú)法確保該二進(jìn)制字節(jié)流是安全的。通過(guò)上文可知,虛擬機(jī)規(guī)范中沒有限制二進(jìn)制字節(jié)流的來(lái)源,那么任意來(lái)源的二進(jìn)制字節(jié)流虛擬機(jī)都能接受,為了防止字節(jié)流中有安全問(wèn)題,因此需要驗(yàn)證!
驗(yàn)證的過(guò)程
(1)文件格式驗(yàn)證
這個(gè)階段主要驗(yàn)證輸入的二進(jìn)制字節(jié)流是否符合class文件結(jié)構(gòu)的規(guī)范。二進(jìn)制字節(jié)流只有通過(guò)了本階段的驗(yàn)證,才會(huì)被允許存入到方法區(qū)中。
本驗(yàn)證階段是基于二進(jìn)制字節(jié)流的,而后面的三個(gè)驗(yàn)證階段都是在方法區(qū)中進(jìn)行,并基于類特定的數(shù)據(jù)結(jié)構(gòu)的。
通過(guò)上文可知,加載開始前,二進(jìn)制字節(jié)流還沒進(jìn)方法區(qū),而加載完成后,二進(jìn)制字節(jié)流已經(jīng)存入方法區(qū)。而在文件格式驗(yàn)證前,二進(jìn)制字節(jié)流尚未進(jìn)入方法區(qū),文件格式驗(yàn)證通過(guò)之后才進(jìn)入方法區(qū)。也就是說(shuō),加載開始后,立即啟動(dòng)了文件格式驗(yàn)證,本階段驗(yàn)證通過(guò)后,二進(jìn)制字節(jié)流被轉(zhuǎn)換成特定數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)至方法區(qū)中,繼而開始下階段的驗(yàn)證和創(chuàng)建Class對(duì)象等操作。這個(gè)過(guò)程印證了:加載和驗(yàn)證是交叉進(jìn)行的。
(2)元數(shù)據(jù)驗(yàn)證
本階段對(duì)方法區(qū)中的字節(jié)碼描述信息進(jìn)行
網(wǎng)頁(yè)標(biāo)題:深入淺出Java虛擬機(jī)是你通往高級(jí)Java開發(fā)的必經(jīng)之路
文章路徑:http://www.rwnh.cn/article4/jjshie.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供做網(wǎng)站、服務(wù)器托管、企業(yè)建站、App設(shè)計(jì)、網(wǎng)站設(shè)計(jì)、品牌網(wǎng)站設(shè)計(jì)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)
網(wǎng)頁(yè)設(shè)計(jì)公司知識(shí)