1. 前言-為何要再談JVM
很多人認為,分佈式Java應用看上去好像和Java虛擬機沒什麼太多關註的東西,別說分佈式系統瞭,就是一個單機JavaSE系統也不用將JVM學得透透的,有個JVM執行不就行瞭嗎。據筆者的經驗回答則是,在大型分佈式系統,尤其是雲計算服務平臺,SAAS也好,PAAS也罷,要求編寫的應用必須要高效,你就當你運行的是一個配置很小的機器上,要求資源比較苛刻。所以瞭解,甚至再學習JVM相關知識還是十分必要的。之前筆者寫過一篇關於JVM加載類的文章http://suhuanzheng7784877.iteye.com/blog/964784。後來發現這些還是不夠的,還是需要在總結一下JVM運行時的其他環節。對於我們開發分佈式的Java系統是有一種潛移默化的作用,主要用於自己回顧用。
2. JVM結構
JVM主要負責什麼工作呢,首先它負責將我們編寫的java文件編譯成為class文件;第二它負責加載已有的class文件;第三它還負責class的執行;第四JVM還負責內存的分配與回收,這也是為什麼大傢在寫代碼的過程中不用手工寫代碼分配資源和強制回收資源瞭,有個強勢的管傢——JVM為您做瞭;JVM還要和底層操作系統接口進行交互完成多線程資源的同步、並發機制,所以為什麼我們實現一個Runnable接口或者集成Thread就能完成多線程操作瞭,表面上看您除瞭編寫一些特殊的類什麼都沒做,實際上是JVM為您默默做瞭那麼多。
Sun,哦,不,筆者老是改不瞭口,Oracle的Java規范JVM如下:
Class文件被類加載器加載,JVM為其開辟內存空間,內存空間又分為四個大部分:方法區、堆區、棧區、本地方法棧區。JVM啟動優先級比較小的線程來運行垃圾回收器,時刻監視並回收開辟的內存區域。
3. JVM到底如何編譯Java代碼
運行class文件需要將java文件編譯成為class文件,JavaSDK如何編譯的呢?javac將java源代碼編譯class過程如下:
1:分析輸入到符號表:將java文件內容的字符串轉變為關鍵字序列,之後生成一個類似於樹狀結構的內容,叫做抽象語法樹。所謂輸入到符號表的意思,就是將類中出現的符號輸入類自身的符號表中等。所謂符號一般是指:父類、接口,因為要根據這些符號生成無參構造函數。
2:處理註解階段:在JDK1.5之前,JVM沒有這個階段,但是JDK1.5之後咱們在Java類文件中寫的註解需要JVM來進行處理、加強,至於註解保留到何時才生效,具體請參考http://suhuanzheng7784877.iteye.com/blog/1054838。
之後根據抽象語法樹中等等信息聯系到一起,進行有效性判斷,一些列的語義判斷,校驗,優化某些代碼,之後就是我們看到的class文件。class中除瞭字節碼,還包括瞭JVM執行class的相關信息:
結構信息:class的文件格式版本號以及各部分的數量與大小。
元數據:元數據代表類的繼承關系、接口、聲明信息、屬性、方法聲明信息、常量池信息。
方法信息:包括字節碼、異常處理表、局部變量區大小
我們利用反編譯工具反編譯一下以前自己寫過的class源碼,是不是發現確實JDK編譯此文件的時候和我們源代碼有些許出入,那是它為我們做瞭優化。
4. JVM到底如何執行Java代碼
JVM執行class文件有3種方式,第一是解釋執行、第二是編譯執行、第三就是反射執行。可能大傢對於第三個反射執行更為熟悉,因為這也是對大傢相對真真正正編代碼體驗過的,而前兩者都是由JVM給咱做的,所以聽上去就十分陌生。
1):解析執行
在JVM中解析執行專門執行那些執行頻率不高的代碼,解析執行內部原理十分類似於咱們學過的匯編語言,JVM用一些自己自定義的指令(匯編語言也有自己一些簡單的指令),來完成既有代碼的執行過程,說白瞭,到瞭最底層,面向對象還是要過程化的。
通過4條常用指令完成對類的解析和執行。Invokestatic負責調用static方法,invokevirtual負責調用實例對象方法,invokeinterface對應的是調用接口方法,invokespecial負責private方法的調用以及編譯源碼後(就是class)生成的構造方法。
如下一段代碼
Java代碼
import java.io.UnsupportedEncodingException;
public class UTest {
public static void main(String[] args) throws UnsupportedEncodingException {
String c = java.net.URLDecoder
.decode("%E4%BA%A7%E5%93%81%E6%8F%8F%E8%BF%B0%E5%9F%9F%E8%B6%8A%E7%95%8C","utf-8");
System.out.println(" " + c);
}
}
編譯成class後,使用javap –c UTest看字節碼內容
Java代碼
Compiled from "UTest.java"
public class UTest extends java.lang.Object{
public UTest();
Code:
0: aload_0//裝載局部變量第一個值到操作數棧
1: invokespecial #8; //Method java/lang/Object."<init>":()V//初始化構造器
4: return
public static void main(java.lang.String[]) throws java.io.UnsupportedEncoding
Exception;
Code:
0: ldc #19; //String %E4%BA%A7%E5%93%81%E6%8F%8F%E8%BF%B0%E5%9F%9F%E8%B
6%8A%E7%95%8C//將字符串值裝載到常量池中
2: ldc #21; //String utf-8//將字符串值裝載到常量池中
4: invokestatic #23; //Method java/net/URLDecoder.decode:(Ljava/lang/Str
ing;Ljava/lang/String;)Ljava/lang/String;
//調用靜態方法java.net.URLDecoder.decode
7: astore_1//將操作數棧中棧頂的值彈出放入局部變量區
8: getstatic #29; //Field java/lang/System.out:Ljava/io/PrintStream;
11: new #35; //class java/lang/StringBuilder
14: dup
15: ldc #37; //String
17:invokespecial #39; //Method java/lang/StringBuilder."<init>":(Ljava/la
ng/String;)V
20: aload_1
21:invokevirtual #42; //Method java/lang/StringBuilder.append:(Ljava/lang
/String;)Ljava/lang/StringBuilder;
24:invokevirtual #46; //Method java/lang/StringBuilder.toString:()Ljava/l
ang/String;
27:invokevirtual #50;//Method java/io/PrintStream.println:(Ljava/lang/St
ring;)V
30: return
}
估計大傢都看煩瞭,習慣瞭面向對象思維的大傢一看這種匯編似的語言確實頭疼,耐不下心。如果不是搞JVM開發的,其實瞭解一下就可以,畢竟瞭解一下JVM運行原理,對於我們寫代碼,無異於又過瞭一層思考。
解釋執行比較慢,但是呢,比較省資源,省內存,因為采用棧這種數據結構進行指令的收編和采集,運用,空間利用率可見是比較節儉的。
2):編譯執行:
為瞭提升性能,JDK還可以編譯執行,也叫做即時編譯器,通常是對執行頻率比較高的代碼進行及時編譯執行。編譯執行犧牲瞭部分空間資源,換來的是優化瞭字節碼的編譯實際代碼塊,就是說我們編寫的代碼在JVM編譯器看來是可優化的,所以它采用優化編譯,將原先的代碼進行一些調整,進而更好地利用JVM資源。編譯執行那個主要分為2類,一個是客戶端類型,屬輕量級;一類是服務端類型,屬於重量級。