1. 程式人生 > >從上帝視角看Java如何執行

從上帝視角看Java如何執行

JVM記憶體結構

可以看出JVM從巨集觀上可以分為 ‘內部’  及 ‘外部’  兩個部分(便於記憶理解):

‘內部’包含:執行緒共享(公有)資料區 和 執行緒隔離(私有)資料區

‘外部’包含:類載入子系統、垃圾回收器、執行引擎、本地庫介面、本地方法庫

 

以上部件構成了整個jvm,接下來我們一個一個零件拆開了看。

 

class檔案

  一個java檔案會通過編譯工具(javac)編譯成class位元組碼檔案,通過jvm進行載入執行。因為jvm遮蔽了底層作業系統的差異(平臺無關性),所以一次編譯到處執行。

 

類載入子系統

  類載入子系統:負責查詢和裝載class檔案,將其中的二進位制資料載入到jvm中。

位元組碼 --> 載入 -->  驗證 --> 準備 --> 解析  --> 初始化

載入:通過類的完全限定名找到類檔案所在位置,根據其中的位元組碼建立java.lang.Class物件,所以才會說萬物皆物件,我們也可以繼承ClassLoader,重寫findClass方法來自定義實現類載入器。預設情況下我們都使用AppClassLoader

驗證:確保載入的位元組碼的是否符合虛擬機器的要求,是java提供的一種自我保護機制,不讓其危害虛擬機器安全。其主要包括四種驗證,位元組碼驗證、檔案格式驗證,元資料驗證、符號引用驗證。

準備:為類變數分配地址和初始化值,類變數會分配到方法區(元空間)中,這裡的初始化是指該資料型別的預設初始值,例如int對應的是0,long對應的0L,只有在初始化時才會動顯示賦值

解析:把類中的二進位制資料中的符號引用轉換為直接引用;例如我們通過user.getInfo();這裡的.getInfo()就是符號引用,在解析階段會將它指向真正的記憶體位置,這就是直接引用

初始化:主要為類的靜態變數賦予正確的值,比如int num = 10; 這裡num的值會從準備階段的0變為10;並且若該類有父類,會對其進行初始化操作;如果類中有初始化語句,系統會按照順序進行初始化

 

雙親委派模式

雙親委派:自底向上檢查是否載入成功,自頂向下嘗試載入。

當一個類載入器收到類載入請求,它不會自己進行載入,而是將該請求丟給父類載入,如果父類還存在父類,則會依次向上請求,直到到達頂級載入器,如果父類載入器能載入完成就返回載入成功,否則子類載入器才會自己嘗試載入。

System.out.println(Test.class.getClassLoader());
System.out.println(Test.class.getClassLoader().getParent());
System.out.println(Test.class.getClassLoader().getParent().getParent());
System.out.println(String.class.getClassLoader());

通過程式碼驗證,可以很輕鬆的瞭解 AppClassLoader -> ExtClassLoader -> BootstrapClassLoader 這三層的關係。

 

類載入的三種方式

1. new關鍵字載入

  User user = new User();

    靜態載入,在執行時候通過new關鍵字建立類例項

2. Class.forName()載入

  Class clazz = Class.forName(“User”);  
  Object user=clazz.newInstance();

    動態載入,通過Class.forName()來載入類,然後呼叫類的newInstance()方法例項化物件

3. ClassLoader 例項的 loadClass() 方法

  Class clazz = classLoader.loadClass(“User”); 
  Object user=clazz.newInstance();

    動態載入,可通過繼承ClassLoader實現自定義類載入器

 

執行緒私有和執行緒公有

 

JVM記憶體區從巨集觀上可以分為 執行緒私有 和 執行緒公有 兩塊。

執行緒私有部分

  這部分沒有執行緒安全問題,隨著執行緒執行結束而結束;包含程式計數器、虛擬機器棧、本地方法棧三個部件。

程式計數器:

   程式計數器也叫PC暫存器,作用是cpu進行切換的時候,指向當前時刻需要獲取指令的位置。

   特點:

  1. 執行緒私有
  2. 一塊較小的區域
  3. 記錄程式執行的位置
  4. 不存在記憶體溢位OutOfMemoryError

虛擬機器棧:

  棧資料結構實現,入口和出口只有一個,稱之為入棧和出棧,先進後出(FILO)

  棧的作用主要是執行方法,先執行的方法在最下面,然後依次放入,方法執行完畢之後從上往下依次退出;所以方法執行就是壓棧,方法結束就是出棧(銷燬棧幀)。

public void start(){    
    say();    
    run();
}

 

虛擬機器棧如何執行

棧幀

  棧幀存在Java虛擬機器棧中,是虛擬機器棧中的單位元素。方法執行會建立棧幀,一個方法就是一個棧幀,一個棧幀分為四個部分:

1. 區域性變量表

         存放方法引數或者內部定義的一組變數列表;例如方法中宣告的物件:

  User user = new User();  //區域性變數user
2. 運算元棧

           執行位元組碼指令的時候使用,通俗的講就是方法的執行在運算元棧中進行,通過壓棧和出棧進行訪問

3. 動態連結

           Java執行期間是動態連結的,需要將指向方法的符號引用轉換為直接引用(記憶體地址);在類載入解析階段,將符號引用轉換為直接引用稱之為靜態解析。而此處正好就是動態連結

           user.getInfo();  //找到這個getInfo()方法的記憶體位置

4. 返回地址

         方法不管正常執行結束還是異常退出,需要返回方法被呼叫的位置

 

以上四個部分對應方法執行的過程。虛擬裡面包含很多個棧幀,每個方法對應一個棧幀。

將一個class檔案,通過bin/javap.exe檔案進行反彙編可以查看出以上四個部分。

棧溢位:當棧的深度大於虛擬機器允許會報StackOverflowError,-Xss可設定大小

/** 遞迴演示如何棧溢位 */
public static int num = 0;
public static void a(){    
    num++;    
    a();
}
public static void main(String[] args) {    
    try{        
        a();    
    }catch (Exception ex){        
        System.out.println("呼叫次數:"+num);   
    }
} 

記憶體溢位:當棧需要擴充套件而無法申請空間會報OutOfMemoryError

 

本地方法棧

    本地方法棧和虛擬機器棧類似,區別在於虛擬機器棧主要為jvm執行位元組碼服務,而本地方法棧為Native方法服務,即本地方法服務;所以本地方法棧也是一塊記憶體私有區域,與虛擬機器棧相同也有同樣的異常問題。

  特點:

  1. 與虛擬機器棧基本類似
  2. 區域在於本地方法棧為Native方法服務(windows下呼叫dll檔案)
  3. Sun HotSpot將虛擬機器棧和本地方法棧合併
  4. StackOverflowErrorOutOfMemoryError

 

執行緒公有部分

  這部分存線上程安全問題,平常我們所指的記憶體優化,溢位等問題都是需要關注這個區域。包含堆、方法區(也叫元空間)兩個部件。

方法區(元空間)

  類載入器載入類的時候,會將一些類的元資料資訊(位元組碼)儲存在這個區域,例如:類變數,靜態方法,普通方法等,方法區是執行緒共享的,多個執行緒能用到同一個類

  jdk1.7合併方法區到了堆裡面

  jdk1.8保留了方法區的概念,只不過實現方式不同,jdk1.8稱為元空間,與堆不相連,但是與堆共享實體記憶體,邏輯上可以認為是在堆中

 特點:

  1. 執行緒共享
  2. 儲存類資訊、常量、靜態變數、方法描述等資訊
  3. HotSpot虛擬機器中稱之為永久代
  4. GC很少回收這個區域
  5. 存在OutOfMemoryError,可以通過-XX:MaxPermSize設定大小

 

  堆中用於存放所有例項化物件和陣列,堆中資訊執行緒共享,所有jvm部件中分配記憶體中最大的區域,在虛擬機器啟動時就建立,垃圾回收器主要管理該區域,堆分為新生代(佔堆記憶體1/3)和老年代(佔堆記憶體2/3),新生代更細緻可以分為Eden、From Survivor、To Survivor空間,比例8:1:1 ;可以通過-Xmx、-Xms設定大小

在堆中產生了一個例項物件或陣列,可以在棧中宣告一個變數,用於指向堆中的物件,該變數的取值等於堆中物件的記憶體地址,所以我們在列印變數名的時候是一串記憶體地址

 Test test = new Test();
 System.out.println(test);  //輸出Test@1b6d3586

  萬物皆物件,當我們在實際開發中,建立了許多物件,為了防止記憶體洩露,java確保有效的使用記憶體,會由java虛擬機器自動垃圾回收器來管理;且把堆分為新生代和老年代進行管理

新生代與老年代

  新生代是Java物件出生的地方,是新物件分配記憶體的地方,大部分物件存活時間都不需要太久,這個區域會頻繁觸發MinorGC進行垃圾回收;
  而老年代存放的都是存活時間較久或者記憶體較大的物件,所以Full GC不會頻繁執行。

Minor GC

  發生在新生代中的垃圾回收機制,採用複製演算法(掃描存活物件,複製到一塊新記憶體空間中),From Survivor 和 to Survivor是相對的,也就是說Minor GC發生時,Eden區和其中一個Survivor區會把一些仍然存活的物件放置另外一個Survivor 區,然後清理Eden區和之前的Survivor 區,下次同理,當達到一定 ‘年齡’ 後,新生代會把物件放入老年代(每發生一次Minor GC增加1歲,預設15歲)

Full GC

  發生在老年代中的垃圾回收機制,採用標記-清除(標記存活的物件,清除未標記的物件,即需要回收的物件),因為老年代中的物件較穩定,所以發生Full GC的頻率相對Minor GC較少,但是一次回收的時間會比Minor GC更長