顯示具有 [ JVM 應用 ] 標籤的文章。 顯示所有文章
顯示具有 [ JVM 應用 ] 標籤的文章。 顯示所有文章

2012年7月4日 星期三

[ JVM 應用 ] Class 類別檔結構

前言 : 
程式編譯的結果從本地機器碼轉變為位元組碼, 是儲存格式的一小步, 卻是程式設計語言發展的一大步. Java 剛誕生時提出一個口號 "Write Once, Run Anywhere". 透過需將 .java 編譯成位元組編碼的 .class, 再透過不同平台的 JVM 載入與執行, 從而實現程式的 "一次編寫, 到處執行". 而各種不同平台的 JVM 與所有平台都統一使用的程式儲存格式 - 位元組編碼 (ByteCode) 是構成平臺無關性的基石. 


Class 類別檔的結構 : 
Class 檔是一組以 8 位元單位的二進位資料流, 各個資料項目嚴格按照順序緊湊的排列在 Class 檔中. 根據 Java JVM 規範, Class 檔案採用一種類似 C 語言結構體的伪結構來儲存, 這種伪結構中只有兩種資料類型 : 無符號數和表, 而無符號數屬於基本的資料結構, 以 u1, u2, u4, u8 來分別代表 1, 2, 4, 8 個位元組的無符號數. 

表是由多個無號數或其它表作為資料項目構成的複合型資料類型, 所有的表慣性會以 "_info" 結尾. 表用於描述有層次關係的複合結構的資料, 整個 Class 本質上就是一張表, 由下面的資料項目構成 : 
 

無論是無符號數或是表, 當需要描述同一類型但數量不定的多個資料時, 經常會使用一個前置的容量計數器加若干個連續資料項目的形式, 這時候稱一系列連續的某一類型資料為某一類型的集合. 在 上表中的資料項目, 無論是順序還是數量, 都是被嚴格限定的, 接下來我們來看看表中幾個資料項目的具體意義. 

Magic Number 與 Class 檔的版本 : 
每個 Class 檔的頭 4 個位元組稱為 Magic Number, 它的唯一作用是用於確定這個檔案是個合法可以被 JVM 接受的 Class 檔. 而 Class 文件 Magic Number 的值為 "0xCAFEBABE", 這個數值在 Java 還被稱作 "Oak" 語言時就已經確定下來. 緊接著 Magic Number 的 4 個位元組儲存的是 Class 檔的版本號 : 第 5 和 第 6 個位元組是次版本號 (Minor Version), 第 7 還有 第 8 個位元組是主版本 (Major Version). Java 的版本號是從 45 開始. JDK 1.1 後每個 JDK 大版本發佈主版本向上加 1 ( JDK 1.0~1.1 使用了 45.0 ~ 45.3 的版本號). 高版本的 JDK 能向下相容以前的版本 Class, 但不能執行以後版本的 Class 檔. 最新的 JDK 版本 1.7 產生的 Class 檔版本號值為 51.0. 接著為了講解, 我們用下面代碼編譯成的 Class 檔進行說明 : 
  1. package ch06;  
  2.   
  3. public class Ex6_1 {  
  4.     private int m;  
  5.       
  6.     public int inc(){return m+1;}  
  7. }  
下圖為使用Hex 編輯器打開上面編譯後的 Class 檔結果, 可以清楚看見頭 4 個位元組是 16 進制的 0xCAFEBABE, 代表次版本號的 0x0000 與 主版本號的 0x0032 (十進位的 50), 該版號說明這個 Class 檔是可以被 JDK 1.6 以上的 JVM 執行 : 
 

下表列出從 JDK 1.1 到 1.7 之間, 主流 JDK 編譯出來的預設與可支援的 Class 檔版本號 : 
 

常數池 : 
緊接著版本訊息之後是常數池入口, 常數池是 Class 檔結構中占用空間最大的資料項目之一. 由於常數池中常數的數量是不固定的, 所以在常數池的入口需要放置一項 u2 類型的資料, 代表常數池容量計數值 ( constant_pool_count ). 這個容量計數是從 1 而不是 0 開始, 參考下圖 : 
 

這代表常數池中有 21 項常數, 索引值為 1~22. 制定 Class 檔案格式規範時, 將第 0 項常數空出來是有特殊作用, 用途是為了滿足後面某些指向常數池的索引值的資料在特定情況下需要表達 "不引用任何一個常數池專案" 的意思, 這種情況就可以把索引值設為 0 來表示. Class 檔案結構中只有常數池的容量計數是從 1 開始, 對於其他集合類型, 包括介面索引, 欄位表集合等的容量計數都與一般習慣相同, 是從 0 開始. 常數池中主要存放兩大類常數 : 字面量 (Literal) 和 符號參考 (Symbolic References). 字面常量比較接近 Java 語言層面的常數概念, 如內容字串或被聲明為 final 的常數等. 而符號引用則屬於編譯原理的概念, 包括了下面三類常數 : 
* 類別和介面的全限定名 (Full Qualified Name) 
* 欄位的名稱和描述符 (Descriptor) 
* 方法的名稱 
Java 程式碼在進行 javac 編譯的時候並不像 C++ 那樣有 "link" 這一步驟, 而是 JVM 載入 Class 檔的時候進行動態的連接. 也就是說在 Class 檔中不會保留各個方法與欄位的最終記憶體布局資訊, 因此這些欄位與方法的符號參考不經過轉換是無法直接被 JVM 使用的. 當 JVM 執行時, 需要從常數池獲得對應的符號參考, 在類別新增或執行時解析並翻譯到具體的記憶體位址中.

常數池的每一項常數都是一個表, 共有 11 種結構. 這 11 種表都有一個共同特點, 就是表開始的第一位是一個 u1 類型的標誌位元 (tag, 取值為 1 至 12, 缺少標誌為 2 的資料類型), 代表當前這個常數屬於哪種常數類型, 11 種常數類型所代表的具體說明如下表 : 
 

之所以說常數池是最繁瑣的資料, 是因為這 11 種常數類型各自均有自己的結構. 回頭參考上面表到偏移位置 0x0000000A 是 0x07 可以知道這個常數屬於 CONSTANT_Class_info 類型 : 
 

此類型的常數代表一個類別或介面的符號參考, 它的結構如下說明 : 
 

tag 是標誌位元說明常數類型 ; name_index 是一個索引值, 它指向常數池中的一個 CONSTANT_Utf8_info 類型的常數, 此常數代表這個類 (或介面) 的全限定名, 這裡的 name_index (偏移位置 : 0x0000000B) 為 0x0002, 即指向常數池的第二項常數 : 
 

繼續從上表知道第二個常數類型 (偏移量 : 0x0000000D) 0x01 是 CONSTANT_Utf8_info 類型的常數, 而該類型結構如下表 : 
 

length 說明這個 UTF-8 編碼字串長度是多少位元組, 它後面緊接長度為 length 位元組的連續資料是一個使用 UTF-8 縮略編碼表示的字串. 因為 Class 檔中的方法, 欄位等都需要參考CONSTANT_Utf8_info 類型常數來描述名稱, 並且該類型最大長度也就是 Java 中方法和欄位名的最大長度 = 2^16 = 65535. 所以 Java 程式中如果定義了超過 64KB 英文字元的變數或方法, 將會編譯失敗. 底下為對應欄位標示 : 
 

到此為止, 我們分析了 Class 檔中 21 個常數中的前兩個, 其餘的 19 個常數都可以透過類似方法計算出來. 為了方便分析類別檔, 已經有一個現成的工具可以幫我們快速了解類別檔的資訊 : javap. 底下為其執行結果範例 : 

2012年7月2日 星期一

[ JVM 應用 ] JVM 性能監控與故障處理工具 - JDK 的視覺化工具

前言 : 
除了剛剛介紹 JDK 上豐富的命令列工具外, 還有兩個功能強大的視覺化工具 : JConsole 和 VisualVM. 其中 JConsole 在 JDK 1.5 就已經提供的 JVM 監控工具, 而 VisualVM 是在 JDK 1.6 Update7 中才首次發布, 現在已經成為主力推動的多合一故障處理工具, 並且已經從 JDK 中分離出來成為可以獨立開發的開放原始碼專案. 

JConsole - Java 監視與管理主控台 : 
JConsole (Java Monitoring and Management Console) 是一款基於 JMX 的視覺化監視與管理工具. 

- 啟動 JConsole 
透過 JDK/bin 目錄下的 "jconsole.exe" 啟動 JConsole 後, 將自動搜尋出本機執行的所有 JVM 進程 (透過 jps), 如下圖所示, 你可以選擇其中一個進程開始進行監控 : 
 

選擇一個 LVM 進程後, 雙點擊可以進入管理介面 : 
 

- 記憶體監控 
"Memory" 頁籤相當於視覺化的 jstat 命令, 用於監視收集器管理的 JVM 記憶體 (Java Heap 與 永久代) 的變化趨勢. 我們可以使用下面代碼並執行來體驗一下它的監視功能 : 
  1. package ch04;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5.   
  6. public class Ex4_6 {  
  7.     static class OOMObject{  
  8.         public byte[] placeholder = new byte[64*1024];  
  9.     }  
  10.   
  11.     public static void fillHeap(int num) throws InterruptedException  
  12.     {  
  13.         List list = new ArrayList();  
  14.         for(int i=0; i
  15.         {  
  16.             Thread.sleep(50);  
  17.             list.add(new OOMObject());  
  18.         }  
  19.         System.gc();  
  20.         Thread.sleep(2000);  
  21.     }  
  22.       
  23.     /** 
  24.      * Goal : 以 64KB/50ms 速度往 Java Heap 填充資料, 共填充 1000 次. 
  25.      * VM Args : -Xms100M -Xmx100M -XX:+UseSerialGC 
  26.      * @param args 
  27.      */  
  28.     public static void main(String[] args) throws Exception{  
  29.         fillHeap(1000);       
  30.         Thread.sleep(10000);  
  31.     }  
  32. }  
在下圖可以看到記憶體持續成長而形成一條向上的平滑曲線. 另外在進行 System.gc() 後你會發現記憶體沒有減少, 主要是因為 list 物件還參考著分配的記憶體 (因為 age 夠老, 已經都在老年代), 如果你將 System.gc() 移到 main 方法呼叫 fillHeap() 之後, 就可以看到記憶體被成功回收 : 
 

- Threads 監控 
"Threads" 標籤功能相當於視覺化的 "jstack" 命令, 遇到 Threads 停頓時候可以使用這個功能進行監控分析. 前面講解 jstack 命令有提到過 Thread 長時間停頓原因主要有 : 等待外部資源, 閉環, 鎖等待. 透過下面代碼分別展示一下這幾種狀況 : 
  1. package ch04;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.InputStreamReader;  
  5.   
  6. public class Ex4_7 {  
  7.     public static void createBusyThread()  
  8.     {  
  9.         Thread thread = new Thread(new Runnable(){  
  10.             @Override  
  11.             public void run()  
  12.             {  
  13.                 while(true); // 閉環.  
  14.             }  
  15.         }, "testBusyThread");  
  16.         thread.start();  
  17.     }  
  18.       
  19.     public static void createLockThread(final Object lock)  
  20.     {  
  21.         Thread thread = new Thread(new Runnable(){  
  22.             @Override  
  23.             public void run()  
  24.             {  
  25.                 synchronized(lock)  
  26.                 {  
  27.                     try  
  28.                     {  
  29.                         lock.wait();  // 鎖等待  
  30.                     }  
  31.                     catch(InterruptedException e){e.printStackTrace();}  
  32.                 }  
  33.             }  
  34.         }, "testLockThread");  
  35.         thread.start();  
  36.     }  
  37.   
  38.     /** 
  39.      * Goal : 展示  Thread 停頓原因 - 閉環, 鎖等待. 
  40.      * @param args 
  41.      */  
  42.     public static void main(String[] args) throws Exception{  
  43.         BufferedReader br = new BufferedReader(new InputStreamReader(System.in));  
  44.         br.readLine();  
  45.         createBusyThread();  
  46.         br.readLine();  
  47.         Object obj = new Object();  
  48.         createLockThread(obj);  
  49.     }  
  50. }  
執行上面程式後, 使用 JConsole 監控該程式並選擇 "Threads" 頁籤後, 選擇 "main" Thread. 如下圖所示, 堆疊追蹤顯示 BufferedReader 在 readByte 方法中等待 System.in 的鍵盤輸入, 這時候 Thread 為 Runnable 狀態, Runnable 狀態進程會被分配執行時間, 但 readByte 方法檢查到流沒有更新時會立刻歸還執行權杖, 這種等待只消耗很少的 CPU 資源 : 
 

接著在執行 Console 下任意輸入一鍵讓程式往下走, 接著監控 testBusyThread 線程, 如下圖所示, testBusyThread 一直在執行空循環, 從堆疊追蹤中可以看到該線程一直停留在程式碼第 13 行的位置 (while(true)). 這時候線程為 Runnable 狀態, 而沒有歸還執行權杖的動作, 會在空循環上用盡全部的執行時間直到線程切換, 這種等待會耗費較多的 CPU 資源 : 
 

接著在執行 Console 下任意輸入一鍵讓程式往下走, 接著監控 testLockThread 線程. 下圖顯示線程在等待 lock 物件的 notify 或 notifyAll 方法的出現, 線程此時處在 WAITING 狀態, 在被喚醒前不會被分配執行時間 : 
 

testLockThread 線程處於正常的活鎖狀態, 只要 lock 對象的 notify() 或 notifyAll() 方法被呼叫, 這個 線程 便能啟動並繼續執行. 在 JDK 命令列工具 有一個死鎖範例, 執行後再用 JConsole 監控 : 
 

可以發現線程 Thread-1, Thread-2 處於 Block 狀態, 接著點擊 "Detect Deadlock" 可以檢查已經死鎖的線程 : 
 

VisualVM - 多合一故障處理工具 : 
VisualVM (All-in-One Java Troubleshooting Tool) 是目前為止隨 JDK 發布的功能最強大的執行監控與故障處理工具. "All-in-One" 意味它除了執行監控, 故障處理外, 還提供了很多其他 面的功能. 如性能分析(Profiling). 而且 VisualVM 還有一個很大的優點 : 不需要被監視的程式基於特殊 Agent 執行, 因此它對應用程式的實際性能影響很小, 使得它可以直接執行在應用環境中. 

- 下載執行 
VisualVM 在較新的 JDK 已經不隨之分布, 如果要使用需要自行下載, 下載後解壓縮可以在 bin 目錄下找到 "visualvm.exe", 執行後經過初始設定可以進入到下面的 GUI 介面 (我使用版本為 1.3.4): 
 

- 分析程式性能 
VisualVM 在 JDK 1.6 update7 中才首次出現, 但不意味它只能使用在 JDK 1.6 上的程式, 它具備很強向下相容能力, 這對無數已經處於維護狀態的老專案很有意義. 當然並非所有功能都能完美向下相容, 主要特性的相容性如下表所示 : 
 

在開啟 VisualVM 後, 可以在左邊的 Applications 面板找到要監測的 JVM 進程 : (底下對 LVMID=7640 進行監測
 

接著可以在右方面板點擊 "Sampler" 頁籤, 檢視 CPU 與 記憶體的使用狀況 : 
 
(CPU Usage

 
(Memory Usage)

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...