淺談JVM

目錄

前言

一、Java虛擬機結構

1-1、整體結構

1-2、編譯流程

1-3、類加載器

1-4、加載流程

二、虛擬機內存管理

三、垃圾回收

3-1、垃圾收集算法

3-2、垃圾回收算法

四、Dalvik VM與JVM的不同

五、Dalvik與ART的不同


前言

在這個專欄的上一篇介紹了class文件和dex文件解析相關的內容,如果有感興趣的可以看一看:

文章地址:https://blog.csdn.net/JArchie520/article/details/83684686

這一篇接着來說說虛擬機,如果沒有虛擬機,那麼即使有class文件和dex文件也是毫無作用的。因爲DVM和ART都是從JVM演變而來的,對JVM做了一些優化,使之更適合於移動端,所以這裏重點來說一下Java虛擬機,因爲把它搞清楚了,再瞭解一下DVM和ART相較於它的優勢,那麼對虛擬機這塊的學習算是暫時OK了。

一、Java虛擬機結構

1-1、整體結構

先來看一張圖(純手工製作,畫的有點醜將就着看吧),這張圖就是JVM整體的組成部分,主要由以下幾個模塊來組成:

1、Class文件生成模塊:這一部分在上一篇中已經詳細的說過了,主要是通過JDK中的javac編譯命令去生成class文件。

2、類加載器子系統:將class字節碼加載到JVM虛擬機內存中,類加載器的核心就是ClassLoader,通常所說的類加載器就是指ClassLoader,classloader則是動態更新的核心,下一篇會介紹Android中的ClassLoader,這裏暫時不說。

3、內存空間:Class字節碼被ClassLoader加載到JVM對應的內存空間之後,JVM則把內存分爲方法區、堆區、棧區和本地方法棧四個部分,這四個部分分別被用來存儲class字節碼不同的部分。

4、垃圾收集器:通常就是我們所說的GC

以上這四個部分是對開發者來說是比較重要的,如果理解了這四個部分那你對JVM就已經有了比較深入的瞭解了。其它的指令計數器、執行引擎、本地方法接口這些部分則是JVM底層用來與CPU打交道的,我們可以不用過多關心。

1-2、編譯流程

從上面這張圖中可以看出:由最開始的Java源代碼經過詞法分析器等一系列的分析最後通過字節碼生成器生成了JVM字節碼,也就是class字節碼,這個過程其實就是javac內部的執行流程,因爲javac是編譯命令,所以整個編譯的流程就是編譯器分析源文件,核心就是對源文件的詞法和語法分析,如果你大學學過編譯原理這門課的話你肯定知道任何一門語言的核心都是編譯器,能寫出來編譯器的都不是一般人。

1-3、類加載器

上面已經瞭解了整個編譯流程,在編譯流程結束後會生成class字節碼,有了字節碼,那肯定是需要ClassLoader將字節碼加載到JVM的虛擬機內存中,所以來了解下在Java中都有哪些類加載器呢?下面這張圖中就是JVM所提供的所有ClassLoader:

1、Bootstrap ClassLoader:這個classloader是加載jre\lib\rt.jar這個jar包中所有的class字節碼,rt.jar是jdk提供的核心的運行時環境的jar包。

2、Extension ClassLoader:這個classloader是加載jre\lib\ext這個目錄下所有的jar包中的字節碼,它和上面的Bootstrap ClassLoader都是用來加載jdk特定jar包的。

3、App ClassLoader:它是用來加載應用程序的ClassLoader,所以它是應用程序真正用到的加載器。

4、Custom ClassLoader:這個是可以自定義的ClassLoader,它的作用是可以重寫一個ClassLoader,然後讓它來加載自定義的Class文件。Android虛擬機也繼承了Java虛擬機這一特性,所以在Android開發中也可以實現動態加載。

1-4、加載流程

瞭解了以上這些類加載器之後,那接下來就是要看一下這些類加載器是如何將Class字節碼加載到JVM對應的內存中的,先來看一下下面這張流程圖吧:

首先是Loading就是加載,接着是Linking是一個連接的過程,這個過程又會分爲三個步驟執行,首先第一步是Verifing驗證,然後是Preparing準備就緒,最後是Resolving分析字節碼,在走完整個Linking流程以後纔會執行Initializing去完成變量的初始化。下面來具體的看一下這幾步分別都有着什麼作用:

  • Loading:類的信息從文件中獲取並且載入到JVM的內存裏
  • Verifying:檢查讀入的結構是否符合JVM規範的描述
  • Preparing:分配一個數據結構用來存儲類的信息
  • Resolving:把這個類的常量池中的所有的符號引用改變成直接引用
  • Initializing:執行靜態初始化程序,把靜態變量初始化成指定的值

比如我在java源代碼中定義了一個常量:

public static final String CONSTANT = "Jarchie"; 

這樣一行代碼其實它並不會立即給CONSTANT這個常量賦值,二是會先走上面說的這個流程。

二、虛擬機內存管理

內存管理也是JVM比較重要的模塊,而且也是面試過程中的一個重點,如果考察JVM的內容很容易被問到這個東西。在上面說到JVM結構的時候提到過,JVM將內存空間劃分爲四個區:方法區、堆區、棧區、本地方法棧,下面就來看一下每個區分別是什麼作用?

1、Java棧區

作用:它存放的是Java方法執行時的所有的數據,是用來描述Java方法執行的完整的內存模型

組成:由棧幀組成,一個棧幀代表一個方法的執行。

下面來說說Java棧幀是什麼?

棧幀的作用:每個方法從調用到執行完成就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

舉個栗子:a()方法在運行時調用到b()方法,在執行到調用代碼時,JVM會創建一個保存b()方法的棧幀,然後把這個棧幀push到Java棧區中,當b()方法執行完成返回到a()方法中時,這個棧幀就會隨之被pop出Java棧區,這就是一個棧幀描述一個方法調用的完整過程。

棧幀的組成:局部變量表、棧操作數、動態鏈接、方法出口,可見棧幀存儲了方法調用過程中的所有內容。

2、本地方法棧

作用:本地方法棧是專門爲native方法服務的,它也是通過棧幀來記錄每個方法的調用

普通棧區是爲Java方法服務的,本地方法棧是專門爲native方法服務的,這是Java棧區和本地方法棧的一個重要區別。

3、方法區

作用:存儲被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的數據等,且方法區是永遠佔據內存的

4、堆區

作用:所有通過new創建的對象的內存都在堆中分配

特點:是虛擬機中最大的一塊內存,是GC要回收的部分

對於堆區這塊大內存,JVM是如何分配的,來看一下下面這張圖(我覺得這張畫的還不錯😀):

上面:Young Generation是堆區中的新生代區,對於剛創建的對象會被放入到新生代區。

中間:Old Generation是老年代區,當新生代區內存不足時,Java虛擬機會按照一定的算法將新生代區的對象移到老年代區中,這樣新生代區就又有了新的內存空間。

下面:Permanent Generation是持久代(這個現在好像已經移除了),主要存放的是Java類的類信息,與GC要收集的Java對象關係不大。

GC重點回收的是新生代和老年代這兩部分的內存區域,當新生代和老年代都沒有足夠的內存時,JVM就會拋出OOM內存溢出的異常。JVM將內存區域分新生代和老年代而不是一塊完整的內存區域,這樣做最大的好處就是開發人員可以按需分配新老兩代的內存空間,使之更好的適應不同的業務場景,當然了對於移動端不會這麼做哈,這個手動調節說的是服務器。

三、垃圾回收

垃圾回收也就是我們平時經常說的GC,這個也是面試中經常被問到的東西。既然是垃圾回收,那首先就來看看哪些對象會被標記爲垃圾對象?

3-1、垃圾收集算法

1、引用計數算法

這個算法是Java虛擬機最早使用的算法,在JDK1.2之前都是使用的這個算法來標記對象是不是垃圾對象,在內存中創建一個對象的同時會爲它產生一個引用計數器,同時將引用計數器加1,每當有新的引用引入到子對象的時候,計數器就累計加1,當其中的一個引用銷燬的時候,引用計數器減1,當引用計數器減爲0的時候,標誌這個對象已經是垃圾對象了,可以被回收。

這種算法存在一個很大的問題,舉個栗子:下圖中ObjectA引用了ObjectB,所以ObjectB的引用計數器是+1,而ObjectB又引用了ObjectA,所以ObjectA的引用計數器也是+1,同時ObjectA和ObjectB都是不可達的,就是沒有路徑能指向這兩個對象,所以它們其實已經是垃圾了,但是在這種算法下卻不能被回收。

2、可達性算法

由於引用計數法存在的問題,所以從JDK1.2以後對算法進行了改進,開始使用可達性算法,也叫根搜索算法,這個算法是由離散數學中的圖論引入的。整個程序把所有的引用關係看做一張圖,從GCRoot根節點開始尋找對應的所有的引用節點,然後繼續尋找對應節點所有的引用,當所有的引用節點尋找完畢之後,剩餘的節點則被認爲是沒有被引用的節點,即不可達的節點,被認爲是垃圾對象,所以這種算法就避免了上面出現的那個問題。

上面這張圖中可以看到,從GC Root這個根對象引用開始一直遍歷,只要路徑可達,那麼這個對象就是被引用則不能被回收,像ObjectD、ObjectE、ObjectF因爲沒有路徑可以到達這幾個部分,所以它們都是垃圾對象可以被回收。在確定垃圾對象的時候經常會說到引用,下面來看一下對象都有哪些類型的引用呢?

3、引用的類型

引用類型分爲四種:強引用、軟引用、弱引用、虛引用,實際開發中使用最多的是強飲用和弱引用。

弱引用的創建,簡單的來看個例子吧:

Object obj = new Object(); //強引用創建,obj這個引用指向Object對象
WeakReference<Object> wf = new WeakReference<Object>(obj); //創建obj對象的弱引用wf
obj = null; //此時只有wf這個弱引用指向Object對象
//獲取真正的引用,使用時要判斷獲取到的引用是否爲空
//因爲弱引用不會阻礙對象的回收,所以很有可能被置爲空
wf.get(); 

在確定了垃圾對象以後,那麼如何回收垃圾對象呢?

3-2、垃圾回收算法

1、標記-清楚算法:通過一張圖來看一下它具體是如何進行垃圾回收的

首先它會從根集合也就是根節點來遍歷所有的引用,從根集合可以路由到A對象的引用,通過A對象也可以路由到C對象的引用,而B對象成爲了不可達的對象引用,所以掃描過後B被標記爲可回收對象,最後在垃圾回收執行的時候,會直接把B對象置爲空,在內存塊中就剩下了A和C對象的引用,B則被垃圾回收給回收掉了,這就是標記-清楚算法的流程。

優點:不需要進行對象的移動,並且僅對不存活的對象進行處理,在存活對象較多的情況下極爲高效。

缺點:由於它是直接回收不存活的對象,所以會造成內存碎片,不利於後續對象的內存分配。

2、複製算法:同樣的通過圖形來看一下:

它的處理較爲簡單,首先也是從根集合進行遍歷,遍歷到A引用是可達的,則把A引用複製到另一塊空閒的內存中,接着發現B引用是不可達的,則跳過不進行復制,繼續遍歷發現C也是可達的,同樣的把C也複製到這塊空閒的內存中,等所有的複製都處理完以後,它會把原來的內存空間清空,只保留複製以後的這塊內存空間,這樣就完成了對垃圾對象的回收,這種算法其實是利用了以空間換時間的思想。

優點:當存活對象較少時,極爲高效

缺點:它需要一塊內存作爲交換空間進行對象的移動

3、標記-整理算法:同樣的通過下圖來看一下它又是如何處理的:

首先它也是從根集合進行遍歷,通過整個內存區的掃描,將可回收對象掃描出來,到了第二階段圖中B就被標記爲了可回收對象,從第二階段到第三階段,直接掃描整個空間並清除被標記的對象,可見標記-整理算法是在標記-清楚算法上進行的改進,只是在回收不存活對象佔用的空間時,它會將所有的存活對象往左端空閒處移動,並更新對應的指針,所以這種算法在標記-清楚算法上進行對象的移動,因此成本更高,但是解決了內存碎片的問題,同時它因爲需要不斷的移動對象到另一側,所以它不適合頻繁創建和回收對象這種場景。

通過對比可以發現,這三種算法只是相對優劣,並沒有哪一種算法是徹底解決了所有問題,所以這三種算法在虛擬機中是結合使用的。

4、觸發回收

  • Java虛擬機無法再爲新的對象分配內存空間了(即應用程序的存儲空間已經分配完了)
  • 手動調用System.gc()方法(強烈不推薦)
  • 低優先級的GC線程,被運行時就會執行GC

四、Dalvik VM與JVM的不同

  • 執行的文件不同,一個是class,一個是dex 
  • 類加載系統與JVM區別較大
  • JVM只能同時存在一個,而DVM可以同時存在多個
  • Dalvik是基於寄存器(寄存器是比內存快的存儲介質,所以運行速度更快)的,而JVM是基於棧的

五、Dalvik與ART的不同

首先了解兩個名詞JIT和AOT,關於這兩個詞的解釋我在《Dart語言之常用數據類型》一文中Dart概述裏有提到過,有不瞭解的可以直接點過去看看,這裏不再解釋了,下面直接來看它們的不同點:

  • DVM使用JIT來將字節碼轉換成機器碼,效率低(JIT應用程序每次運行都會將字節碼轉化成本地機器碼再去執行)
  • ART採用了AOT預編譯技術,執行速度更快(應用程序安裝時就將字節碼轉換成本地機器碼)
  • ART會佔用更多的應用安裝時間和存儲空間(以空間換時間)

對於DVM和ART這兩種虛擬機就不再詳細介紹了,這裏也已經對比着瞭解了一下,它們的內存管理和垃圾回收等模塊和JVM都是類似的,區別較大的是類加載模塊,這個會在下一篇中介紹,當然了介紹的是Android中的類加載機制,寫到這裏,這一篇就已經結束了,有不對的地方歡迎留言探討,因爲我也是個菜鳥!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章