請點贊,你的點贊對我意義重大颁井,滿足下我的虛榮心厅贪。
??常在河邊走,哪有不濕鞋雅宾⊙蹋或許面試過程中你遇到的問題就在這呢?
??關(guān)注我個人簡介眉抬,面試不迷路~
一贯吓、描述JVM類加載過程
這道題想考察什么?
了解JVM是如何加載類的蜀变,并且通過JVM類加載過程能更直觀了解掌握如APT注解處理器執(zhí)行悄谐、熱修復等技術(shù)的本質(zhì)
考察的知識點
JVM類加載過程
考生如何回答
類加載的本質(zhì)
一般情況下,類的數(shù)據(jù)都是在Class
文件中库北。將描述類的數(shù)據(jù) 從Class
文件加載到內(nèi)存 同時 對數(shù)據(jù)進行校驗爬舰、轉(zhuǎn)換解析 和 初始化,最終形成可被虛擬機直接使用的Java
使用類型寒瓦。
類加載過程
java類加載過程:加載-->驗證-->準備-->解析-->初始化情屹,之后類就可以被使用了。絕大部分情況下是按這
樣的順序來完成類的加載全過程的杂腰。但是是有例外的地方垃你,解析也是可以在初始化之后進行的,這是為了支持
java的運行時綁定喂很,并且在一個階段進行過程中也可能會激活后一個階段惜颇,而不是等待一個階段結(jié)束再進行后一個階段。
1.加載
加載時jvm做了這三件事:
1)通過一個類的全限定名來獲取該類的二進制字節(jié)流
2)將這個字節(jié)流的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)運行時數(shù)據(jù)結(jié)構(gòu)
3)在內(nèi)存堆中生成一個代表該類的java.lang.Class對象少辣,作為該類數(shù)據(jù)的訪問入口
2.驗證
驗證凌摄、準備、解析這三步可以看做是一個連接的過程漓帅,將類的字節(jié)碼連接到JVM的運行狀態(tài)之中
驗證是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求望伦,不會威脅到jvm的安全
驗證主要包括以下幾個方面的驗證:
1)文件格式的驗證林说,驗證字節(jié)流是否符合Class文件的規(guī)范,是否能被當前版本的虛擬機處理
2)元數(shù)據(jù)驗證屯伞,對字節(jié)碼描述的信息進行語義分析腿箩,確保符合java語言規(guī)范
3)字節(jié)碼驗證 通過數(shù)據(jù)流和控制流分析,確定語義是合法的劣摇,符合邏輯的
4)符號引用驗證 這個校驗在解析階段發(fā)生
3.準備
為類的靜態(tài)變量分配內(nèi)存珠移,初始化為系統(tǒng)的初始值。對于final static修飾的變量末融,直接賦值為用戶的定義值钧惧。如下面的例子:這里在準備階段過后的初始值為0,而不是7:
public static int a=7
4.解析
解析是將常量池內(nèi)的符號引用轉(zhuǎn)為直接引用(如物理內(nèi)存地址指針)
5.初始化
到了初始化階段勾习,jvm才真正開始執(zhí)行類中定義的java代碼
1)初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程浓瞪。類構(gòu)造器<clinit>()方法是由編譯器自動收集
類中的所有類變量的賦值動作和靜態(tài)語句塊(static塊)中的語句合并產(chǎn)生的。
2)當初始化一個類的時候巧婶,如果發(fā)現(xiàn)其父類還沒有進行過初始化乾颁、則需要先觸發(fā)其父類的初始化。
3)虛擬機會保證一個類的<clinit>()方法在多線程環(huán)境中被正確加鎖和同步艺栈。
二英岭、請描述new一個對象的流程
這道題想考察什么?
對JVM的理解
考察的知識點
JVM 對象分配湿右、并發(fā)安全
考生應該如何回答
JVM創(chuàng)建對象的過程如下圖:
虛擬機遇到一條new指令時诅妹,首先檢查是否被類加載器加載,如果沒有毅人,那必須先執(zhí)行相應的類加載過程吭狡。類加載就是把class加載到JVM的運行時數(shù)據(jù)區(qū)的過程。
檢查加載
首先檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用丈莺,并且檢查類是否已經(jīng)被加載划煮、解析和初始化過。
符號引用:以一組符號來描述所引用的目標场刑。符號引用可以是任何形式的字面量,JAVA在編譯的時候一個每個java類都會被編譯成一個class文件蚪战,但在編譯的時候虛擬機并不知道所引用類的地址(實際地址)牵现,就用符號引用來代替,而在類的解析階段就是為了把這個符號引用轉(zhuǎn)化成為真正的地址的階段邀桑。
假設(shè)People類被編譯成一個class文件時瞎疼,如果People類引用了Tool類,但是在編譯時People類并不知道引用類的實際內(nèi)存地址壁畸,因此只能使用符號引用(org.simple.Tool)來代替贼急。而在類裝載器裝載People類時茅茂,此時可以通過虛擬機獲取Tool類的實際內(nèi)存地址,因此便可以既將符號org.simple.Tool替換為Tool類的實際內(nèi)存地址太抓。
分配內(nèi)存
完成類的加載檢查后空闲,虛擬機將為新生對象分配內(nèi)存。為對象分配空間的任務等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來走敌。
指針碰撞
如果Java堆中內(nèi)存是絕對規(guī)整的碴倾,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊掉丽,中間放著一個指針作為分界點的指示器跌榔,那所分配內(nèi)存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式稱為—指針碰撞捶障。
空閑列表
如果Java堆中的內(nèi)存并不是規(guī)整的僧须,已使用的內(nèi)存和空閑的內(nèi)存相互交錯,那就沒有辦法簡單地進行指針碰撞了项炼,虛擬機就必須維護一個列表担平,記錄上哪些內(nèi)存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例芥挣,并更新列表上的記錄驱闷,這種分配方式稱為—空閑列表。
選擇哪種分配方式由Java堆是否規(guī)整決定空免,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定空另。
- 如果是Serial、ParNew等帶有壓縮的整理的垃圾回收器的話蹋砚,系統(tǒng)采用的是指針碰撞扼菠,既簡單又高效。
- 如果是使用CMS這種不帶壓縮(整理)的垃圾回收器的話坝咐,理論上只能采用較復雜的空閑列表循榆。
并發(fā)安全
除如何劃分可用空間之外,還有另外一個需要考慮的問題是對象創(chuàng)建在虛擬機中是非常頻繁的行為墨坚,即使是僅僅修改一個指針所指向的位置秧饮,在并發(fā)情況下也并不是線程安全的,可能出現(xiàn)正在給對象A分配內(nèi)存泽篮,指針還沒來得及修改盗尸,對象B又同時使用了原來的指針來分配內(nèi)存的情況。
解決這個問題有兩種方案:
CAS
對分配內(nèi)存空間的動作進行同步處理—實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性帽撑。
分配緩沖
把內(nèi)存分配的動作按照線程劃分在不同的空間之中進行泼各,即每個線程在Java堆中預先分配一小塊私有內(nèi)存,也就是本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)亏拉。
JVM在線程初始化時扣蜻,同時也會申請一塊指定大小的內(nèi)存逆巍,只給當前線程使用,這樣每個線程都單獨擁有一個Buffer莽使,如果需要分配內(nèi)存锐极,就在自己的Buffer上分配,這樣就不存在競爭的情況吮旅,可以大大提升分配效率溪烤,當Buffer容量不夠的時候,再重新從Eden區(qū)域申請一塊繼續(xù)使用庇勃。
TLAB的目的是在為新對象分配內(nèi)存空間時檬嘀,讓每個Java應用線程能在使用自己專屬的分配指針來分配空間,減少同步開銷责嚷。
TLAB只是讓每個線程有私有的分配指針鸳兽,但底下存對象的內(nèi)存空間還是給所有線程訪問的,只是其它線程無法在這個區(qū)域分配而已罕拂。當一個TLAB用滿(分配指針top撞上分配極限end了)揍异,就新申請一個TLAB。
默認情況下啟用允許在年輕代空間中使用線程本地分配塊(TLAB)爆班。要禁用TLAB衷掷,需要指定-XX:-UseTLAB
。
內(nèi)存空間初始化
內(nèi)存分配完成后柿菩,虛擬機需要將分配到的內(nèi)存空間都初始化為零值戚嗅。這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對應的零值枢舶。(如int值為0懦胞,boolean值為false等等)。
設(shè)置
完成空間初始化后凉泄,虛擬機對對象進行必要的設(shè)置躏尉,例如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息(Java classes在Java hotspot VM內(nèi)部表示為類元數(shù)據(jù))后众、對象的哈希碼胀糜、對象的GC分代年齡等信息。這些信息存放在對象的對象頭之中蒂誉。
對象初始化
在以上工作都完成之后教藻,從虛擬機的視角來看,一個新的對象已經(jīng)產(chǎn)生了拗盒。但從Java程序的視角來看怖竭,對象創(chuàng)建才剛剛開始锥债,所有的字段都還為零值陡蝇。所以痊臭,一般來說,執(zhí)行new指令之后會接著把對象按照程序員的意愿進行初始化(構(gòu)造方法)登夫,這樣一個真正可用的對象才算完全產(chǎn)生出來广匙。
三、Java對象會不會分配到棧中恼策?
這道題想考察什么鸦致?
創(chuàng)建的對象是否都在堆中,如果不是涣楷,對照JVM運行時數(shù)據(jù)區(qū)堆棧相關(guān)內(nèi)容分唾,能夠把控對象不在堆中對程序的影響
考察的知識點
逃逸分析
考生應該如何回答
Java對象可能會分配到棧中。
逃逸分析
逃逸分析指的是分析對象動態(tài)作用域狮斗,當一個對象在方法中定義后绽乔,它可能被外部方法所引用。比如:調(diào)用參數(shù)傳遞到其他方法中碳褒,這種稱之為方法逃逸折砸。甚至還有可能被外部線程訪問到,例如:賦值給其他線程中訪問的變量沙峻,這個稱之為線程逃逸睦授。從不逃逸到方法逃逸到線程逃逸,稱之為對象由低到高的不同逃逸程度摔寨。
如果確定一個對象不會逃逸出線程之外去枷,那么讓對象在棧上分配內(nèi)存可以提高JVM的效率。如果是逃逸分析出來的對象可以在棧上分配的話祷肯,那么該對象的生命周期就跟隨線程了沉填,就不需要垃圾回收,如果是頻繁的調(diào)用此方法則可以得到很大的性能提高佑笋。
逃逸分析的觸發(fā)前提條件必須觸發(fā)JIT執(zhí)行
public class EscapeAnalysisTest{
public static void main(String[] args){
long start = System.currentTimeMillis();
for (int i = 0; i < 50000000; i++){
allocate();
}
System.out.println((System.currentTimeMillis() - start) + " ms");
}
static void allocate(){
Object obj = new Object();
}
}
在上述代碼中翼闹,Object對象屬于不可逃逸,JVM可以做棧上分配蒋纬。在啟動JVM時候猎荠,通過 -XX:-DoEscapeAnalysis
參數(shù)可以關(guān)閉逃逸分析(JVM默認開啟)。
開啟逃逸分析:
關(guān)閉逃逸分析:
測試結(jié)果可見蜀备,開啟逃逸分析對代碼的執(zhí)行性能有很大的影響关摇!
四、GC的流程是怎么樣的碾阁?介紹下GC回收機制與分代回收策略
這道題想考察什么输虱?
Java基礎(chǔ)掌握情況,掌握對象回收過程以避免開發(fā)時出現(xiàn)內(nèi)存問題
考察的知識點
GC機制
考生如何回答
說到GC垃圾回收脂凶,首先要知道什么是“垃圾”宪睹,垃圾就是沒有用的對象愁茁,那么怎樣判定一個對象是不是垃圾(能不能被回收)?Java 虛擬機中使用一種叫作可達性分析的算法來決定對象是否可以被回收亭病。
可達性分析
可達性分析就通過一組名為”GC Root"的對象作為起始點鹅很,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈罪帖,最后通過判斷對象的引用鏈是否可達來決定對象是否可以被回收促煮。
GC Root指的是:
- Java 虛擬機棧(局部變量表)中的引用的對象。也就是正在運行的方法中的局部變量所引用的對象
- 方法區(qū)中靜態(tài)引用指向的對象整袁。也就是類中的static修飾的變量所引用的對象
- 方法區(qū)中常量引用的對象菠齿。
- 仍處于存活狀態(tài)中的線程對象。
- Native 方法中 JNI 引用的對象坐昙。
優(yōu)點
可達性分析可以解決引用計數(shù)器所不能解決的循環(huán)引用問題泞当。即便對象a和b相互引用,只要從GC Roots出發(fā)無法到達a或者b民珍,那么可達性分析便不會將它們加入存活對象合集之中襟士。
缺點
在多線程環(huán)境下,其他線程可能會更新已經(jīng)訪問過的對象中的引用嚷量,從而造成誤報(將引用設(shè)置為null)或者漏報(將引用設(shè)置為未被訪問過的對象)陋桂。誤報并沒有什么傷害,Java虛擬機至多損失了部分垃圾回收的機會蝶溶。漏報則比較麻煩嗜历,因為垃圾回收器可能回收事實上仍被引用的對象內(nèi)存。 一旦從原引用訪問已經(jīng)被回收了的對象抖所,則很有可能會直接導致Java虛擬機崩潰梨州。
垃圾回收算法
在標記出對象是否可被回收后,接下來就需要對可回收對象進行回收田轧”┙常基本的回收算法有:標記-清理、標記-整理與復制算法傻粘。
標記清除算法
從”GC Roots”集合開始每窖,將內(nèi)存整個遍歷一次,保留所有可以被 GC Roots 直接或間接引用到的對象弦悉,而剩下的對象都當作垃圾對待并回收窒典,過程分為 標記 和 清除 兩個步驟。
- 優(yōu)點:實現(xiàn)簡單稽莉,不需要將對象進行移動瀑志。
- 缺點:這個算法需要中斷進程內(nèi)其他組件的執(zhí)行(stop the world),并且可能產(chǎn)生內(nèi)存碎片,提高了垃圾回收的頻率劈猪。
標記整理算法
與標記-清除不同的是它并不簡單地清理未標記的對象缩膝,而是將所有的存活對象壓縮到內(nèi)存的一端。最后岸霹,清理邊界外所有的空間。
- 優(yōu)點:這種方法既避免了碎片的產(chǎn)生将饺,又不需要兩塊相同的內(nèi)存空間贡避,因此,其性價比比較高予弧。
- 缺點:所謂壓縮操作刮吧,仍需要進行局部對象移動,所以一定程度上還是降低了效率掖蛤。
復制算法
將現(xiàn)有的內(nèi)存空間分為兩快杀捻,每次只使用其中一塊,在垃圾回收時將正在使用的內(nèi)存中的存活對象復制到未被使用的內(nèi)存塊中蚓庭。之后致讥,清除正在使用的內(nèi)存塊中的所有對象,交換兩個內(nèi)存的角色器赞,完成垃圾回收垢袱。
- 優(yōu)點:按順序分配內(nèi)存即可,實現(xiàn)簡單港柜、運行高效请契,不用考慮內(nèi)存碎片。
- 缺點:可用的內(nèi)存大小縮小為原來的一半夏醉,對象存活率高時會頻繁進行復制爽锥。
分代回收策略
不同的垃圾收集器實現(xiàn)采用不同的算法進行垃圾回收,除此之外現(xiàn)代虛擬機還會采用分代機制來進行垃圾回收畔柔,根據(jù)對象存活的周期不同氯夷,把堆內(nèi)存劃分為不同區(qū)域,不同區(qū)域采用不同算法進行垃圾回收靶擦。
分代的垃圾回收策略肠槽,是基于這樣一個事實:不同的對象的生命周期是不一樣的。因此奢啥,不同生命周期的對象可以采取不同的收集方式秸仙,以便提高回收效率。
在Java程序運行的過程中桩盲,會產(chǎn)生大量的對象寂纪,其中有些對象是與業(yè)務信息相關(guān),比如Http請求中的Session對象、線程捞蛋、Socket連接孝冒,這類對象跟業(yè)務直接掛鉤,因此生命周期比較長拟杉。但是還有一些對象庄涡,主要是程序運行過程中生成的臨時變量,這些對象生命周期會比較短搬设,比如:String對象穴店,由于其不變類的特性,系統(tǒng)會產(chǎn)生大量的這些對象拿穴,有些對象甚至只用一次即可回收泣洞。
試想,在不進行對象存活時間區(qū)分的情況下默色,每次垃圾回收都是對整個堆空間進行回收球凰,花費時間相對會長,同時腿宰,因為每次回收都需要遍歷所有存活對象呕诉,但實際上,對于生命周期長的對象而言吃度,這種遍歷是沒有效果的义钉,因為可能進行了很多次遍歷,但是他們依舊存在规肴。因此捶闸,分代垃圾回收采用分治的思想,進行代的劃分拖刃,把不同生命周期的對象放在不同代上删壮,不同代上采用最適合它的垃圾回收方式進行回收。
代際劃分
堆內(nèi)存分為年輕代(Young Generation)和老年代(Old Generation)兑牡。而持久代使用非堆內(nèi)存央碟,主要用于存儲一些類的元數(shù)據(jù),常量池均函,java類亿虽,靜態(tài)文件等信息。
垃圾回收
年輕代會劃分出Eden區(qū)域與兩個大小對等的Survivor區(qū)域苞也。 其比例一般為8:1:1洛勉,這是因為根據(jù)統(tǒng)計95%的對象朝生夕死,存活時間極短如迟。
- 新生成的對象優(yōu)先存放在新生代中
- 存活率很低收毫,回收效率很高
- 一般采用的 GC 回收算法是復制算法
當新對象生成攻走,并且在Eden申請空間失敗時,就會觸發(fā)GC此再,對Eden區(qū)域進行GC昔搂,清除非存活對象,并且把尚且存活的對象移動到Survivor區(qū)输拇,然后整理Survivor的兩個區(qū)摘符。這種方式的GC是對年輕代的Eden區(qū)進行宠进,不會影響到年老代媚狰。因為大部分對象都是從Eden區(qū)開始的暮蹂,同時Eden區(qū)不會分配的很大奏窑,所以Eden區(qū)的GC會頻繁進行。所以一般在這里需要使用速度快奶甘、效率高的算法,使Eden區(qū)能盡快空閑出來。
minor gc
新對象的內(nèi)存分配都是先在Eden區(qū)域中進行的洽糟,當Eden區(qū)域的空間不足于分配新對象時,就會觸發(fā)年輕代上的垃圾回收堕战,我們稱之為"minor gc"坤溃。同時,每個對象都有一個“年齡”嘱丢,這個年齡實際上指的就是該對象經(jīng)歷過的minor gc的次數(shù)薪介。如圖1所示,當對象剛分配到Eden區(qū)域時越驻,對象的年齡為“0”汁政,當minor gc被觸發(fā)后,所有存活的對象(仍然可達對象)會被拷貝到其中一個Survivor區(qū)域缀旁,同時年齡增長為“1”记劈。并清除整個Eden內(nèi)存區(qū)域中的非可達對象。
當?shù)诙蝝inor gc被觸發(fā)時并巍,JVM會通過Mark算法找出所有在Eden內(nèi)存區(qū)域和Survivor1內(nèi)存區(qū)域存活的對象目木,并將他們拷貝到新的Survivor2內(nèi)存區(qū)域(這也就是為什么需要兩個大小一樣的Survivor區(qū)域的原因),同時對象的年齡加1. 最后懊渡,清除所有在Eden內(nèi)存區(qū)域和Survivor1內(nèi)存區(qū)域的非可達對象刽射。
當對象的年齡足夠大(年齡可以通過JVM參數(shù)進行指定,默認為15歲剃执,CMS收集器默認6歲誓禁,不同的垃圾收集器會略微有點不同 ),當minor gc再次發(fā)生時肾档,它會從Survivor內(nèi)存區(qū)域中升級到年老代中现横,如圖3所示。
major gc
當minor gc發(fā)生時,又有對象從Survivor區(qū)域升級到Tenured區(qū)域戒祠,但是Tenured區(qū)域已經(jīng)沒有空間容納新的對象了骇两,那么這個時候就會觸發(fā)年老代上的垃圾回收,我們稱之為"major gc"姜盈。而在年老代上選擇的垃圾回收算法則取決于JVM上采用的是什么垃圾回收器低千。
總結(jié)
在JVM中一般采用可達性分析法進行是否可回收的判定,確定對象需要被回收后馏颂,對象在哪個代際將會采用不同的垃圾回收算法進行回收示血,這些算法包括:標記-清除,標記-整理與復制算法救拉。
而之所以采用分代策略的原因是:不同的對象的生命周期是不一樣的难审。因此,不同生命周期的對象可以采取不同的收集方式亿絮,以便提高回收效率告喊。 如果每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長派昧,而對于生命周期長的對象而言黔姜,這種遍歷是沒有效果的,因為可能進行了很多次遍歷蒂萎,但是他們依舊存在秆吵。
今天的面試分享到此結(jié)束拉~下期在見