欧美free性护士vide0shd,老熟女,一区二区三区,久久久久夜夜夜精品国产,久久久久久综合网天天,欧美成人护士h版

首頁綜合 正文
目錄

柚子快報邀請碼778899分享:java JVM高頻面試點

柚子快報邀請碼778899分享:java JVM高頻面試點

http://yzkb.51969.com/

文章目錄

JVM內(nèi)存模型程序計數(shù)器Java虛擬機(jī)棧本地方法棧Java堆方法區(qū)運行時常量池

Java對象對象的創(chuàng)建如何為對象分配內(nèi)存

對象的內(nèi)存布局對象頭實例數(shù)據(jù)對齊填充

對象的訪問定位

垃圾收集器找到垃圾引用計數(shù)法可達(dá)性分析(根搜索法)

引用概念的擴(kuò)充回收方法區(qū)垃圾收集算法分代收集理論弱分代假說(Weak Generational Hypothesis):絕大多數(shù)對象都是朝生夕滅的。強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的對象就越難以消亡。新生代和老年代跨代引用假說(Intergenerational Reference Hypothesis):存在互相引用關(guān)系的兩個對象,是應(yīng)該傾 向于同時生存或者同時消亡的。分代垃圾收集

標(biāo)記-清除算法標(biāo)記-復(fù)制算法標(biāo)記-整理算法Stop The World

經(jīng)典的垃圾收集器CMS 收集器G1 收集器

內(nèi)存分配策略對象優(yōu)先在Eden區(qū)分配大對象直接進(jìn)入老年代長期存活的對象將進(jìn)入老年代動態(tài)對象年齡判定空間分配擔(dān)保

JVM類加載機(jī)制類加載的時機(jī)類加載的過程加載驗證準(zhǔn)備解析初始化

類加載器類與類加載器雙親委派模型三層類加載器雙親委派模型的機(jī)制

JVM內(nèi)存模型

Java虛擬機(jī)在執(zhí)行java程序的過程中會把它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域

這些區(qū)域各自有各自的用途,各自有各自的特性,或稱之為運行時數(shù)據(jù)區(qū)域

線程共享的:方法區(qū),堆線程獨享的:程序計數(shù)器,虛擬機(jī)棧,本地方法棧

程序計數(shù)器

一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。

功能:記錄線程的狀態(tài),記錄指令的行號,使線程切換后能恢復(fù)到正確的位置

特點:唯一一個不會發(fā)生OOM的區(qū)域

OOM: OutOfMemory,即內(nèi)存數(shù)據(jù)溢出

Java虛擬機(jī)棧

為線程所私有,與線程的生命周期相同

**功能:**存放棧幀,每一個方法的執(zhí)行都會有一個棧幀被創(chuàng)建,用于存儲局部變量表、操作數(shù)棧、動態(tài)連接、方法出口等信息,每一個方法被調(diào)用到執(zhí)行完畢的過程,都對應(yīng)著一個棧幀在虛擬機(jī)棧中入棧到出棧的過程

局部變量表中存放了基本類型的數(shù)據(jù)和引用類型數(shù)據(jù)的引用(起始地址)

在《Java虛擬機(jī)規(guī)范》中,對這個內(nèi)存區(qū)域規(guī)定了兩類異常狀況:

如果線程請求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果Java虛擬機(jī)棧容量可以動態(tài)擴(kuò)展,當(dāng)棧擴(kuò)展時無法申請到足夠的內(nèi)存會拋出OutOfMemoryError異常,不過現(xiàn)在使用的HotSpot虛擬機(jī)是不可以動態(tài)擴(kuò)展的

本地方法棧

本地方法棧與虛擬機(jī)棧非常相似,區(qū)別只是后者為Java方法服務(wù),前者為虛擬機(jī)用到的本地方法服務(wù)。

本地方法棧也會在棧深度溢出或者棧擴(kuò)展失敗時分別拋出StackOverflowError和OutOfMemoryError異常

Java堆

Java堆是虛擬機(jī)所管理的內(nèi)存中最大的一塊,是被所有線程共享的區(qū)域

功能:存放對象實例以及數(shù)組

Java堆正是垃圾收集器管理的內(nèi)存區(qū)域,因為所謂垃圾回收(GC)回收的就是對象。

堆的工作就是為線程的對象分配空間,同時為了減少線程之間爭搶資源的情況, Java堆中可以劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)

方法區(qū)

方法區(qū)是一個線程共享的區(qū)域

功能:存儲已被虛擬機(jī)加載的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等數(shù)據(jù)。

運行時常量池

運行時常量池是方法區(qū)的一部分,.class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表,用于存放編譯期生成的各種字面量與符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。

Java對象

對象的創(chuàng)建

首先將去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程,在類加載檢查通過后,接下來虛擬機(jī)將為新生對象分配內(nèi)存。

如何為對象分配內(nèi)存

為對象分配空間的任務(wù)實際上便等同于把一塊確定大小的內(nèi)存塊從Java堆中劃分出來。

指針碰撞 假設(shè)Java堆中內(nèi)存是絕對規(guī)整的,所有被使用過的內(nèi)存都被放在一邊,空閑的內(nèi)存被放在另一邊,中間放著一個指針作為分界點的指示器,那所分配內(nèi)存就僅僅是把那個指針向空閑空間方向挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump The Pointer)。 空閑列表 但如果Java堆中的內(nèi)存并不是規(guī)整的,已被使用的內(nèi)存和空閑的內(nèi)存相互交錯在一起,那就沒有辦法簡單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個列表,記錄上哪些內(nèi)存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。

選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有空間壓縮整理(Compact)的能力決定。

對象的內(nèi)存布局

對象頭

對象頭部分包括兩類信息。

第一類是用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等,

對象頭的另外一部分是類型指針,即對象指向它的類型元數(shù)據(jù)的指針。,Java虛擬機(jī)通過這個指針來確定該對象是哪個類的實例。

此外,如果對像是一個Java數(shù)組,那在對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因為虛擬機(jī)可以通過普通 Java對象的元數(shù)據(jù)信息確定Java對象的大小,但是如果數(shù)組的長度是不確定的,將無法通過元數(shù)據(jù)中的信息推斷出數(shù)組的大小。

實例數(shù)據(jù)

對象真正存儲的有效信息,即我們在程序代碼里面所定義的各種類型的字段內(nèi)容,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄起來

對齊填充

對象的第三部分是對齊填充,這并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot虛擬機(jī)的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說就是任何對象的大小都必須是8字節(jié)的整數(shù)倍,對象頭部分已經(jīng)被精心設(shè)計成正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,如果對象實例數(shù)據(jù)部分沒有對齊的話,就需要通過對齊填充來補全。

對象的訪問定位

創(chuàng)建對象自然是為了后續(xù)使用該對象,我們的Java程序會通過棧上的reference數(shù)據(jù)來操作堆上的具體對象。

由于reference類型在《Java虛擬機(jī)規(guī)范》里面只規(guī)定了它是一個指向?qū)ο蟮囊茫]有定義這個引用應(yīng)該通過什么方式去定位、訪問到堆中對象的具體位置,所以對象訪問方式也是由虛擬機(jī)實現(xiàn)而定的。

主流的訪問方式主要有使用句柄和直接指針兩種:

如果使用句柄訪問的話,Java堆中將可能會劃分出一塊內(nèi)存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自具體的地址信息。如果使用直接指針訪問的話,Java堆中對象的內(nèi)存布局就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,reference中存儲的直接就是對象地址,如果只是訪問對象本身的話,就不需要多一次間接訪問的開銷

使用句柄的好處就是當(dāng)垃圾回收時,需要移動對象的位置,此時只需要修改句柄中指向?qū)嵗龜?shù)據(jù)的指針即可,而直接指針的訪問方式就必須在移動對象之后修改所有reference的值,因為一個對象往往不止一個引用。

使用直接指針的好處也顯而易見,就是快,比句柄訪問少一次指針定位的開銷

就HotSpot而言,它主要使用第二種方式進(jìn)行對象訪問,并且從整個軟件開發(fā)的范圍來看,在各種語言、框架中使用句柄來訪問的情況也十分常見。

垃圾收集器

垃圾收集(Garbage Collection,GC),這一板塊我們需要搞懂三個問題:

what:回收什么when:什么時候回收how:怎么回收

垃圾收集器是回收內(nèi)存中的垃圾的,那么什么是垃圾? 作為初學(xué)者,我們只需要知道這里說的垃圾,就是沒用的對象,雖然這么說不太準(zhǔn)確,但對開發(fā)者來說,知道這些就夠了,除非你要去開發(fā)虛擬機(jī)

找到垃圾

回收垃圾的第一步,找到垃圾,即沒用的對象,那么什么樣的對象是沒用的呢,我們需要借助以下兩者算法

引用計數(shù)法

在對象中添加一個引用計數(shù)器,每當(dāng)有一個地方引用它時,計數(shù)器值就加一;當(dāng)引用失效時,計數(shù)器值就減一;任何時刻計數(shù)器為零的對象就是不可能再被使用的。

優(yōu)點:簡單快速

缺點:無法解決循環(huán)引用的問題

可達(dá)性分析(根搜索法)

這個算法的基本思路就是通過一系列稱為“GC Roots”的根對象作為起始節(jié)點集,從這些節(jié)點開始,根據(jù)引用關(guān)系向下搜索過程所走過的路徑稱為“引用鏈”(Reference Chain)如果某個對象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象不可達(dá)時,則證明此對象是不可能再被使用的

在Java技術(shù)體系里面,固定可作為GC Roots的對象包括以下幾種:

在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調(diào)用的方法堆棧中使用到的 參數(shù)、局部變量、臨時變量等。在方法區(qū)中類靜態(tài)屬性引用的對象,譬如Java類的引用類型靜態(tài)變量。在方法區(qū)中常量引用的對象,譬如字符串常量池(String Table)里的引用。在本地方法棧中JNI(即通常所說的Native方法)引用的對象。Java虛擬機(jī)內(nèi)部的引用,如基本數(shù)據(jù)類型對應(yīng)的Class對象,一些常駐的異常對象(比如 NullPointExcepiton、OutOfMemoryError)等,還有系統(tǒng)類加載器。所有被同步鎖(synchronized關(guān)鍵字)持有的對象。反映Java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊的回調(diào)、本地代碼緩存等。除了這些固定的GC Roots集合以外,根據(jù)用戶所選用的垃圾收集器以及當(dāng)前回收的內(nèi)存區(qū)域不同,還可以有其他對象“臨時性”地加入,共同構(gòu)成完整GC Roots集合。

我們的HotSpot虛擬機(jī)采用的就是可達(dá)性分析算法

引用概念的擴(kuò)充

無論是通過引用計數(shù)算法判斷對象的引用數(shù)量,還是通過可達(dá)性分析算法判斷對象是否引用鏈可達(dá),判定對象是否存活都和“引用”離不開關(guān)系。

在JDK 1.2版之前,Java里面的引用是很傳統(tǒng)的定義:如果reference類型的數(shù)據(jù)中存儲的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱該reference數(shù)據(jù)是代表某塊內(nèi)存、某個對象的引用。

這種定義并沒有什么不對,只是現(xiàn)在看來有些過于狹隘了,一個對象在這種定義下只有“被引用”或者“未被引用”兩種狀態(tài),對于描述一些“食之無味,棄之可惜”的對象就顯得無能為力。譬如我們希望能描述一類對象:當(dāng)內(nèi)存空間還足夠時,能保留在內(nèi)存之中,如果內(nèi)存空間在進(jìn)行垃圾收集后仍然非常緊張,那就可以拋棄這些對象——很多系統(tǒng)的緩存功能都符合這樣的應(yīng)用場景。

在JDK 1.2版之后,Java對引用的概念進(jìn)行了擴(kuò)充,將引用分為強引用(Strongly Re-ference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

強引用是最傳統(tǒng)的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,即類似“Object obj=new Object()”這種引用關(guān)系。無論任何情況下,只要強引用關(guān)系還存在,垃圾收集器就永遠(yuǎn)不會回收掉被引用的對象。軟引用是用來描述一些還有用,但非必須的對象。只被軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常前,會把這些對象列進(jìn)回收范圍之中進(jìn)行第二次回收,如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。在JDK 1.2版之后提供了SoftReference類來實現(xiàn)軟引用。弱引用也是用來描述那些非必須對象,但是它的強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只 能生存到下一次垃圾收集發(fā)生為止。當(dāng)垃圾收集器開始工作,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只 被弱引用關(guān)聯(lián)的對象。在JDK 1.2版之后提供了WeakReference類來實現(xiàn)弱引用。虛引用也稱為“幽靈引用”或者“幻影引用”,它是最弱的一種引用關(guān)系。一個對象是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的只是為了能在這個對象被收集器回收時收到一個系統(tǒng)通知。在JDK 1.2版之后提供了PhantomReference類來實現(xiàn)虛引用

回收方法區(qū)

方法區(qū)回收囿于苛刻的判定條件,其區(qū)域垃圾收集的回收成果往往遠(yuǎn)低于此。 方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:廢棄的常量和不再使用的類型。

回收廢棄常量與回收 Java堆中的對象非常類似。舉個常量池中字面量回收的例子,假如一個字符串“java”曾經(jīng)進(jìn)入常量池中,但是當(dāng)前系統(tǒng)又沒有任何一個字符串對象的值是“java”,換句話說,已經(jīng)沒有任何字符串對象引用常量池中的“java”常量,且虛擬機(jī)中也沒有其他地方引用這個字面量。如果在這時發(fā)生內(nèi)存回收,而且垃圾收集器判斷確有必要的話,這個“java”常量就將會被系統(tǒng)清理出常量池。常量池中其他類(接口)、方法、字段的符號引用也與此類似。

判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:

該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類及其任何派生子類的實例。加載該類的類加載器已經(jīng)被回收,這個條件除非是經(jīng)過精心設(shè)計的可替換類加載器的場景,如 OSGi、JSP的重加載等,否則通常是很難達(dá)成的。該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

Java虛擬機(jī)被允許對滿足上述三個條件的無用類進(jìn)行回收,這里說的僅僅是“被允許”,而并不是和對象一樣,沒有引用了就必然會回收

垃圾收集算法

分代收集理論

弱分代假說(Weak Generational Hypothesis):絕大多數(shù)對象都是朝生夕滅的。

強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的對象就越難以消亡。

這兩個分代假說共同奠定了多款常用的垃圾收集器的一致的設(shè)計原則:

收集器應(yīng)該將Java堆劃分出不同的區(qū)域,然后將回收對象依據(jù)其年齡(年齡即對象熬過垃圾收集過程的次數(shù))分配到不同的區(qū)域之中存儲。

顯而易見,如果一個區(qū)域中大多數(shù)對象都是朝生夕滅,難以熬過垃圾收集過程的話,那么把它們集中放在一起,每次回收時只關(guān)注如何保留少量存活而不是去標(biāo)記那些大量將要被回收的對象,就能以較低代價回收到大量的空間;如果剩下的都是難以消亡的對象,那把它們集中放在一塊,虛擬機(jī)便可以使用較低的頻率來回收這個區(qū)域,這就同時兼顧了垃圾收集的時間開銷和內(nèi)存的空間有效利用。

新生代和老年代

設(shè)計者一般至少會把Java堆劃分為新生代(Young Generation)和老年代(Old Generation)兩個區(qū)域[。

顧名思義,在新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,而每次回收后存活的少量對象,將會逐步晉升到老年代中存放。

跨代引用假說(Intergenerational Reference Hypothesis):存在互相引用關(guān)系的兩個對象,是應(yīng)該傾 向于同時生存或者同時消亡的。

如果某個新生代對象存在跨代引用,由于老年代對象難以消亡,該引用會使得新生代對象在收集時同樣得以存活,進(jìn)而在年齡增長之后晉升到老年代中,這時跨代引用也隨即被消除了。

依據(jù)這條假說,我們就不應(yīng)再為了少量的跨代引用去掃描整個老年代,也不必浪費空間專門記錄每一個對象是否存在及存在哪些跨代引用,只需在新生代上建立一個全局的數(shù)據(jù)結(jié)構(gòu)(該結(jié)構(gòu)被稱為“記憶集”,Remembered Set),這個結(jié)構(gòu)把老年代劃分成若干小塊,標(biāo)識出老年代的哪一塊內(nèi)存會存在跨代引用。此后當(dāng)發(fā)生Minor GC時,只有包含了跨代引用的小塊內(nèi)存里的對象才會被加入到GC Roots進(jìn)行掃描。雖然這種方法需要在對象改變引用關(guān)系(如將自己或者某個屬性賦值)時維護(hù)記錄數(shù)據(jù)的正確性,會增加一些運行時的開銷,但比起收集時掃描整個老年代來說仍然是劃算的。

分代垃圾收集

部分收集(Partial GC):指目標(biāo)不是完整收集整個Java堆的垃圾收集,其中又分為:

新生代收集(Minor GC/Young GC):指目標(biāo)只是新生代的垃圾收集。老年代收集(Major GC/Old GC):指目標(biāo)只是老年代的垃圾收集。目前只有CMS收集器會有單獨收集老年代的行為。另外請注意“Major GC”這個說法現(xiàn)在有點混淆,在不同資料上常有不同所指,讀者需按上下文區(qū)分到底是指老年代的收集還是整堆收集。混合收集(Mixed GC):指目標(biāo)是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收 集器會有這種行為。 整堆收集(Full GC):收集整個Java堆和方法區(qū)的垃圾收集。

標(biāo)記-清除算法

先標(biāo)記需要回收的對象,再統(tǒng)一回收

標(biāo)記-復(fù)制算法

將內(nèi)存劃分成相等兩塊Eden和Survivor,一塊用完了就把存活的對象復(fù)制的另一邊,以此往復(fù),但是為了避免浪費太多空間,而且大部分對象具有朝生夕死的特性

現(xiàn)在采用Eden:Survivor=8:1:1的方式,劃分三塊,每次都將 Elden 和正在用的那塊Survivor 空間復(fù)制到另一塊空閑的Survivor,以此往復(fù)

由于無法保證每次垃圾回收后存活的對象都不超過10%,所有當(dāng)Survivor區(qū)不夠用時,可以直接將多余的存活對象直接放入老年代,稱之為老年代分配擔(dān)保機(jī)制

標(biāo)記-整理算法

回收對象后,將所有存活的對象向內(nèi)存空間的一端移動,然后直接清理邊界以外的內(nèi)存。 可以避免內(nèi)存空間過于碎片化,有利于內(nèi)存分配

Stop The World

如果采用了移動對象的垃圾收集算法,那么在移動對象時就必須暫停用于應(yīng)用程序,稱之為Stop The World

經(jīng)典的垃圾收集器

CMS 收集器

CMS (Concurrent Mark Sweep),基于標(biāo)記-清除算法,

運作過程:-初始標(biāo)記,-并發(fā)標(biāo)記,-重新標(biāo)記,-并發(fā)清除

優(yōu)點:并發(fā)收集,低停頓

缺點:對處理器資源敏感,無法處理浮動垃圾,會產(chǎn)生大量空間碎片

G1 收集器

G1 (garbage first) 收集器,面向服務(wù)端,是一款全功能垃圾收集器,開創(chuàng)基于Region的堆內(nèi)存布局。能實現(xiàn)在指定長度為M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間不超過N毫秒的功能。

運作過程:-初始標(biāo)記,-并發(fā)標(biāo)記,-最終標(biāo)記,-篩選回收

內(nèi)存分配策略

對象優(yōu)先在Eden區(qū)分配

大多數(shù)情況下,對象在新生代Eden區(qū)中分配。當(dāng)Eden區(qū)沒有足夠空間進(jìn)行分配時,虛擬機(jī)將發(fā)起一次Minor GC。

大對象直接進(jìn)入老年代

大對象就是指需要大量連續(xù)內(nèi)存空間的Java對象,最典型的大對象便是那種很長的字符串,或者元素數(shù)量很龐大的數(shù)組,大對象對虛擬機(jī)的內(nèi)存分配來說就是一個不折不扣的壞消息,比遇到一個大對象更壞的消息就是遇到一群“朝生夕滅”的“短命大對象”,我們寫程序的時候應(yīng)注意避免。

在Java虛擬機(jī)中要避免大對象的原因是,在分配空間時,它容易導(dǎo)致內(nèi)存明明還有不少空間時就提前觸發(fā)垃圾收集,以獲取足夠的連續(xù)空間才能安置好它們,而當(dāng)復(fù)制對象時,大對象就意味著高額的內(nèi)存復(fù)制開銷。

HotSpot虛擬機(jī)提供了-XX:PretenureSizeThreshold參數(shù),指定大于該設(shè)置值的對象直接在老年代分配,這樣做的目的就是避免在Eden區(qū)及兩個Survivor區(qū)之間來回復(fù)制,產(chǎn)生大量的內(nèi)存復(fù)制操作。

長期存活的對象將進(jìn)入老年代

多數(shù)收集器都采用了分代收集來管理堆內(nèi)存,那內(nèi)存回收時就必須能決策哪些存活對象應(yīng)當(dāng)放在新生代,哪些存活對象放在老年代中。

為做到這點,虛擬機(jī)給每個對象定義了一個對象年齡(Age)計數(shù)器,存儲在對象頭中。 對象通常在Eden區(qū)里誕生,如果經(jīng)過第一次Minor GC后仍然存活,并且能被Survivor容納的話,該對象會被移動到Survivor空間中,并且將其對象年齡設(shè)為1歲。對象在Survivor區(qū)中每熬過一次Minor GC,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為15),就會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數(shù)-XX: MaxTenuringThreshold設(shè)置。

動態(tài)對象年齡判定

HotSpot虛擬機(jī)并不是永遠(yuǎn)要求對象的年齡必須達(dá)到-XX:MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于 Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進(jìn)入老年代,無須等到-XX: MaxTenuringThreshold中要求的年齡。

空間分配擔(dān)保

在發(fā)生Minor GC之前,虛擬機(jī)必須先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,

如果這個條件成立,那這一次Minor GC可以確保是安全的。如果不成立,則虛擬機(jī)會先查看XX:HandlePromotionFailure參數(shù)的設(shè)置值是否允許擔(dān)保失?。℉andle Promotion Failure)

如果允許,那會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,

如果大于,將嘗試進(jìn)行一次Minor GC,盡管這次Minor GC是有風(fēng)險的;如果小于,或者-XX: HandlePromotionFailure設(shè)置不允許冒險,那這時就要改為進(jìn)行一次Full GC。

JVM類加載機(jī)制

類加載的時機(jī)

一個類型從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期將會經(jīng)歷加載(Loading)、驗證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗證、準(zhǔn)備、解析三個部分統(tǒng)稱為連接(Linking)

加載、驗證、準(zhǔn)備、初始化和卸載這五個階段的順序是確定的,類型的加載過程必須按 照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,

《Java虛擬機(jī)規(guī)范》則是嚴(yán)格規(guī)定了有且只有六種情況必須立即對類進(jìn)行“初始化”(而加載、驗證、準(zhǔn)備自然需要在此之前開始)

遇到new、getstatic、putstatic或invokestatic這四條字節(jié)碼指令時,如果類型沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化階段。能夠生成這四條指令的典型Java代碼場景有:

使用new關(guān)鍵字實例化對象的時候。讀取或設(shè)置一個類型的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候。調(diào)用一個類型的靜態(tài)方法的時候。 使用java.lang.reflect包的方法對類型進(jìn)行反射調(diào)用的時候,如果類型沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。當(dāng)初始化類的時候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。當(dāng)虛擬機(jī)啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類),虛擬機(jī)會先 初始化這個主類。當(dāng)使用JDK 7新加入的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解 析結(jié)果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種類型的方法句 柄,并且這個方法句柄對應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。當(dāng)一個接口中定義了JDK 8新加入的默認(rèn)方法(被default關(guān)鍵字修飾的接口方法)時,如果有 這個接口的實現(xiàn)類發(fā)生了初始化,那該接口要在其之前被初始化。

類加載的過程

加載

“加載”(Loading)階段是整個“類加載”(Class Loading)過程中的一個階段

在加載階段,Java虛擬機(jī)需要完成以下三件事情:

通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入 口。

驗證

驗證是連接階段的第一步,這一階段的目的是確保.class文件的字節(jié)流中包含的信息符合《Java虛擬機(jī)規(guī)范》的全部約束要求,保證這些信息被當(dāng)作代碼運行后不會危害虛擬機(jī)自身的安全。

驗證階段大致上會完成下面四個階段的檢驗動作:文件格式驗證、元數(shù)據(jù)驗證、字節(jié)碼驗證和符號引用驗證。

準(zhǔn)備

準(zhǔn)備階段是正式為類中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內(nèi)存并設(shè)置類變量初始值的階段

這時候進(jìn)行內(nèi)存分配的僅包括類變量,而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。其次是這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值。

特殊情況

public static final int value = 123;

編譯時Javac將會為value生成ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)就會根據(jù)ConstantValue的設(shè)置將value賦值為123。

解析

解析階段是Java虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程

符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標(biāo)即可。符號引用與虛擬機(jī)實現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定是已經(jīng)加載到虛擬機(jī)內(nèi)存當(dāng)中的內(nèi)容。各種虛擬機(jī)實現(xiàn)的內(nèi)存布局可以各不相同,但是它們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在《Java虛擬機(jī)規(guī)范》的*.class*文件格式中。直接引用(Direct References):直接引用是可以直接指向目標(biāo)的指針、相對偏移量或者是一個能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實現(xiàn)的內(nèi)存布局直接相關(guān)的,同一個符號引用在不同虛擬機(jī)實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在虛擬機(jī)的內(nèi)存中存在。

翻譯成人話就是程序運行之前, .class里的引用指向的都是一個邏輯上的地址,因為這個時候我們的程序還只是一張圖紙,還沒有變成實體,當(dāng)虛擬機(jī)給常量分配空間之后,我們程序里的常量就有了實體,此時,就需要讓我們的引用指向真實的地址。

解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符這7類符號引用進(jìn)行,分別對應(yīng)于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dyna-mic_info和CONSTANT_InvokeDynamic_info8種常量類型

初始化

類的初始化階段是類加載過程的最后一個步驟,進(jìn)行準(zhǔn)備階段時,變量已經(jīng)賦過一次系統(tǒng)要求的初始零值,而在初始化階段,則會根據(jù)程序員通過程序編碼制定的主觀計劃去初始化類變量和其他資源。

我們也可以從另外一種更直接的形式來表達(dá):初始化階段就是執(zhí)行類構(gòu)造器()方法的過程, ()并不是程序員在Java代碼中直接編寫的方法,它是Javac編譯器的產(chǎn)物,但我們非常有必要了解這個方法具體是如何產(chǎn)生的,以及()方法執(zhí)行過程中各種可能會影響程序運行行為的細(xì)節(jié),這部分比起其他類加載過程更貼近于普通的程序開發(fā)人員的實際工作。

()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊可以賦值,但是不能訪問,如以下代碼: public class Test {

static {

i = 0; // 給變量賦值可以正常編譯通過

System.out.print(i); // 這句編譯器會提示“非法向前引用”

}

static int i = 1;

}

()方法與類的構(gòu)造函數(shù)(即在虛擬機(jī)視角中的實例構(gòu)造器()方法)不同,它不需要顯式地調(diào)用父類構(gòu)造器,Java虛擬機(jī)會保證在子類的()方法執(zhí)行前,父類的()方法已經(jīng)執(zhí)行 完畢。因此在Java虛擬機(jī)中第一個被執(zhí)行的()方法的類型肯定是java.lang.Object。 由于父類的()方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作,如下代碼中,字段B的值將會是2而不是1。 static class Parent {

public static int A = 1;

static {

A = 2;

}

}

static class Sub extends Parent {

public static int B = A;

}

public static void main(String[] args) {

System.out.println(Sub.B);

}

()方法對于類或接口來說并不是必需的,如果一個類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生成()方法。接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成()方法。但接口與類不同的是,執(zhí)行接口的()方法不需要先執(zhí)行父接口的()方法, 因為只有當(dāng)父接口中定義的變量被使用時,父接口才會被初始化。此外,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的()方法。Java虛擬機(jī)必須保證一個類的()方法在多線程環(huán)境中被正確地加鎖同步,如果多個線程同時去初始化一個類,那么只會有其中一個線程去執(zhí)行這個類的()方法,其他線程都需要阻塞等待,直到活動線程執(zhí)行完畢()方法。如果在一個類的()方法中有耗時很長的操作,那就可能造成多個進(jìn)程阻塞,在實際應(yīng)用中這種阻塞往往是很隱蔽的。

需要注意,其他線程雖然會被阻塞,但如果執(zhí)行<clinit>()方法的那條線程退出<clinit>()方法 后,其他線程喚醒后則不會再次進(jìn)入<clinit>()方法。同一個類加載器下,一個類型只會被初始化一 次。

類加載器

Java虛擬機(jī)設(shè)計團(tuán)隊有意把類加載階段中的“通過一個類的全限定名來獲取描述該類的二進(jìn)制字節(jié)流”這個動作放到Java虛擬機(jī)外部去實現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需的類。實現(xiàn)這個動作的代碼被稱為“類加載器”(Class Loader)。

類與類加載器

類加載器雖然只用于實現(xiàn)類的加載動作,但它在Java程序中起到的作用卻遠(yuǎn)超類加載階段。對于任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在Java虛擬機(jī)中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。

這句話可以表達(dá)得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個*.class*文件,被同一個Java虛擬機(jī)加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

這里所指的“相等”,包括代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance() 方法的返回結(jié)果,也包括了使用instanceof關(guān)鍵字做對象所屬關(guān)系判定等各種情況。如果沒有注意到類加載器的影響,在某些情況下可能會產(chǎn)生具有迷惑性的結(jié)果,請看如下代碼

/**

* 類加載器與instanceof關(guān)鍵字演示

*

*/

public class ClassLoaderTest {

public static void main(String[] args) throws Exception {

ClassLoader myLoader = new ClassLoader() {

@Override

public Class loadClass(String name) throws ClassNotFoundException {

try {

String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";

InputStream is = getClass().getResourceAsStream(fileName);

if (is == null) {

return super.loadClass(name);

}

byte[] b = new byte[is.available()];

is.read(b);

return defineClass(name, b, 0, b.length);

} catch (IOException e) {

throw new ClassNotFoundException(name);

}

}

};

Object obj = myLoader.loadClass("org.fenixsoft.classloading.ClassLoaderTest").newInstance();

System.out.println(obj.getClass());

System.out.println(obj instanceof org.fenixsoft.classloading.ClassLoaderTest);

}

}

/*

運行結(jié)果:

class org.fenixsoft.classloading.ClassLoaderTest

false

*/

兩行輸出結(jié)果中,從第一行可以看到這個對象確實是類org.fenixsoft.classloading.ClassLoaderTest實例化出來的,但在第二行的輸出中卻發(fā)現(xiàn)這個對象與類org.fenixsoft.classloading.ClassLoaderTest做所屬類型檢查的時候返回了false。這是因為Java虛擬機(jī)中同時存在了兩個ClassLoaderTest類,一個是由虛擬機(jī)的應(yīng)用程序類加載器所加載的,另外一個是由我們自定義的類加載器加載的,雖然它們都來自同一個Class文件,但在Java虛擬機(jī)中仍然是兩個互相獨立的類,做對象所屬類型檢查時的結(jié)果自然為false。

雙親委派模型

站在Java虛擬機(jī)的角度來看,只存在兩種不同的類加載器:

一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(xiàn),是虛擬機(jī)自身的一部分;另外一種就是其他所有的類加載器,這些類加載器都由Java語言實現(xiàn),獨立存在于虛擬機(jī)外部,并且全都繼承自抽象類java.lang.ClassLoader。 站在Java開發(fā)人員的角度來看,類加載器就應(yīng)當(dāng)劃分得更細(xì)致一些。自JDK 1.2以來,Java一直保持著三層類加載器、雙親委派的類加載架構(gòu),盡管這套架構(gòu)在Java模塊化系統(tǒng)出現(xiàn)后有了一些調(diào)整變動,但依然未改變其主體結(jié)構(gòu),

三層類加載器

啟動類加載器(Bootstrap Class Loader):

前面已經(jīng)介紹過,這個類加載器負(fù)責(zé)加載存放在*\lib**目錄,或者被-Xbootclasspath參數(shù)所指定的路徑中存放的,而且是Java虛擬機(jī)能夠識別的類庫加載到虛擬機(jī)的內(nèi)存中(按照文件名識別,如rt.jar、tools.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)。啟動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給啟動類加載器去處理,那直接使用null代替即可,

擴(kuò)展類加載器(Extension Class Loader): 這個類加載器是在類sun.misc.Launcher$ExtClassLoader 中以Java代碼的形式實現(xiàn)的。它負(fù)責(zé)加載JAVA_HOME>\lib 目錄中,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中所有的類庫。根據(jù)“擴(kuò)展類加載器”這個名稱,就可以推斷出這是一種Java系統(tǒng)類庫的擴(kuò)展機(jī)制,JDK的開發(fā)團(tuán)隊允許用戶將具有通用性的類庫放置在ext目錄里以擴(kuò)展Java SE的功能,在JDK 9之后,這種擴(kuò)展機(jī)制被模塊化帶來的天然的擴(kuò)展能力所取代。由于擴(kuò)展類加載器是由Java代碼實現(xiàn)的,開發(fā)者可以直接在程序中使用擴(kuò)展類加載器來加載*.class*文件。 應(yīng)用程序類加載器(Application Class Loader): 這個類加載器由sun.misc.Launcher$AppClassLoader來實現(xiàn)。由于應(yīng)用程序類加載器是ClassLoader類中的getSystemClassLoader()方法的返回值,所以有些場合中也稱它為“系統(tǒng)類加載器”。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所有的類庫,開發(fā)者同樣可以直接在代碼中使用這個類加載器。如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。

雙親委派模型的機(jī)制

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到最頂層的啟動類加載器中,

只有當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去完成加載。

作用:

Java中的類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進(jìn)行加載,因此Object類在程序的各種類加載器環(huán)境中都能夠保證是同一個類。反之,如果沒有使用雙親委派模型,都由各個類加載器自行去加載的話,如果用戶自己也編寫了一個名為java.lang.Object的類,并放在程序的ClassPath中,那系統(tǒng)中就會出現(xiàn)多個不同的Object類,Java類型體系中最基礎(chǔ)的行為也就無從保證,應(yīng)用程序?qū)兊靡黄靵y。

柚子快報邀請碼778899分享:java JVM高頻面試點

http://yzkb.51969.com/

文章來源

評論可見,查看隱藏內(nèi)容

本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。

轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。

本文鏈接:http://m.gantiao.com.cn/post/19196045.html

發(fā)布評論

您暫未設(shè)置收款碼

請在主題配置——文章設(shè)置里上傳

掃描二維碼手機(jī)訪問

文章目錄