天天看點

JVM簡介

一、什麼是JVM

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

  Java語言的一個非常重要的特點就是與平台的無關性。而使用Java虛拟機是實作這一特點的關鍵。一般的進階語言如果要在不同的平台上運作,至少需要編譯成不同的目标代碼。而引入Java語言虛拟機後,Java語言在不同平台上運作時不需要重新編譯。Java語言使用Java虛拟機屏蔽了與具體平台相關的資訊,使得Java語言編譯程式隻需生成在Java虛拟機上運作的目标代碼(位元組碼),就可以在多種平台上不加修改地運作。Java虛拟機在執行位元組碼時,把位元組碼解釋成具體平台上的機器指令執行。這就是Java的能夠“一次編譯,到處運作”的原因。

二、JVM的組成

我們先把JVM這個虛拟機畫出來,如下圖所示:

JVM簡介

從這張圖中我們可以看出,JVM是運作在作業系統之上的,它與硬體沒有直接的互動,我們再來看JVM由哪些部分組成,如下圖所示:

JVM簡介

1、Class Loader 類加載器

  類加載器的作用是加載類檔案到記憶體,比如編寫一個HelloWord.java 程式,然後通過javac 編譯成class 檔案,那怎麼才能加載到記憶體中被執行呢?Class Loader 承擔的就是這個責任,那不可能随便建立一個.class 檔案就能被加載的,Class Loader 加載的class 檔案是有格式要求。

Class Loader 隻管加載,隻要符合檔案結構就加載,至于說能不能運作,則不是它負責的,那是由Execution Engine 負責的。

2、Execution Engine 執行引擎

  執行引擎也叫做解釋器(Interpreter) ,負責解釋指令,送出作業系統執行。

3、Native Interface 本地接口

  本地接口的作用是融合不同的程式設計語言為Java 所用,它的初衷是融合C/C++ 程式,Java 誕生的時候是C/C++ 橫行的時候,要想立足,必須有一個聰明的、睿智的調用C/C++ 程式,于是就在記憶體中專門開辟了一塊區域處理标記為native 的代碼,它的具體做法是Native Method Stack 中登記native 方法,在Execution Engine 執行時加載native libraies 。目前該方法使用的是越來越少了,除非是與硬體有關的應用,比如通過Java 程式驅動列印機,或者Java 系統管理生産裝置,在企業級應用中已經比較少見,因為現在的異構領域間的通信很發達,比如可以使用Socket 通信,也可以使用Web Service 等等,不多做介紹。

4、 Runtime data area 運作資料區

  運作資料區是整個JVM 的重點。我們所有寫的程式都被加載到這裡,之後才開始運作,Java 生态系統如此的繁榮,得益于該區域的優良自治。

整個JVM 架構由加載器加載檔案,然後執行器在記憶體中處理資料,需要與異構系統互動是可以通過本地接口進行,瞧,一個完整的系統誕生了!

三、JVM的記憶體管理

所有的資料和程式都是在運作資料區存放,它包括以下幾部分:

1、  Stack 棧

棧也叫棧記憶體,是Java程式的運作區,是線上程建立時建立,它的生命期是跟随線程的生命期,線程結束棧記憶體也就釋放,對于棧來說不存在垃圾回收問題,隻要線程一結束,該棧就Over。問題出來了:棧中存的是那些資料呢?又什麼是格式呢?

棧中的資料都是以棧幀(Stack Frame)的格式存在,棧幀是一個記憶體區塊,是一個資料集,是一個有關方法(Method)和運作期資料的資料集,當一個方法A被調用時就産生了一個棧幀F1,并被壓入到棧中,A方法又調用了B方法,于是産生棧幀F2也被壓入棧,執行完畢後,先彈出F2棧幀,再彈出F1棧幀,遵循“先進後出”原則。

那棧幀中到底存在着什麼資料呢?棧幀中主要儲存3類資料:本地變量(Local Variables),包括輸入參數和輸出參數以及方法内的變量;棧操作(Operand Stack),記錄出棧、入棧的操作;棧幀資料(Frame Data),包括類檔案、方法等等。光說比較枯燥,我們畫個圖來了解一下Java棧,如下圖所示:

JVM簡介

 圖示在一個棧中有兩個棧幀,棧幀2是最先被調用的方法,先入棧,然後方法2又調用了方法1,棧幀1處于棧頂的位置,棧幀2處于棧底,執行完畢後,依次彈出棧幀1和棧幀2,線程結束,棧釋放。

2、Heap 堆記憶體

jvm中分為堆和方法區,堆又進一步分為新生代和老年代,方法區為永久代。

 堆中區分的新生代和老年代是為了垃圾回收,新生代中的對象存活期一般不長,而老年代中的對象存活期較長,是以當垃圾回收器回收記憶體時,新生代中垃圾回收效果較好,會回收大量的記憶體,而老年代中回收效果較差,記憶體回收不會太多。

基于以上特性,新生代中一般采用複制算法,因為存活下來的對象是少數,所需要複制的對象少,而老年代對象存活多,不适合采用複制算法,一般是标記整理和标記清除算法。

因為複制算法需要留出一塊單獨的記憶體空間來以備垃圾回收時複制對象使用,是以将新生代分為eden區和兩個survivor區,每次使用eden和一個survivor區,另一個survivor作為備用的對象複制記憶體區。

一個JVM執行個體隻存在一個堆類存,堆記憶體的大小是可以調節的。類加載器讀取了類檔案後,需要把類、方法、常變量放到堆記憶體中,以友善執行器執行,堆記憶體分為三部分:

  Permanent Space 永久存儲區

永久存儲區是一個常駐記憶體區域,用于存放JDK自身所攜帶的Class,Interface的中繼資料,也就是說它存儲的是運作環境必須的類資訊,被裝載進此區域的資料是不會被垃圾回收器回收掉的,關閉JVM才會釋放此區域所占用的記憶體。

  Young Generation Space 新生區

新生區是類的誕生、成長、消亡的區域,一個類在這裡産生,應用,最後被垃圾回收器收集,結束生命。新生區又分為兩部分:伊甸區(Eden space)和幸存者區(Survivor pace),所有的類都是在伊甸區被new出來的。幸存區有兩個: 0區(Survivor 0 space)和1區(Survivor 1 space)。當伊甸園的空間用完時,程式又需要建立對象,JVM的垃圾回收器将對伊甸園區進行垃圾回收,将伊甸園區中的不再被其他對象所引用的對象進行銷毀。然後将伊甸園中的剩餘對象移動到幸存0區。若幸存0區也滿了,再對該區進行垃圾回收,然後移動到1區。那如果1區也滿了呢?再移動到養老區。

  Tenure generation space養老區

養老區用于儲存從新生區篩選出來的JAVA對象,一般池對象都在這個區域活躍。   三個區的示意圖如下:

JVM簡介

 3、 Method Area 方法區

方法區是被所有線程共享,該區域儲存所有字段和方法位元組碼,以及一些特殊方法如構造函數,接口代碼也在此定義。

4、PC Register 程式計數器

每個線程都有一個程式計數器,就是一個指針,指向方法區中的方法位元組碼,由執行引擎讀取下一條指令。

5、Native Method Stack 本地方法棧

四、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

五、JVM相關問題

問:堆和棧有什麼差別

答:堆是存放對象的,但是對象内的臨時變量是存在棧記憶體中,如例子中的methodVar是在運作期存放到棧中的。

棧是跟随線程的,有線程就有棧,堆是跟随JVM的,有JVM就有堆記憶體。

問:堆記憶體中到底存在着什麼東西?

答:對象,包括對象變量以及對象方法。

問:類變量和執行個體變量有什麼差別?

答:靜态變量是類變量,非靜态變量是執行個體變量,直白的說,有static修飾的變量是靜态變量,沒有static修飾的變量是執行個體變量。靜态變量存在方法區中,執行個體變量存在堆記憶體中。

問:我聽說類變量是在JVM啟動時就初始化好的,和你這說的不同呀!

答:那你是道聽途說,信我的,沒錯。

問:Java的方法(函數)到底是傳值還是傳址?

答:都不是,是以傳值的方式傳遞位址,具體的說原生資料類型傳遞的值,引用類型傳遞的位址。對于原始資料類型,JVM的處理方法是從Method Area或Heap中拷貝到Stack,然後運作frame中的方法,運作完畢後再把變量指拷貝回去。

問:為什麼會産生OutOfMemory産生?

答:一句話:Heap記憶體中沒有足夠的可用記憶體了。這句話要好好了解,不是說Heap沒有記憶體了,是說新申請記憶體的對象大于Heap空閑記憶體,比如現在Heap還空閑1M,但是新申請的記憶體需要1.1M,于是就會報OutOfMemory了,可能以後的對象申請的記憶體都隻要0.9M,于是就隻出現一次OutOfMemory,GC也正常了,看起來像偶發事件,就是這麼回事。       但如果此時GC沒有回收就會産生挂起情況,系統不響應了。

問:我産生的對象不多呀,為什麼還會産生OutOfMemory?

答:你繼承層次忒多了,Heap中 産生的對象是先産生 父類,然後才産生子類,明白不?

問:OutOfMemory錯誤分幾種?

答:分兩種,分别是“OutOfMemoryError:java heap size”和”OutOfMemoryError: PermGen space”,兩種都是記憶體溢出,heap size是說申請不到新的記憶體了,這個很常見,檢查應用或調整堆記憶體大小。

“PermGen space”是因為永久存儲區滿了,這個也很常見,一般在熱釋出的環境中出現,是因為每次釋出應用系統都不重新開機,久而久之永久存儲區中的死對象太多導緻新對象無法申請記憶體,一般重新啟動一下即可。

問:為什麼會産生StackOverflowError?

答:因為一個線程把Stack記憶體全部耗盡了,一般是遞歸函數造成的。

問:一個機器上可以看多個JVM嗎?JVM之間可以互訪嗎?

答:可以多個JVM,隻要機器承受得了。JVM之間是不可以互訪,你不能在A-JVM中通路B-JVM的Heap記憶體,這是不可能的。在以前老版本的JVM中,會出現A-JVM Crack後影響到B-JVM,現在版本非常少見。

問:為什麼Java要采用垃圾回收機制,而不采用C/C++的顯式記憶體管理?

答:為了簡單,記憶體管理不是每個程式員都能折騰好的。

問:為什麼你沒有詳細介紹垃圾回收機制?

答:垃圾回收機制每個JVM都不同,JVM Specification隻是定義了要自動釋放記憶體,也就是說它隻定義了垃圾回收的抽象方法,具體怎麼實作各個廠商都不同,算法各異,這東西實在沒必要深入。

問:JVM中到底哪些區域是共享的?哪些是私有的?

答:Heap和Method Area是共享的,其他都是私有的,

問:什麼是JIT,你怎麼沒說?

答:JIT是指Just In Time,有的文檔把JIT作為JVM的一個部件來介紹,有的是作為執行引擎的一部分來介紹,這都能了解。Java剛誕生的時候是一個解釋性語言,别噓,即使編譯成了位元組碼(byte code)也是針對JVM的,它需要再次翻譯成原生代碼(native code)才能被機器執行,于是效率的擔憂就提出來了。Sun為了解決該問題提出了一套新的機制,好,你想編譯成原生代碼,沒問題,我在JVM上提供一個工具,把位元組碼編譯成原生碼,下次你來通路的時候直接通路原生碼就成了,于是JIT就誕生了,就這麼回事。

問:JVM還有哪些部分是你沒有提到的?

答:JVM是一個異常複雜的東西,寫一本磚頭書都不為過,還有幾個要說明的:

常量池(constant pool):按照順序存放程式中的常量,并且進行索引編号的區域。比如int i =100,這個100就放在常量池中。

安全管理器(Security Manager):提供Java運作期的安全控制,防止惡意攻擊,比如指定讀取檔案,寫入檔案權限,網絡通路,建立程序等等,Class Loader在Security Manager認證通過後才能加載class檔案的。

方法索引表(Methods table),記錄的是每個method的位址資訊,Stack和Heap中的位址指針其實是指向Methods table位址。

問:為什麼不建議在程式中顯式的生命System.gc()?

答:因為顯式聲明是做堆記憶體全掃描,也就是Full GC,是需要停止所有的活動的(Stop  The World Collection),你的應用能承受這個嗎?

問:JVM有哪些調整參數?

答:非常多,自己去找,堆記憶體、棧記憶體的大小都可以定義,甚至是堆記憶體的三個部分、新生代的各個比例都能調整。

jvm