目錄
前言
JVM簡介
JVM內存區(qū)域劃分
JVM的類加載機制
1.加載
雙親委派模型
2.驗證
驗證選項
3.準備
4.解析
5.初始化
觸發(fā)類加載
JVM的垃圾回收策略 GC
一:找? ? ?誰是垃圾?
1.引用計數
2.可達性分析? (這個方案是Java采取的方案)。
二:釋放垃圾對象
三種典型的策略
JVM實現思路
前言
我們在學習JVM的時候,其實里面的內容是非常之多的,但是里面的大部分內容都是屬于八股,想要徹底搞明白,就需要看大量的關于JVM的源代碼,JVM的源代碼是C++寫的。想要深入研究的可以去看看《深入理解Java虛擬機》這本書。
這篇文章主要針對JVM中的常見的面試題來展開。
JVM簡介
JVM 是 Java Virtual Machine 的簡稱,意為 Java虛擬機。
虛擬機是指通過軟件模擬的具有完整硬件功能的、運行在一個完全隔離的環(huán)境中的完整計算機系統。
常見的虛擬機:JVM、VMwave、Virtual Box。
JVM 和其他兩個虛擬機的區(qū)別:
- VMwave與VirtualBox是通過軟件模擬物理CPU的指令集,物理系統中會有很多的寄存器;
- JVM則是通過軟件模擬Java字節(jié)碼的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都進行了裁剪。
JVM 是一臺被定制過的現實當中不存在的計算機。
JVM內存區(qū)域劃分
JVM其實就是一個Java進程,Java進程也就是JVM會從操作系統這里申請一大塊內存空間,給Java代碼來使用。
JVM從操作系統申請的這塊內存空間中,進行進一步的劃分,給出了每塊劃分后的空間的不同用途。
其中,最核心的就是棧、堆、元數據區(qū)(方法區(qū))。
- 虛擬機棧是給Java代碼來使用的,主要存放一些局部變量,還有維護方法之間的調用關系。
- 本地方法棧則是給JVM內部的本地方法來使用的。
- 堆上存放的就是new出來的對象、成員變量。
- 程序計數器中存放的就是一個內存地址,這個內存地址就是下一個要執(zhí)行字節(jié)碼所在的地址,作用就是記錄當前程序執(zhí)行到那個指令了。
需要注意的是,堆和元數據區(qū),在一個JVM 中只存在一份,也就是多個線程共享堆區(qū)和元數據區(qū)。
棧(本地方法棧和虛擬機棧)和程序計數器則是存在多份的,也就是每個線程都會有一份。
JVM的線程操作和操作系統的線程操作是一對一的關系。也就是說每次在Java代碼中創(chuàng)建的線程都會在操作系統中有一個線程與之對應。
這里的面試題主要就是判斷某個變量或者對象在JVM的那個區(qū)域?
例如下面代碼:
void func() {
Test t1 = new Test();
}
上述代碼在一個方法里面我們實例化了一個Test對象。
?func方法是在元數據區(qū)以一些二進制的指令來存儲的。
我們可以看到t1變量是一個在方法里面定義的,所以他是一個局部變量,局部變量就存儲在棧上。
而new Test(); 這個對象的本體則是在堆上的。
其實像這里的關于JVM區(qū)域的面試題,我們只需要知道JVM的每個區(qū)域都是存儲什么東西的就好了。
- 虛擬機棧是給Java代碼來使用的,主要存放一些局部變量,還有維護方法之間的調用關系。
- 本地方法棧則是給JVM內部的本地方法來使用的。
- 堆上存放的就是new出來的對象、成員變量。
- 程序計數器中存放的就是一個內存地址,這個內存地址就是下一個要執(zhí)行字節(jié)碼所在的地址,作用就是記錄當前程序執(zhí)行到那個指令了。
JVM的類加載機制
對與一個類來說,他的生命周期是這樣的:
?前面的5步也是類加載的過程和固定的順序。我們主要研究前面的5步。
類加載具體就是把一個.class文件,也就是類編譯后的文件,加載到內存中,得到了類對象這樣的過程就稱之為類加載。
一個程序想要運行,就需要把指令和數據加載到內存中。類加載就是做的這個事情。
下面是類加載的5個步驟:
1.加載
這里的加載過程其實簡單,就是找到.class文件,然后讀取文件的內容。
但是在找.class文件的這個過程中,會有一個非常重要的機制:雙親委派模型
雙親委派模型
在JVM中,加載類需要用到一組特殊的模塊:類加載器。
在JVM中,內置了三個類加載器。
- BootStrap ClassLoader? ? 負責加載Java標準庫中的類
- Extension ClassLoader? ? ?負責加載一些非標準的但是是Sun/Oracle擴展庫的類
- Application ClassLoader? ? 負責加載項目中自己寫的類、以及第三方庫中的類
當具體加載一個類的時候,他的過程是這樣的:
需要先給定一個類的全限定類名,"java.lang.String"? 這個類名是一個字符串的形式。
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到最頂層BootStrap ClassLoader類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去完成加載。
具體可以參考下圖:
2.驗證
由于.class文件有著明確的數據格式(二進制的),這一階段的主要目的就是確保Class文件中的字節(jié)流中包含的信息符合《Java虛擬機規(guī)范》的全部約束要求。
驗證選項
文件格式驗證
字節(jié)碼驗證
符號引用驗證……
3.準備
準備階段是正式為類中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內存并設置類變量初始值的階段。
比如下面這樣的代碼:
public static int value = 123;
此時在準備階段value的值并不是123,而是0。
?
4.解析
解析階段是Java虛擬機將常量池內的符號引用替換為直接引用的過程,也就是初始化常量的過程。
- 符號引用:就是字符串常量在.class文件已經存在,但是他們只知道彼此之間的相對位置,并不知道自己在內存中的具體位置。
- 直接引用:真正的加載到內存中,就會把字符串常量填充到內存中的特定地址上去。此時字符串引用的就是直接引用,(也就是Java中普通的引用)。
5.初始化
在初始化階段,JVM才真正的執(zhí)行類中編寫的Java代碼,將主導權交給應用程序,初始化階段就是執(zhí)行類的構造方法的過程。(類要是有父類,就需要先初始化父類,在初始化子類)。
觸發(fā)類加載
注意:類加載這個動作不是說JVM一啟動就會進行加載,因為JVM整體是一個懶加載的策略,也就是非必要,不加載。
以下三種請況就會加載:
- 創(chuàng)建了這個類的實例
- 使用了這個類的靜態(tài)方法/靜態(tài)屬性
- 使用子類,會觸發(fā)父類的加載
JVM的垃圾回收策略 GC
Java中的垃圾回收是為了幫助我們自動釋放內存的一種機制。
面試題:為什么需要垃圾回收機制
因為在程序運行過程中,會向操作系統申請大量的內存空間,但是這些空間也有可能會消耗盡,因為不斷地分配內存空間而不進行回收,就好像不停地生產生活垃圾而從來不打掃一樣。
上面我們談到了關于JVM的幾個區(qū)域,那么垃圾回收釋放的是那個區(qū)域的空間呢?
需要注意的是,棧和程序計數器是每個線程都會有一份的。他們會隨著線程的銷毀而一起銷毀的。
而元數據區(qū)里面的存儲的類對象,很少會進行銷毀。
所以我們釋放的就是堆中的空間。上面我們談到堆中主要就是存放new 出來的對象的。
GC也就是以對象為單位進行釋放的。(釋放對象)
GC中主要分為兩個階段:
一:找? ? ?誰是垃圾?
Java通過引用來判斷是否是垃圾對象,如果沒有引用指向,就判定這個對象是垃圾。
1.引用計數
給對象安排一個額外的空間,保存了一個整數,表示該對象有幾個引用指向它。Java實際上并沒有采取這樣的方案,(Python、PHP采用了這個方案)。
Test t1 = new Test();
?此時是有一個引用指向的,所以引用計數器為1。
如果代碼變成這樣:
Test t1 = new Test();
Test t2 = t1;
?也就是說隨著引用的增加,計數器就會增加,引用的銷毀,計數器就會減少。
當計數器為0時,就會認為該對象沒有引用指向了,就是垃圾了。
但是缺點也是很明顯:
- 浪費內存空間
- 存在循壞引用的情況
2.可達性分析? (這個方案是Java采取的方案)。
把對象之間的引用關系理解成為了一個樹形結構,從一些特殊的起點出發(fā),進行遍歷,只要能訪問到,是可達的,不是垃圾,再把不可達的當做垃圾即可。
?此時通過root這個引用是可以訪問到整個樹的任意節(jié)點的。
可達性分析的關鍵要點在于要進行上述的遍歷,需要有起點的。
起點可以是:
- 棧上的局部變量(每個棧的每個局部變量都是起點)
- 常量池中引用的對象
- 方法區(qū)中靜態(tài)成員引用的對象
可達性分析,總體就是從所有的起點出發(fā),看看該對象里面又通過哪些引用能訪問到那些對象,順藤摸瓜的把所有可以訪問的對象都訪問一遍,遍歷的同時把對象標記為“可達”。
可達性分析,克服了引用計數的兩個缺點
但是也是有自己的問題:
- 消耗更多的時間 因此即使某個對象成了垃圾,也不能第一時間發(fā)現,因為在掃描的過程中,也是需要時間的。
- 在進行可達性分析的時候,要順藤摸瓜,一旦這個過程中,當前代碼中的對象的引用關系發(fā)生了變化,就可以出現bug。
因此為了更好的完成這個順藤摸瓜的過程,就需要讓其他的業(yè)務線程都暫停工作?。。。⊿TW)
(STW)? ?stop the world !
但是Java畢竟發(fā)展了這么多年,拉進回收這里也是在不斷的進行優(yōu)化,STW這個問題也可以比較好的對付了。
二:釋放垃圾對象
三種典型的策略
1:標記清除
?如果現在向內存申請了一塊下面這樣的空間,然后我標出來的就是垃圾對象,需要清除的。
?這種策略就是直接把垃圾對象的內存就釋放了。
但是這種簡單粗暴的方式會產生內存碎片。
內存碎片:申請空間都是連續(xù)的整塊空間,現在上述圖中的空閑空間都是散落在獨立的空間里面的。現在空閑總空間可能超過1G,但是我想申請500M,卻是申請不了。
2:復制算法
這種方法是把空間分為兩部分。一次只使用一半。
復制算法就是把不是垃圾的對象拷貝到一邊去,然后在統一釋放整個區(qū)域。
?此時我要釋放的是2和4,我就需要把剩下1和3復制到另一邊去。然后再把這邊全部釋放。
?復制算法解決了內存碎片的問題,但是也有缺點:
- 內存利用率比較低
- 如果大部分對象都是保留的,垃圾很少,此時的復制成本就比較高
3:標記整理
類似于順序表刪除中間元素,有一個搬運的過程
?解決了內存碎片問題但是搬運的整體開銷也是比較大的。
JVM實現思路
實際上,JVM的實現方式是結合了上述幾種思想之后的方法。
分代回收思想
具體細節(jié):
- 給對象設置年齡這樣的概念,用來描述這個對象存在多久了。如果一個對象剛誕生,那么就是0歲。
- 每次進過一次掃描(可達性分析)如果沒有被標記為垃圾對象,這是對象年齡就增加一歲。
- 通過年齡來區(qū)分這個對象的活動時間。
經驗規(guī)律:年齡越大的對象,也將會持續(xù)存在更長的時間。
針對不同的年齡來采取不同的回收策略
JVM針對這幾個區(qū)域來執(zhí)行不同的策略。
1:新創(chuàng)建的對象,放在伊甸區(qū)
垃圾回收掃描到伊甸區(qū)之后,大多數的對象將會在第一輪掃描下被GC給淘汰掉。
2:如果伊甸區(qū)的對象,熬過第一輪GC,就會通過復制算法,拷貝到生存區(qū)。
生存區(qū)分為兩半(大小相等),一次只使用其中的一半。
如果GC在掃描生存區(qū)的時候,發(fā)現垃圾對象也就淘汰,不是垃圾的,就通過復制算法拷貝到生存區(qū)的另一邊。
3:當對象在生存區(qū)熬過了若干次GC的時候,年齡也變大了。此時就會通過復制算法拷貝到老年代。
4:進入老年代之后,由于年齡都比較大了,被標記為垃圾對象的概念也很小,所以針對老年代的GC掃描也會降低頻率。文章來源:http://www.zghlxwxcb.cn/news/detail-562850.html
特殊情況:如果對象非常大,直接進入老年代(大對象進行復制算法,成本非常高,而且大對象也不會很多)。文章來源地址http://www.zghlxwxcb.cn/news/detail-562850.html
到了這里,關于JVM——類加載和垃圾回收的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!