一、JVM生命周期
Java虛擬機的生命周期 一個運行中的Java虛擬機有著一個清晰的任務:執行Java程序。程序開始執行時他才運行,程序結束時他就停止。你在同一台機器上運行三個程序,就會有三個運行中的Java虛擬機。 Java虛擬機總是開始於一個main()方法,這個方法必須是公有、返回void、直接受一個字符串數組。在程序執行時,你必須給Java虛擬機指明這個包換main()方法的類名。 Main()方法是程序的起點,他被執行的線程初始化為程序的初始線程。程序中其他的線程都由他來啟動。Java中的線程分為兩種:守護線程 (daemon)和普通線程(non-daemon)。守護線程是Java虛擬機自己使用的線程,比如負責垃圾收集的線程就是一個守護線程。當然,你也可以把自己的程序設置為守護線程。包含Main()方法的初始線程不是守護線程。 只要Java虛擬機中還有普通的線程在執行,Java虛擬機就不會停止。如果有足夠的權限,你可以調用exit()方法終止程序。
二、Java代碼編譯和執行的整個過程
開發人員編寫Java代碼(.java文件),然後將之編譯成字節碼文件(.class文件),再然後字節碼被裝入內存,一旦字節碼進入虛擬機,它就會被解釋器解釋執行,或者是被即時代碼發生器有選擇的轉換成機器碼執行。
(1)Java代碼編譯是由Java源碼編譯器來完成,也就是Java代碼到JVM字節碼的過程。
源代碼、詞法分析器、語法分析器、語義分析器、字節碼生成器、JVM字節碼
(2)Java字節碼的執行是由JVM執行引擎來完成的。
JVM字節碼、字節碼解釋器
JVM字節碼、機器無關優化、機器相關優化、寄存器分配器、目標代碼生成器、目標代碼
Java代碼編譯和執行的整個過程包含三個重要的機制為:java源碼編譯機制、類加載機制、類執行機制
1.Java源碼編譯機制(分析和輸出到符號表、注解處理、語義分析和生成class文件)
最後生成的class文件由以下部分組成:
結構信息:包括class文件格式版本號及各部分的數量與大小的信息
元數據:對應於Java源碼中聲明與常量的信息。包含類/繼承的超類/實現的接口的聲明信息、域與方法聲明信息和常量池
方法信息:對應於Java源碼中語句和表達式對應的信息。包含字節碼、異常處理器表、求值棧與局部變量區的大小、求值棧的類型記錄、調試符號信息
2.類加載機制
每一個Java虛擬機都有一個類加載器子系統,負責加載程序中的類型(類和接口),並賦予唯一的名字。每一個Java虛擬機都有一個執行引擎負責執行被加載類中包含的指令。
JVM的類加載是通過ClassLoader及其子類來完成的
啟動類加載器(Bootstrap ClassLoader):負責加載$JAVA_HOME中jre/lib/rt.jar裡所有的class,由C++實現,不是ClassLoader子類
擴展類加載器(Extension ClassLoader):負責加載java平台中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包
應用程序加載器(App ClassLoader):負責加載啟動參數中指定的classpath的jar包及目錄中class
用戶自定義加載器(Custom ClassLoader):屬於應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規范自行實現ClassLoader
加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視為已加載此類,保證此類只所有ClassLoader加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗 試加載此類。
加載:
1. 通過“類全名”來獲取定義此類的二進制字節流
2. 將字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構
3. 在java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口。
三、類執行機制
JVM是基於堆棧的虛擬機。JVM為每個新創建的線程都分配一個堆棧.也就是說,對於一個Java程序來說,它的運行就是通過對堆棧的操作來完成的。堆棧以幀為單位保存線程的狀態。JVM對堆棧只進行兩種操作:以幀為單位的壓棧和出棧操作。
棧幀由三部分組成:局部變量區,操作數棧和幀數據區。
JVM執行class字節碼,線程創建後,都會產生程序計數器(PC)和棧(Stack),程序計數器存放下一條要執行的指令在方法內的偏移量,棧中存放一個個棧幀,每個棧幀對應著每個方法的每次調用,而棧幀又是有局部變量區和操作數棧兩部分組成,局部變量區用於存放方法中的局部變量和參數,操作數棧中用於存放方法執行過程中產生的中間結果。
二、java垃圾回收機制
典型的垃圾收集算法
(1) Mark-Sweep(標記-清除)算法:這是最基礎的垃圾回收算法,之所以說它是最基礎的是因為它最容易實現,思想也是最簡單的。標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所占用的空間。具體過程如下圖所示:
從圖中可以很容易看出標記-清除算法實現起來比較容易,但是有一個比較嚴重的問題就是容易產生內存碎片,碎片太多可能會導致後續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。
(2)Copying(復制)算法:為了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。具體過程如下圖所示:
這種算法雖然實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。很顯然,Copying算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那麼Copying算法的效率將會大大降低。
(3)Mark-Compact(標記-整理)算法: 為了解決Copying算法的缺陷,充分利用內存空間,提出了Mark-Compact算法。該算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。具體過程如下圖所示:
(4) Generational Collection(分代收集)算法:
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據對象存活的生命周期將內存劃分為若干個不同的區域。一般情況下將堆區劃分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點采取最��合的收集算法。
目前大部分垃圾收集器對於新生代都采取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要復制的操作次數較少,但是實際中並不是按照1:1的比例來劃分新生代的空間的,一般來說是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象復制到另一塊Survivor空間中,然後清理掉Eden和剛才使用過的Survivor空間。
而由於老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。
注意,在堆區之外還有一個代就是永久代(Permanet Generation),它用來存儲class類、常量、方法描述等。對永久代的回收主要回收兩部分內容:廢棄常量和無用的類。
兩個重要方法:
System.gc()方法:使用System.gc()可以不管JVM使用的哪一種垃圾回收的算法,都可以請求Java的垃圾回收。
Finalize()方法:在JVM垃圾回收器收集一個對象之前,一般要求程序調用適當的方法釋放資源,但在沒有明確釋放資源的情況下,Java提供了缺省機制來終止該對象釋放資源,這個方法就是finalize()。它的原型就是protect void finalize() throws Throws
Throwable 在finalize()方法返回之後,對象消失,垃圾收集開始執行。原型中的throws
Throwable表示它可以拋出任何類型的異常。
之所以要使用finalize(),是存在著垃圾回收器不能處理的特殊情況:如打開的文件資源不屬於垃圾回收器的回收范圍。
此外:
(1)每個對象只能調用finalize( )方法一次。如果在finalize( )方法執行時產生異常(exception),則該對象仍可以被垃圾收集器收集。
對象在JVM堆區的狀態:
(1) 可觸及狀態:程序中還有變量引用,那麼此狀態為可觸及狀態。
(2) 可復活狀態:當程序中已經沒有變量引用這個對象,那麼此對象由可觸及狀態轉為可復活狀態。GC線程將在一定的時間准備調用此對象的finalize()方法,finalize方法內的代碼有可能將對象轉為可觸及狀態,否則對象轉化為不可觸及狀態。
(3) 不可觸及狀態:只有當對象處於不可觸及狀態時,GC線程才能回收此對象的內存。