虛擬機
Java虛擬機(Java Virtual Machine)是用來屏蔽底層硬件的,它負責把字節碼(*.class)文件翻譯成機器碼,從而達到跨平臺的目的。
java源代碼先通過編譯器編譯成class文件,再經過特定的JVM(不同的操作系統下JVM不同)翻譯成操作系統能看懂的機器碼。因此JVM不侷限於Java語言,它面向的是一個字節碼規範,如果能把其它語言代碼(如PHP)編譯成正確的Class文件,JVM也是可以運行的。
Java虛擬機,顧名思義它是一個虛擬機,虛擬機和普通PC一樣能運行程序,PC機運行程序所必要的因素JVM也需要滿足。
內存結構
內存分五個部分:
- 堆內存
- 方法區
- 虛擬機棧
- 本地方法棧
- 程序計數器
內存分類
分成兩部分
線程私有部分:虛擬機棧、本地方法棧、程序計數器
線程共享部分:堆內存、方法區
堆內存
堆內存隨虛擬機創建而創建,是佔用內存最多的,也是Gc(垃圾回收器是一個運行在堆(heap)上的一個線程)回收的主要內存,它用來存放創建的對象。由於幾乎所有的對象都要在堆上創建,所以它是線程共享的,所有線程創建的對象都要在同一個堆上面(如果堆線程私有,那麼意味着每個線程都有自己的堆,首先會很浪費內存,再者這樣也會加大線程間通信的難度)。
堆內存細分成年輕代(Young Gen)和老年代(Old Gen),它們的比例是1比2,堆內存如果是30MB,年輕代佔10MB,老年代佔20MB。
年輕代細分爲Eden區,From區(Survivor0),To區(Survivor1),它們的比例是8:1:1,From和To只使用一個,另一個是作爲一個臨時容器使用的,因此只有90%的內存可以使用。
java線程創建一個對象時,會首選存到Eden區,如果內存不足則會觸發MinorGc(年輕代內存回收),Gc從Gc-root開始遍歷找到所有有引用的對象,第一次把它們複製到其中一個區,例如From區,並對每個對象進行計數。
第二次再觸發MinorGc時,再從Gc-root遍歷一次,把Eden和From區的有效對象移動到To區(因爲上一次是放到了From區),再清空Eden和From,並且把對象計數再加1,如此反覆,默認加到16的時候會移動到老年區(可以通過啓動參數修改)。
這樣做的好處是From和To中的內存都是連續的,不擔心碎片過多導致無法創建大對象的問題。
直到老年代內存不足時會觸發MajorGc,會對老年代進行垃圾回收,但是老年代只有一個內存塊,沒有臨時的(類似From和To),因此只能使用標記壓縮回收的方式進行回收,且老年代內存塊最大,因此效率較低,由於執行Gc時,會觸發Stop-The-World,線程不能訪問內存對象,導致客戶端響應慢。
還有一種Gc叫做fullGc,根據名稱可以知道它是既回收Young Gen又回收Old Gen的。
方法區
也叫靜態區,用來存放類信息、靜態變量、方法數據、方法代碼等。線程需要創建對象時,需要從這裏獲取信息,存放的都是在整個程序中永遠唯一的元素,因此它也是線程共享的。
虛擬機棧
棧是一種數據結構,不管它叫什麼棧,都是先入後出(FILO)的,都是用於存數據的。在這裏只是存了方法調用的臨時變量而已。
每個線程有一個私有的棧,隨着線程的創建而創建。棧裏面存着“棧幀”,每調用一個方法會創建一個棧幀,棧幀中存放了局部變量表(基本數據類型和對象引用)、操作數棧、方法出口等信息。棧的大小可以固定也可以動態擴展。當棧調用深度大於JVM所允許的範圍,會拋出StackOverflowError的錯誤(多是因爲遞歸造成)。
虛擬機棧是用來執行方法用的,當調用一個方法時,會創建一個棧幀,棧幀壓入虛擬機棧頂,如果此方法又調用另一個方法,則會再創建一個棧幀壓入棧頂,如此循環,因此在查看代碼拋出異常時是反着的,從下向上纔是調用的順序。
public class Test {
public static void main(String [] args) {
A();
}
public static void A() {
B();
}
public static void B() {
C();
}
public static void C() {
D();
}
public static void D() {
}
}
本地方法棧
用來調用Native方法的,如果是純java則不需要使用這部分。
程序計數器
用來記錄當前線程正在執行的行號,因爲多線程是需要搶佔CPU資源的,當線程被掛起之後在下一次喚醒之前,需要有一個變量來記錄當前執行的位置,以便再次搶到CPU資源後繼續執行。