標籤:div 方式 申請 類的方法 一個棧 包括 c++ 迴圈 關聯
我在寫程式的時候想過的以下這些問題,不知道大家是不是都是這樣:
1. 類是怎麼載入的,儲存在哪裡?類的Object Storage Service在哪裡,類和類對象怎麼關聯、對應的?
2. 方法存在哪裡?子類繼承父類之後覆蓋父類的方法之後JVM什麼機制執行子類or父類方法?
3. static變數和普通變數是放著一起嗎?
4. 為什麼共用變數要做線程同步控制,而函數內變數不用?
5. JVM什麼時候會做記憶體回收,怎麼收的?
6. JVM怎麼實現平台無關性的?做到一處編寫,到處啟動並執行?
下面就是我找了很多資料,寫了點程式搞明白這些問題,可能一篇檔案寫不完,會寫很多,最終一定是把所有問題都搞清楚。
1. Java類的編譯過程
機器碼:是機器語言的指令集體繫結構的表示方式。好比"加"在彙編中用add表示,類似的在這個中則是用1100表示(1100隻是舉例用,實際不是),作業系統看到1100就會執行add指令。
Java位元組碼:相信應該很多人都聽過位元組碼,JAVA不同於c/c++直接編譯成機器碼,而是編譯成位元組碼,位元組碼是平台無關的,它是介於Java和機器語言的中繼語言,JAVA位元組碼是Java代碼部署的最小單元。這也就解釋了上面的第6個問題,Java如何?平台無關性的,編寫好的Java位元組碼如果在window平台,window平台的JVM(準確說是JRE)會將位元組碼最後翻譯成符合window平台的機器碼,在linux平台JVM(準確說是JRE)會將位元組碼最後翻譯成符合linux平台的機器碼。
位元組碼長什麼樣呢?我寫了段小程式,如下所示:
class Test{ public static void main(String[] args){ Apple apple = new Apple("wo"); apple.eat("jige"); } }class Apple{ private String name; public Apple(String name){ this.name = name; } public void eat(String who){ System.out.println(who +" eat " + name + "‘ apple"); }}
java命令編譯後的位元組碼如下:
cafe babe 0000 0034 000f 0a00 0300 0c07000d 0700 0e01 0006 3c69 6e69 743e 01000328 2956 0100 0443 6f64 6501 000f 4c696e65 4e75 6d62 6572 5461 626c 6501 00046d61 696e 0100 1628 5b4c 6a61 7661 2f6c616e 672f 5374 7269 6e67 3b29 5601 000a536f 7572 6365 4669 6c65 0100 0954 6573742e 6a61 7661 0c00 0400 0501 0004 54657374 0100 106a 6176 612f 6c61 6e67 2f4f626a 6563 7400 2000 0200 0300 0000 00000200 0000 0400 0500 0100 0600 0000 1d000100 0100 0000 052a b700 01b1 0000 00010007 0000 0006 0001 0000 0001 0009 00080009 0001 0006 0000 002d 0002 0004 00000009 043c 053d 1b1c 603e b100 0000 01000700 0000 1200 0400 0000 0300 0200 04000400 0500 0800 0600 0100 0a00 0000 02000b
詐一看還以為是機器碼,但是不是,我們通過javap -c查看組合語言,如所示:
class Test { Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class Apple 3: dup 4: ldc #3 // String wo 6: invokespecial #4 // Method Apple."<init>":(Ljava/lang/String;)V 9: astore_1 10: aload_1 11: ldc #5 // String jige 13: invokevirtual #6 // Method Apple.eat:(Ljava/lang/String;)V 16: return}
以上就是平台無關性的位元組碼,Java 位元組碼指令集包括作業碼和運算元,上面的指令就是JVM編譯指令。
2. 類載入及運行過程
1. Test類載入:在編譯好在命令列敲java Test,系統會啟動一個jvm進程,jvm從classpath類Test所在路徑中找到Test.class位元組碼檔案,將Java的類資訊載入到運行時資料區的方法區中
2. 執行main:jvm找到Test的main方法,開始執行;
3. main方法執行 new Apple時,JVM發現沒有Apple類資訊,所以JVM馬上載入Apple類,在Apple類資訊放到方法區,初始化Apple類對象(Class extend Object)。
4. 載入完Apple類之後,JVM開始在堆上為Apple對象分配記憶體,然後調用建構函式初始化Apple執行個體,這個Apple執行個體持有指向方法區的Apple類的類型資訊的引用(方法表,java動態綁定的實現)。
5. 當調用apple.eat("jige") 的時候,JVM會根據apple引用找到Apple對象,然後在Apple對象中引用找到方法區Apple類的方法表,擷取eat() 函數的位元組碼的地址,線上程私人的線程棧中push棧幀,運行。
這個流程回答了上面第1個問題。
3. Java記憶體模型
1. Java虛擬機器在執行Java程式時會把記憶體分成若干個不同的資料區域,如所示,記憶體分成線程私人(紅色)的和線程共有的(綠色)。
2. 程式計數器
程式計數器(Program Counter Register)是一塊較小的記憶體空間,它的作用可以看做是當前線程所執行的位元組碼的行號指標。在虛擬機器的概念性模型裡(僅是概念性模型,各種虛擬機器可能會通過一些更高效的方式去實現),位元組碼解譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、線程恢複等基礎功能都需要依賴這個計數器來完成。由於Java 虛擬機器的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)只會執行一條線程中的指令。因此,為了線程切換後能恢複到正確的執行位置,每條線程都需要有一個獨立的程式計數器,各條線程之間的計數器互不影響,隔離儲存區 (Isolated Storage),我們稱這類記憶體地區為“線程私人”的記憶體。如果線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Natvie 方法,這個計數器值則為空白(Undefined)。此記憶體地區是唯一一個在Java 虛擬機器規範中沒有規定任何OutOfMemoryError 情況的地區。JVM的程式計數器和機器語言的PC程式計數器是不一樣的,PC程式計數器是CPU中的寄存器,儲存的是程式下一條指令的地址。
3. Java虛擬機器棧
與程式計數器一樣,Java虛擬機器棧(Java Virtual Machine Stacks)也是線程私人的,我覺得應該換個名,應該叫做線程棧,所以實際上是有多少個線程,就有多少個虛擬機器棧,虛擬機器棧生命週期和線程相同,線程產生時,虛擬機器棧隨之產生,線程銷毀時,虛擬機器棧也會銷毀。每個方法被執行時,同時會建立一個棧幀(Stack Frame),用於儲存局部變數表、運算元棧、指向當前方法所屬類的運行時常量池(方法區)的引用,方法返回地址push到棧中,執行完畢之後pop出棧。
3.1 局部變數表:用來儲存函數中的局部變數,包括函數中定義的非靜態變數和函數形參,基本變數直接存值,參考型別儲存指向對象的引用,局部變數表的大小在編譯器就可以確定其大小了,因此在程式執行期間局部變數表的大小是不會改變的。
3.2 運算元棧:一個線程執行方法的過程中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。因此可以這麼說,程式中的所有計算過程都是在藉助於運算元棧來完成的。
3.3 指向運行時常量池的引用,因為在方法執行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向運行時常量。
3.4 方法返回地: 當一個方法執行完畢之後,要返回之前調用它的地方,因此在棧幀中必須儲存一個方法返回地址。
4. 本地方法棧
本地方法棧與Java棧的作用和原理非常相似。區別只不過是Java棧是為執行Java方法服務的,而本地方法棧則是為執行本地方法(Native Method)服務的。在JVM規範中,並沒有對本地方發展的具體實現方法以及資料結構作強制規定,虛擬機器可以自由實現它。在HotSopt虛擬機器中直接就把本地方法棧和Java棧合二為一。也就是說本地方法也會建立棧幀,押棧出棧,線程私人。
5. 堆
在C語言中,堆這部分空間是唯一一個程式員可以管理的記憶體地區。程式員可以通過malloc函數和free函數在堆上申請和釋放空間。那麼在Java中是怎麼樣的呢?
Java中的堆是用來儲存物件本身的以及數組(當然,數組引用是存放在Java棧中的)。只不過和C語言中的不同,在Java中,程式員基本不用去關心空間釋放的問題,Java的記憶體回收機制會自動進行處理。因此這部分空間也是Java垃圾收集器管理的主要區域。另外,堆是被所有線程共用的,在JVM中只有一個堆。
6. 方法區
方法區是JVM中非常非常重要的地區,取名為方法區我覺得不是很恰當,應該存放的內容是以Class為單元的,存放了每個類的資訊(類名、方法資訊、欄位資訊)、運行時常量池(靜態變數、常量、字面量和符號引用),編譯後的代碼。
在JVM規範中,沒有強制要求方法區必須實現記憶體回收。很多人習慣將方法區稱為“永久代”,是因為HotSpot虛擬機器以永久代來實現方法區,從而JVM的垃圾收集器可以像管理堆區一樣管理這部分地區,從而不需要專門為這部分設計記憶體回收機制。不過自從JDK7之後,Hotspot虛擬機器便將運行時常量池從永久代移除了。
JVM-記憶體模型