天天看點

面試必問之JVM原理

1:什麼是JVM

JVM是Java Virtual Machine(Java虛拟機)的縮寫,JVM是一種用于計算裝置的規範,它是一個虛構出來的計算機,是通過在實際的計算機上仿真模拟各種計算機功能來實作的。Java虛拟機包括一套位元組碼指令集、一組寄存器、一個棧、一個垃圾回收堆和一個存儲方法域。 JVM屏蔽了與具體作業系統平台相關的資訊,使Java程式隻需生成在Java虛拟機上運作的目标代碼(位元組碼),就可以在多種平台上不加修改地運作。JVM在執行位元組碼時,實際上最終還是把位元組碼解釋成具體平台上的機器指令執行。

2:JRE/JDK/JVM是什麼關系

JRE(JavaRuntimeEnvironment,Java運作環境),也就是Java平台。所有的Java 程式都要在JRE下才能運作。普通使用者隻需要運作已開發好的java程式,安裝JRE即可。

JDK(Java Development Kit)是程式開發者用來來編譯、調試java程式用的開發工具包。JDK的工具也是Java程式,也需要JRE才能運作。為了保持JDK的獨立性和完整性,在JDK的安裝過程中,JRE也是 安裝的一部分。是以,在JDK的安裝目錄下有一個名為jre的目錄,用于存放JRE檔案。

JVM(JavaVirtualMachine,Java虛拟機)是JRE的一部分。它是一個虛構出來的計算機,是通過在實際的計算機上仿真模拟各種計算機功能來實作的。JVM有自己完善的硬體架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。Java語言最重要的特點就是跨平台運作。使用JVM就是為了支援與作業系統無關,實作跨平台。

3:JVM原理

JVM是java的核心和基礎,在java編譯器和os平台之間的虛拟處理器。它是一種利用軟體方法實作的抽象的計算機基于下層的作業系統和硬體平台,可以在上面執行java的位元組碼程式。

java編譯器隻要面向JVM,生成JVM能了解的代碼或位元組碼檔案。Java源檔案經編譯成位元組碼程式,通過JVM将每一條指令翻譯成不同平台機器碼,通過特定平台運作。

4:JVM的體系結構

類裝載器(ClassLoader)(用來裝載.class檔案)

執行引擎(執行位元組碼,或者執行本地方法)

運作時資料區(方法區、堆、java棧、PC寄存器、本地方法棧)

5:JVM運作時資料區

第一塊:PC寄存器

PC寄存器是用于存儲每個線程下一步将執行的JVM指令,如該方法為native的,則PC寄存器中不存儲任何資訊。

第二塊:JVM棧

JVM棧是線程私有的,每個線程建立的同時都會建立JVM棧,JVM棧中存放的為目前線程中局部基本類型的變量(java中定義的八種基本類型:boolean、char、byte、short、int、long、float、double)、部分的傳回結果以及Stack Frame,非基本類型的對象在JVM棧上僅存放一個指向堆上的位址。

第三塊:堆(Heap)

它是JVM用來存儲對象執行個體以及數組值的區域,可以認為Java中所有通過new建立的對象的記憶體都在此配置設定,Heap中的對象的記憶體需要等待GC進行回收。

(1) 堆是JVM中所有線程共享的,是以在其上進行對象記憶體的配置設定均需要進行加鎖,這也導緻了new對象的開銷是比較大的

(2) Sun Hotspot JVM為了提升對象記憶體配置設定的效率,對于所建立的線程都會配置設定一塊獨立的空間TLAB(Thread Local Allocation Buffer),其大小由JVM根據運作的情況計算而得,在TLAB上配置設定對象時不需要加鎖,是以JVM在給線程的對象配置設定記憶體時會盡量的在TLAB上配置設定,在這種情況下JVM中配置設定對象記憶體的性能和C基本是一樣高效的,但如果對象過大的話則仍然是直接使用堆空間配置設定

(3) TLAB僅作用于新生代的Eden Space,是以在編寫Java程式時,通常多個小的對象比大的對象配置設定起來更加高效。

(4) 所有新建立的Object 都将會存儲在新生代Yong Generation中。如果Young Generation的資料在一次或多次GC後存活下來,那麼将被轉移到OldGeneration。新的Object總是建立在Eden Space。

第四塊:方法區域(Method Area)

(1)在Sun JDK中這塊區域對應的為PermanetGeneration,又稱為持久代。

(2)方法區域存放了所加載的類的資訊(名稱、修飾符等)、類中的靜态變量、類中定義為final類型的常量、類中的Field資訊、類中的方法資訊,當開發人員在程式中通過Class對象中的getName、isInterface等方法來擷取資訊時,這些資料都來源于方法區域,同時方法區域也是全局共享的,在一定的條件下它也會被GC,當方法區域需要使用的記憶體超過其允許的大小時,會抛出OutOfMemory的錯誤資訊。

第五塊:運作時常量池(Runtime Constant Pool)

存放的為類中的固定的常量資訊、方法和Field的引用資訊等,其空間從方法區域中配置設定。

第六塊:本地方法堆棧(Native Method Stacks)

JVM采用本地方法堆棧來支援native方法的執行,此區域用于存儲每個native方法調用的狀态。

6:對象“已死”的判定算法

由于程式計數器、Java虛拟機棧、本地方法棧都是線程獨享,其占用的記憶體也是随線程生而生、随線程結束而回收。而Java堆和方法區則不同,線程共享,是GC的所關注的部分。

在堆中幾乎存在着所有對象,GC之前需要考慮哪些對象還活着不能回收,哪些對象已經死去可以回收。

有兩種算法可以判定對象是否存活:

1.)引用計數算法:給對象中添加一個引用計數器,每當一個地方應用了對象,計數器加1;當引用失效,計數器減1;當計數器為0表示該對象已死、可回收。但是它很難解決兩個對象之間互相循環引用的情況。

2.)可達性分析算法:通過一系列稱為“GC Roots”的對象作為起點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊,當一個對象到GC Roots沒有任何引用鍊相連(即對象到GC Roots不可達),則證明此對象已死、可回收。Java中可以作為GC Roots的對象包括:虛拟機棧中引用的對象、本地方法棧中Native方法引用的對象、方法區靜态屬性引用的對象、方法區常量引用的對象。

在主流的商用程式語言(如我們的Java)的主流實作中,都是通過可達性分析算法來判定對象是否存活的。

7:JVM垃圾回收

GC (Garbage Collection)的基本原理:将記憶體中不再被使用的對象進行回收,GC中用于回收的方法稱為收集器,由于GC需要消耗一些資源和時間,Java在對對象的生命周期特征進行分析後,按照新生代、舊生代的方式來對對象進行收集,以盡可能的縮短GC對應用造成的暫停

(1)對新生代的對象的收集稱為minor GC;

(2)對舊生代的對象的收集稱為Full GC;

(3)程式中主動調用System.gc()強制執行的GC為Full GC。

不同的對象引用類型, GC會采用不同的方法進行回收,JVM對象的引用分為了四種類型:

(1)強引用:預設情況下,對象采用的均為強引用(這個對象的執行個體沒有其他對象引用,GC時才會被回收)

(2)軟引用:軟引用是Java中提供的一種比較适合于緩存場景的應用(隻有在記憶體不夠用的情況下才會被GC)

(3)弱引用:在GC時一定會被GC回收

(4)虛引用:由于虛引用隻是用來得知對象是否被GC

8:垃圾收集算法

1、标記-清除算法

最基礎的算法,分标記和清除兩個階段:首先标記處所需要回收的對象,在标記完成後統一回收所有被标記的對象。

它有兩點不足:一個效率問題,标記和清除過程都效率不高;一個是空間問題,标記清除之後會産生大量不連續的記憶體碎片(類似于我們電腦的磁盤碎片),空間碎片太多導緻需要配置設定大對象時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾回收動作。

2、複制算法

為了解決效率問題,出現了“複制”算法,他将可用記憶體按容量劃分為大小相等的兩塊,每次隻需要使用其中一塊。當一塊記憶體用完了,将還存活的對象複制到另一塊上面,然後再把剛剛用完的記憶體空間一次清理掉。這樣就解決了記憶體碎片問題,但是代價就是可以用内容就縮小為原來的一半。

3、标記-整理算法

複制算法在對象存活率較高時就會進行頻繁的複制操作,效率将降低。是以又有了标記-整理算法,标記過程同标記-清除算法,但是在後續步驟不是直接對對象進行清理,而是讓所有存活的對象都向一側移動,然後直接清理掉端邊界以外的記憶體。

4、分代收集算法

目前商業虛拟機的GC都是采用分代收集算法,這種算法并沒有什麼新的思想,而是根據對象存活周期的不同将堆分為:新生代和老年代,方法區稱為永久代(在新的版本中已經将永久代廢棄,引入了元空間的概念,永久代使用的是JVM記憶體而元空間直接使用實體記憶體)。

這樣就可以根據各個年代的特點采用不同的收集算法。

新生代中的對象“朝生夕死”,每次GC時都會有大量對象死去,少量存活,使用複制算法。新生代又分為Eden區和Survivor區(Survivor from、Survivor to),大小比例預設為8:1:1。

老年代中的對象因為對象存活率高、沒有額外空間進行配置設定擔保,就使用标記-清除或标記-整理算法。

新産生的對象優先進去Eden區,當Eden區滿了之後再使用Survivor from,當Survivor from 也滿了之後就進行Minor GC(新生代GC),将Eden和Survivor from中存活的對象copy進入Survivor to,然後清空Eden和Survivor from,這個時候原來的Survivor from成了新的Survivor to,原來的Survivor to成了新的Survivor from。複制的時候,如果Survivor to 無法容納全部存活的對象,則根據老年代的配置設定擔保(類似于銀行的貸款擔保)将對象copy進去老年代,如果老年代也無法容納,則進行Full GC(老年代GC)。

大對象直接進入老年代:JVM中有個參數配置-XX:PretenureSizeThreshold,令大于這個設定值的對象直接進入老年代,目的是為了避免在Eden和Survivor區之間發生大量的記憶體複制。

長期存活的對象進入老年代:JVM給每個對象定義一個對象年齡計數器,如果對象在Eden出生并經過第一次Minor GC後仍然存活,并且能被Survivor容納,将被移入Survivor并且年齡設定為1。沒熬過一次Minor GC,年齡就加1,當他的年齡到一定程度(預設為15歲,可以通過XX:MaxTenuringThreshold來設定),就會移入老年代。但是JVM并不是永遠要求年齡必須達到最大年齡才會晉升老年代,如果Survivor 空間中相同年齡(如年齡為x)所有對象大小的總和大于Survivor的一半,年齡大于等于x的所有對象直接進入老年代,無需等到最大年齡要求。

9:垃圾收集器

垃圾收集算法是方法論,垃圾收集器是具體實作。JVM規範對于垃圾收集器的應該如何實作沒有任何規定,是以不同的廠商、不同版本的虛拟機所提供的垃圾收集器差别較大,這裡隻看HotSpot虛拟機。

JDK7/8後,HotSpot虛拟機所有收集器及組合(連線)如下:

1.Serial收集器

Serial收集器是最基本、曆史最久的收集器,曾是新生代手機的唯一選擇。他是單線程的,隻會使用一個CPU或一條收集線程去完成垃圾收集工作,并且它在收集的時候,必須暫停其他所有的工作線程,直到它結束,即“Stop the World”。停掉所有的使用者線程,對很多應用來說難以接受。比如你在做一件事情,被别人強制停掉,你心裡奔騰而過的“羊駝”還數的過來嗎?

盡管如此,它仍然是虛拟機運作在client模式下的預設新生代收集器:簡單而高效(與其他收集器的單個線程相比,因為沒有線程切換的開銷等)。

工作示意圖:

2.ParNew收集器

ParNew收集器是Serial收集器的多線程版本,除了使用了多線程之外,其他的行為(收集算法、stop the world、對象配置設定規則、回收政策等)同Serial收集器一樣。

是許多運作在Server模式下的JVM中首選的新生代收集器,其中一個很重還要的原因就是除了Serial之外,隻有他能和老年代的CMS收集器配合工作。

3.Parallel Scavenge收集器

新生代收集器,并行的多線程收集器。它的目标是達到一個可控的吞吐量(就是CPU運作使用者代碼的時間與CPU總消耗時間的比值,即 吞吐量=行使用者代碼的時間/[行使用者代碼的時間+垃圾收集時間]),這樣可以高效率的利用CPU時間,盡快完成程式的運算任務,适合在背景運算而不需要太多互動的任務。

4.Serial Old收集器

Serial 收集器的老年代版本,單線程,“标記整理”算法,主要是給Client模式下的虛拟機使用。

另外還可以在Server模式下:

JDK 1.5之前的版本中雨Parallel Scavenge 收集器搭配使用

可以作為CMS的後背方案,在CMS發生Concurrent Mode Failure是使用

5.Parallel Old收集器

Parallel Scavenge的老年代版本,多線程,“标記整理”算法,JDK 1.6才出現。在此之前Parallel Scavenge隻能同Serial Old搭配使用,由于Serial Old的性能較差導緻Parallel Scavenge的優勢發揮不出來,尴了個尬~~

Parallel Old收集器的出現,使“吞吐量優先”收集器終于有了名副其實的組合。在吞吐量和CPU敏感的場合,都可以使用Parallel Scavenge/Parallel Old組合。組合的工作示意圖如下:

6.CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的收集器,停頓時間短,使用者體驗就好。

基于“标記清除”算法,并發收集、低停頓,運作過程複雜,分4步:

1)初始标記:僅僅标記GC Roots能直接關聯到的對象,速度快,但是需要“Stop The World”

2)并發标記:就是進行追蹤引用鍊的過程,可以和使用者線程并發執行。

3)重新标記:修正并發标記階段因使用者線程繼續運作而導緻标記發生變化的那部分對象的标記記錄,比初始标記時間長但遠比并發标記時間短,需要“Stop The World”

4)并發清除:清除标記為可以回收對象,可以和使用者線程并發執行

由于整個過程耗時最長的并發标記和并發清除都可以和使用者線程一起工作,是以總體上來看,CMS收集器的記憶體回收過程和使用者線程是并發執行的。

CSM收集器有3個缺點:

1)對CPU資源非常敏感

并發收集雖然不會暫停使用者線程,但因為占用一部分CPU資源,還是會導緻應用程式變慢,總吞吐量降低。

CMS的預設收集線程數量是=(CPU數量+3)/4;當CPU數量多于4個,收集線程占用的CPU資源多于25%,對使用者程式影響可能較大;不足4個時,影響更大,可能無法接受。

2)無法處理浮動垃圾(在并發清除時,使用者線程新産生的垃圾叫浮動垃圾),可能出現"Concurrent Mode Failure"失敗。

并發清除時需要預留一定的記憶體空間,不能像其他收集器在老年代幾乎填滿再進行收集;如果CMS預留記憶體空間無法滿足程式需要,就會出現一次"Concurrent Mode Failure"失敗;這時JVM啟用後備預案:臨時啟用Serail Old收集器,而導緻另一次Full GC的産生;

3)産生大量記憶體碎片:CMS基于"标記-清除"算法,清除後不進行壓縮操作産生大量不連續的記憶體碎片,這樣會導緻配置設定大記憶體對象時,無法找到足夠的連續記憶體,進而需要提前觸發另一次Full GC動作。

7.G1收集器

G1(Garbage-First)是JDK7-u4才正式推出商用的收集器。G1是面向服務端應用的垃圾收集器。它的使命是未來可以替換掉CMS收集器。

G1收集器特性:

并行與并發:能充分利用多CPU、多核環境的硬體優勢,縮短停頓時間;能和使用者線程并發執行。

分代收集:G1可以不需要其他GC收集器的配合就能獨立管理整個堆,采用不同的方式處理新生對象和已經存活一段時間的對象。

空間整合:整體上看采用标記整理算法,局部看采用複制算法(兩個Region之間),不會有記憶體碎片,不會因為大對象找不到足夠的連續空間而提前觸發GC,這點優于CMS收集器。

可預測的停頓:除了追求低停頓還能建立可以預測的停頓時間模型,能讓使用者明确指定在一個長度為M毫秒的時間片段内,消耗在垃圾收集上的時間不超N毫秒,這點優于CMS收集器。

為什麼能做到可預測的停頓?

是因為可以有計劃的避免在整個Java堆中進行全區域的垃圾收集。

G1收集器将記憶體分大小相等的獨立區域(Region),新生代和老年代概念保留,但是已經不再實體隔離。

G1跟蹤各個Region獲得其收集價值大小,在背景維護一個優先清單;

每次根據允許的收集時間,優先回收價值最大的Region(名稱Garbage-First的由來);

這就保證了在有限的時間内可以擷取盡可能高的收集效率。

對象被其他Region的對象引用了怎麼辦?

判斷對象存活時,是否需要掃描整個Java堆才能保證準确?在其他的分代收集器,也存在這樣的問題(而G1更突出):新生代回收的時候不得不掃描老年代?

無論G1還是其他分代收集器,JVM都是使用Remembered Set來避免全局掃描:

每個Region都有一個對應的Remembered Set;

每次Reference類型資料寫操作時,都會産生一個Write Barrier 暫時中斷操作;

然後檢查将要寫入的引用指向的對象是否和該Reference類型資料在不同的 Region(其他收集器:檢查老年代對象是否引用了新生代對象);

如果不同,通過CardTable把相關引用資訊記錄到引用指向對象的所在Region對應的Remembered Set中;

進行垃圾收集時,在GC根節點的枚舉範圍加入 Remembered Set ,就可以保證不進行全局掃描,也不會有遺漏。

不計算維護Remembered Set的操作,回收過程可以分為4個步驟(與CMS較為相似):

1)初始标記:僅僅标記GC Roots能直接關聯到的對象,并修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式并發運作時能在正确可用的Region中建立新對象,需要“Stop The World”

2)并發标記:從GC Roots開始進行可達性分析,找出存活對象,耗時長,可與使用者線程并發執行

3)最終标記:修正并發标記階段因使用者線程繼續運作而導緻标記發生變化的那部分對象的标記記錄。并發标記時虛拟機将對象變化記錄線上程Remember Set Logs裡面,最終标記階段将Remember Set Logs整合到Remember Set中,比初始标記時間長但遠比并發标記時間短,需要“Stop The World”

4)篩選回收:首先對各個Region的回收價值和成本進行排序,然後根據使用者期望的GC停頓時間來定制回收計劃,最後按計劃回收一些價值高的Region中垃圾對象。回收時采用複制算法,從一個或多個Region複制存活對象到堆上的另一個空的Region,并且在此過程中壓縮和釋放記憶體;可以并發進行,降低停頓時間,并增加吞吐量。

10:基本結構

從Java平台的邏輯結構上來看,我們可以從下圖來了解JVM:

從上圖能清晰看到Java平台包含的各個邏輯子產品,也能了解到JDK與JRE的差別。

歡迎工作一到五年的Java工程師朋友們加入Java架構開發:860113481

群内提供免費的Java架構學習資料(裡面有高可用、高并發、高性能及分布式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!