您好,登錄后才能下訂單哦!
這篇文章主要講解了“Java虛擬機常見問題有哪些”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Java虛擬機常見問題有哪些”吧!
1.介紹下Java內存區域(運行時數據區)。
程序計數器(Program Counter Register)
Java虛擬機棧(Java Virtual Machine Stacks)
本地方法棧(Native Method Stack)
Java堆(Java Heap)
方法區(Method Area)
運行時常量池(Runtime Constant Pool)
2.怎么判定對象已經“死去”?
引用計數法
可達性分析算法
3.介紹下四種引用(強引用、軟引用、弱引用、虛引用)?
4.垃圾收集有哪些算法,各自的特點? 標記 - 清除算法
復制算法
標記 - 整理算法
對什么?
做了什么?
5.HotSpot為什么要分為新生代和老年代?
6.新生代中Eden區和Survivor區的默認比例?
7.HotSpot GC的分類?
8.HotSpot GC的觸發條件?
9.Full GC后老年代的空間反而變小?
10.什么情況下新生代對象會晉升到老年代?
11.介紹下垃圾收集機制(在什么時候,對什么,做了什么)? 在什么時候?
12.GC Root有哪些?
13.發生Young GC的時候需要掃描老年代的對象嗎?
14.垃圾收集器有哪些?
15.介紹CMS垃圾收集器的特點?
16.介紹下G1垃圾收集器的特點?(較復雜,可以考慮跳過)
17.類加載的過程。
加載:
驗證:
準備:
解析:
初始化:
18.Java虛擬機中有哪些類加載器?
啟動類加載器(Bootstrap ClassLoader):
擴展類加載器(Extension ClassLoader):
應用程序類加載器(Application ClassLoader):
19.什么是雙親委派模型?
20.使用雙親委派模型的好處?
總結
Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分為以下6個運行時數據區域。
一塊較小的內存空間,可以看作當前線程所執行的字節碼的行號指示器。如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Native方法,這個計數器值則為空。
與程序計數器一樣,Java虛擬機棧也是線程私有的,它的生命周期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。
本地方法棧與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。
對大多數應用來說,Java堆是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。
與Java堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。方法區是JVM規范中定義的一個概念,具體放在哪里,不同的實現可以放在不同的地方。
運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。
常見的判定方法有兩種:引用計數法和可達性分析算法,HotSpot中采用的是可達性分析算法。
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。
客觀地說,引用計數算法的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的算法,但是主流的Java虛擬機里面沒有選用引用計數算法來管理內存,其中最主要的原因是它很難解決對象之間相互循環引用的問題。
這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如下圖所示,對象object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的對象。
強引用:在程序代碼之中普遍存在的,類似“Object obj=new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
軟引用:用來描述一些還有用但并非必需的對象,使用SoftReference類來實現軟引用,在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。
弱引用:用來描述非必需對象的,使用WeakReference類來實現弱引用,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。
虛引用:是最弱的一種引用關系,使用PhantomReference類來實現虛引用,一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。
首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
為了解決效率問題,一種稱為“復制”(Copying)的收集算法出現了,它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小為了原來的一半,未免太高了一點。
復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
HotSpot根據對象存活周期的不同將內存劃分為幾塊,一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。
其中新生代又分為1個Eden區和2個Survivor區,通常稱為From Survivor和To Survivor區。
在HotSpot虛擬機中,Eden區和Survivor區的默認比例為8:1:1,即-XX:SurvivorRatio=8,其中Survivor分為From Survivor和ToSurvivor,因此Eden此時占新生代空間的80%。
針對HotSpot VM的實現,它里面的GC其實準確分類只有兩大種:
Partial GC:并不收集整個GC堆的模式,具體如下:
Young GC/Minor GC:只收集新生代的GC。
Old GC:只收集老年代的GC。只有CMS的concurrent collection是這個模式。
Mixed GC:收集整個新生代以及部分老年代的GC,只有G1有這個模式。
Full GC/Major GC:收集整個GC堆的模式,包括新生代、老年代、永久代(如果存在的話)等所有部分的模式。
這里只說常見的Young GC和Full GC。
Young GC:當新生代中的Eden區沒有足夠空間進行分配時會觸發Young GC。
Full GC:
當準備要觸發一次Young GC時,如果發現統計數據說之前Young GC的平均晉升大小比目前老年代剩余的空間大,則不會觸發Young GC而是轉為觸發Full GC。(通常情況)
如果有永久代的話,在永久代需要分配空間但已經沒有足夠空間時,也要觸發一次Full GC。
System.gc()默認也是觸發Full GC。
heap dump帶GC默認也是觸發Full GC。
CMS GC時出現Concurrent Mode Failure會導致一次Full GC的產生。
HotSpot的Full GC實現中,默認新生代里所有活的對象都要晉升到老年代,實在晉升不了才會留在新生代。假如做Full GC的時候,老年代里的對象幾乎沒有死掉的,而新生代又要晉升活對象上來,那么Full GC結束后老年代的使用量自然就上升了。
如果新生代的垃圾收集器為Serial和ParNew,并且設置了-XX:PretenureSizeThreshold參數,當對象大于這個參數值時,會被認為是大對象,直接進入老年代。
Young GC后,如果對象太大無法進入Survivor區,則會通過分配擔保機制進入老年代。
對象每在Survivor區中“熬過”一次Young GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲,可以通過-XX:MaxTenuringThreshold設置),就將會被晉升到老年代中。
如果在Survivor區中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
在觸發GC的時候,具體如下,這里只說常見的Young GC和Full GC。
觸發Young GC:當新生代中的Eden區沒有足夠空間進行分配時會觸發Young GC。
觸發Full GC:
當準備要觸發一次Young GC時,如果發現統計數據說之前Young GC的平均晉升大小比目前老年代剩余的空間大,則不會觸發Young GC而是轉為觸發Full GC。(通常情況)
如果有永久代的話,在永久代需要分配空間但已經沒有足夠空間時,也要觸發一次Full GC。
System.gc()默認也是觸發Full GC。
heap dump帶GC默認也是觸發Full GC。
CMS GC時出現Concurrent Mode Failure會導致一次Full GC的產生。
對那些JVM認為已經“死掉”的對象。即從GC Root開始搜索,搜索不到的,并且經過一次篩選標記沒有復活的對象。
對這些JVM認為已經“死掉”的對象進行垃圾收集,新生代使用復制算法,老年代使用標記-清除和標記-整理算法。
在Java語言中,可作為GC Roots的對象包括下面幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象。
方法區中類靜態屬性引用的對象。
方法區中常量引用的對象。
本地方法棧中JNI(即一般說的Native方法)引用的對象。
在分代收集中,新生代的規模一般都比老年代要小許多,新生代的收集也比老年代要頻繁許多,如果回收新生代時也不得不同時掃描老年代的話,那么Young GC的效率可能下降不少。顯然是不可能區掃描老年代的,那么是通過什么辦法來解決這個問題了?
在大多垃圾收集器中(G1有不同的地方),通過CardTable來維護老年代對年輕代的引用,CardTable可以說是Remembered Set(RS)的一種特殊實現,是Card的集合。Card是一塊2的冪字節大小的內存區域,例如HotSpot用512字節,里面可能包含多個對象。CardTable要記錄的是從它覆蓋的范圍出發指向別的范圍的指針。以分代式GC的CardTable為例,要記錄老年代指向年輕代的跨代指針,被標記的Card是老年代范圍內的。當進行年輕代的垃圾收集時,只需要掃描年輕代和老年代的CardTable即可保證不對全堆掃描也不會有遺漏。CardTable通常為字節數組,由Card的索引(即數組下標)來標識每個分區的空間地址。
目前HotSpot中有7種作用于不同分代的收集器,如下圖所示,如果兩個收集器之間存在連線,就說明它們可以搭配使用。
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
從名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“標記—清除”算法實現的,它的運作過程可以分為6個步驟,包括:初始標記、并發標記、預處理、重新標記、并發清除、重置。
CMS是一款優秀的收集器,它的主要優點在名字上已經體現出來了:并發收集、低停頓,但是CMS還遠達不到完美的程度,它有以下3個明顯的缺點:
CMS收集器對CPU資源非常敏感。
CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。
CMS是一款基于“標記—清除”算法實現的收集器,這意味著收集結束時會有大量空間碎片產生。
G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一。G1是一款面向服務端應用的垃圾收集器。與其他GC收集器相比,G1具備如下特點:并行與并發、分代收集、空間整合、可預測的停頓。
在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存布局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域,雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。
G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率。
Mixed GC是G1垃圾收集器特有的收集方式,Mixed GC大致可劃分為全局并發標記(global concurrent marking)和拷貝存活對象(evacuation)兩個大部分:
global concurrent marking是基于SATB形式的并發標記,包括以下4個階段:初始標記(Initial Marking)、并發標記(Concurrent Marking)、最終標記(Final Marking)、清理(Clean Up)。Evacuation階段是全暫停的。它負責把一部分region里的活對象拷貝到空region里去,然后回收原本的region的空間。
類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段。其中驗證、準備、解析3個部分統稱為連接。
“類加載”過程的一個階段,在加載階段,虛擬機需要完成以下3件事情:
通過一個類的全限定名來獲取定義此類的二進制字節流。將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。
連接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。從整體上看,驗證階段大致上會完成下面4個階段的檢驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。
該階段是正式為類變量(static修飾的變量)分配內存并設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這里所說的初始值“通常情況”下是數據類型的零值,下表列出了Java中所有基本數據類型的零值。
該階段是虛擬機將常量池內的符號引用替換為直接引用的過程。
初始化階段是執行類構造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中的所有類變量(static修飾的變量)的賦值動作和靜態語句塊(static{}塊)中的語句合并產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的。如果該類存在父類,則虛擬機會保證在執行子類的<clinit>()方法前,父類的<clinit>()方法已經執行完畢。因此在虛擬機中第一個被執行<clinit>()方法的類肯定是java.lang.Object。
從Java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都由Java語言實現,獨立于虛擬機外部,并且全都繼承自抽象類java.lang.ClassLoader。
從Java開發人員的角度來看,絕大部分Java程序都會使用到以下3種系統提供的類加載器。
這個類加載器負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,并且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。
這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
這個類加載器由sun.misc.Launcher$AppClassLoader實現。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
我們的應用程序都是由這3種類加載器互相配合進行加載的,如果有必要,還可以加入自己定義的類加載器。這些類加載器之間的關系一般如圖所示。
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
使用雙親委派模型來組織類加載器之間的關系,有一個顯而易見的好處就是Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱為java.lang.Object的類,并放在程序的ClassPath中,那系統中將會出現多個不同的Object類,Java 類型體系中最基礎的行為也就無法保證,應用程序也將會變得一片混亂。
感謝各位的閱讀,以上就是“Java虛擬機常見問題有哪些”的內容了,經過本文的學習后,相信大家對Java虛擬機常見問題有哪些這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。