深入理解Java虛擬機系列(1)——什麼叫做JVM與JVM規範

引言

       (1)內存管理一直是JAVA語言自豪與驕傲的資本,它讓JAVA程序員基本上可以徹底忽略與內存管理相關的細節,只專注於業務邏輯。不過世界上不存在十全十美的好事,在帶來了便利的同時,也因此引入了很多令人抓狂的內存溢出和泄露的問題。

   (2)可怕的事情還不只如此,有些使用其它語言開發的程序員,給JAVA程序員扣上了一個“不懂內存”的帽子,這着實有點讓人難以接受。畢竟JAVA當中沒有malloc和delete、沒有析構函數、沒有指針,剛開始接觸JAVA的程序員們又怎麼可能接觸內存這一部分呢,更何況有不少JAVA程序員還是跳了專業半路出家的朋友。

   (3)不過事實儘管難以接受,但也確實有不少JAVA程序員對內存這部分可謂一竅不知,儘管掌握內存的相關知識,或許並不能給平時的開發帶來翻天覆地的變化和好處,不過它仍然會潛移默化的提高你的技術水準,這一點在瞭解完內存管理之後,相信各位就會深有體會了。

   上面這個幾句話着實寫得不錯,內容的引進來自左簫龍先生筆下的《Java語言的內存管理概述》,有興趣的朋友可以前往筆者的文章,很多都寫得不錯。

Java虛擬機規範跟Java虛擬機

     (1)虛擬機是一種抽象的計算機,通過從實際的計算機中仿真模擬各種計算機功能來實現的。JAVA虛擬機規範是一種對JAVA虛擬機實現的規範要求,是由oracle制定的,而我們平時常說的JAVA虛擬機一般是指的一種具體的JAVA虛擬機規範的實現。比如我們最經常使用的JAVA虛擬機hotspot,其實JAVA虛擬機還有很多種實現,甚至如果你對JAVA虛擬機規範有了深入的瞭解而且對此有興趣的話,可以寫一個自己的JAVA虛擬機,當然這其中的難度不難想象。Java虛擬機有自己完善的硬體架構,如處理器堆棧寄存器等,還具有相應的指令系統。JVM屏蔽了與具體操作系統平臺相關的信息,使得Java程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。

  (2)JVM是Java程序運行的環境,同時是一個操作系統的一個應用程序進程,因此它有自己的生命週期,也有己的代碼和數據空間。

  (3)JVM體系主要是兩個JVM的內部體系結構分爲三個子系統和兩大組件,分別是:類裝載(ClassLoader)子系統、執行引擎子系統和GC子系統,組件是內存運行數據區域和本地接口。

從進程的角度解釋JVM

    讓我們嘗試從操作系統的層面來理解虛擬機。我們知道,虛擬機是運行在操作系統之中的,那麼什麼東西才能在操作系統中運行呢?當然是進程,因爲進程是操作系統中的執行單位。可以這樣理解,當它在運行的時候,它就是一個操作系統中的進程實例,當它沒有在運行時(作爲可執行文件存放於文件系統中),可以把它叫做程序。

對命令行比較熟悉的同學,都知道其實一個命令對應一個可執行的二進制文件,當敲下這個命令並且回車後,就會創建一個進程,加載對應的可執行文件到進程的地址空間中,並且執行其中的指令。下面對比C語言和Java語言的HelloWorld程序來說明問題。

#include <stdio.h>  
#include <stdlib.h>  
  
int main(void) {  
    printf("hello world\n");  
    return 0;  
}  

編譯C語言版的HelloWorld程序
gcc HelloWorld.c -o HelloWorld
運行C語言版的HelloWorld程序
./HelloWorld   

gcc編譯器編譯後的文件直接就是可被操作系統識別的二進制可執行文件,當我們在命令行中敲下 ./HelloWorld這條命令的時候, 直接創建一個進程, 並且將可執行文件加載到進程的地址空間中, 執行文件中的指令。

public class HelloWorld {  
  
    public static void main(String[] args) {  
        System.out.println("HelloWorld");  
    }  
} 
編譯Java版的HelloWorld程序
javac HelloWorld.java 
運行Java版的HelloWorld程序
java -classpath . HelloWorld  
我們在運行Java版的HelloWorld程序的時候, 敲入的命令並不是 ./HelloWorld.class 。 因爲class文件並不是可以直接被操作系統識別的二進制可執行文件 。 

我們敲入的是java這個命令。 這個命令說明, 我們首先啓動的是一個叫做java的程序, 這個java程序在運行起來之後就是一個JVM進程實例。 

上面的命令執行流程是這樣的:

    java命令首先啓動虛擬機進程,虛擬機進程成功啓動後,讀取參數HelloWorld”,把他作爲初始類加載到內存,對這個類進行初始化和動態鏈接,然後從這個類的main方法開始執行。也就是說我們的.class文件不是直接被系統加載後直接在cpu上執行的,而是被一個叫做虛擬機的進程託管的。首先必須虛擬機進程啓動就緒,然後由虛擬機中的類加載器加載必要的class文件,包括jdk中的基礎類(如StringObject等),然後由虛擬機進程解釋class字節碼指令,把這些字節碼指令翻譯成本機cpu能夠識別的指令,才能在cpu上運行。 從這個層面上來看,在執行一個所謂的java程序的時候,真真正正在執行的是一個叫做Java虛擬機的進程,而不是我們寫的一個個的class文件。這個叫做虛擬機的進程處理一些底層的操作,比如內存的分配和釋放等等。我們編寫的class文件只是虛擬機進程執行時需要的“原料”。這些“原料”在運行時被加載到虛擬機中,被虛擬機解釋執行,以控制虛擬機實現我們java代碼中所定義的一些相對高層的操作,比如創建一個文件等,可以將class文件中的信息看做對虛擬機的控制信息,也就是一種虛擬指令。

虛擬機的三個主要功能系統

類加載器子系統:

    這個子系統用來在運行時根據需要加載類。在Java虛擬機執行過程中,只有他需要一個類的時候,纔會調用類加載器來加載這個類,並不會在開始運行時加載所有的類。就像一個人,只有餓的時候纔去吃飯,而不是一次把一年的飯都吃到肚子裏。一般來說,虛擬機加載類的時機,在第一次使用一個新的類的時候。

執行引擎子系統:

    由虛擬機加載的類,被加載到Java虛擬機內存中之後,虛擬機會讀取並執行它裏面存在的字節碼指令。虛擬機中執行字節碼指令的部分叫做執行引擎。

 垃圾收集子系統:

    Java虛擬機會進行自動內存管理。具體說來就是自動釋放沒有用的對象,而不需要程序員編寫代碼來釋放分配的內存。這部分工作由垃圾收集子系統負責

虛擬機的內存結構

    虛擬機的運行,必須加載class文件,並且執行class文件中的字節碼指令。它做這麼多事情,必須需要自己的空間。

    加載的字節碼,需要一個單獨的內存空間來存放;

    線程的執行,也需要內存空間來維護方法的調用關係;

    存放方法中的數據和中間計算結果;

    創建對象,創建的對象需要一個專門的內存空間來存放。

1、程序計數器:(線程私有)

    每個線程擁有一個程序計數器,在線程創建時創建,指向下一條指令的地址執行本地方法時,其值爲undefined。

說的通俗一點,我們知道,Java是支持多線程的,程序先去執行A線程,執行到一半,然後就去執行B線程,然後又跑回來接着執行A線程,那程序是怎麼記住A線程已經執行到哪裏了呢?這就需要程序計數器了。因此,爲了線程切換後能夠恢復到正確的執行位置,每條線程都有一個獨立的程序計數器,這塊兒屬於“線程私有”的內存。

2、Java虛擬機棧:(線程私有)

每個方法被調用的時候都會創建一個棧幀,用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息。局部變量表存放的是:編譯期可知的基本數據類型、對象引用類型。

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

在Java虛擬機規範中,對這個區域規定了兩種異常情況:

  (1)如果線程請求的棧深度太深,超出了虛擬機所允許的深度,就會出現StackOverFlowError(比如無限遞歸。因爲每一層棧幀都佔用一定空間,而 Xss 規定了棧的最大空間,超出這個值就會報錯)

  (2)虛擬機棧可以動態擴展,如果擴展到無法申請足夠的內存空間,會出現OOM

3、本地方法棧:

(1)本地方法棧與java虛擬機棧作用非常類似,其區別是:java虛擬機棧是爲虛擬機執行java方法服務的,而本地方法棧則爲虛擬機執使用到的Native方法服務

(2)Java虛擬機沒有對本地方法棧的使用和數據結構做強制規定,Sun HotSpot虛擬機就把java虛擬機棧和本地方法棧合二爲一。

(3)本地方法棧也會拋出StackOverFlowError和OutOfMemoryError。

4、Java堆:即堆內存(線程共享)

(1)堆是java虛擬機所管理的內存區域中最大的一塊,java堆是被所有線程共享的內存區域,在java虛擬機啓動時創建,堆內存的唯一目的就是存放對象實例幾乎所有的對象實例都在堆內存分配。

(2)堆是GC管理的主要區域,從垃圾回收的角度看,由於現在的垃圾收集器都是採用的分代收集算法,因此java堆還可以初步細分爲新生代和老年代

(3)Java虛擬機規定,堆可以處於物理上不連續的內存空間中,只要邏輯上連續的即可。在實現上既可以是固定的,也可以是可動態擴展的。如果在堆內存沒有完成實例分配,並且堆大小也無法擴展,就會拋出OutOfMemoryError異常。

5、方法區:(線程共享)

(1)用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

(2)Sun HotSpot虛擬機把方法區叫做永久代(Permanent Generation),方法區中最重要的部分是運行時常量池。

6、運行時常量池:

(1)運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時就會拋出OutOfMemoryError異常。 

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