一、基礎知識
1. JVM例項:JVM例項對應了一個獨立執行的java程式,它是程式級別。
2. JVM執行引擎例項:JVM執行引擎例項則對應了屬於使用者執行程式的執行緒,它是執行緒級別的。
3. JVM生命週期:
JVM例項的誕生:當啟動一個Java程式時,一個JVM例項就產生了。
JVM例項的執行: main()作為該程式初始執行緒的起點,任何其他執行緒均由該執行緒啟動。
JVM例項的消亡:當程式中的所有非守護執行緒都終止時,JVM才退出;若安全管理器允許,程式也可以使用Runtime類或者System.exit()來退出。
注: JVM內部有兩種執行緒:守護執行緒和非守護執行緒,main()屬於非守護執行緒,守護執行緒通常由JVM自己使用,java程式也可以標明自己建立的執行緒是守護執行緒。
任何一個擁有public static void main(String[] args)函式的class都可以作為JVM例項執行的起點
二、JVM結構
JVM可以由不同的廠商來實現。由於廠商的不同必然導致JVM在實現上的一些不同,然而JVM還是可以實現跨平臺的特性,這就要歸功於設計JVM時的體系結構了。
JVM體系結構包含三部分:
類載入器(Class Loader)子系統:類載入器(class loader)用來載入 Java 類到 Java 虛擬機器中。一般來說,Java 虛擬機器使用 Java 類的方式如下:Java 源程式(.java 檔案)在經過 Java 編譯器編譯之後就被轉換成 Java 位元組程式碼(.class 檔案)。類載入器負責讀取 Java 位元組程式碼,並轉換成java.lang.Class類的一個例項。每個這樣的例項用來表示一個 Java 類。通過此例項的 newInstance()方法就可以建立出該類的一個物件。實際的情況可能更加複雜,比如 Java 位元組程式碼可能是通過工具動態生成的,也可能是通過網路下載的。但第二次例項化一個類時,就從對應Class類newInstance(),不用每次都讀取.class檔案。
執行引擎(Execution Engine)
執行時資料區(Runtime Data Area):
Java程式執行過程
Java整個程式以及執行的過程相當繁瑣,本文通過一個簡單的程式來簡單的說明整個流程。
如下圖,Java程式從原始檔建立到程式執行要經過兩大步驟:1、原始檔由編譯器編譯成位元組碼(ByteCode) 2、位元組碼由java虛擬機器解釋執行。因為java程式既要編譯同時也要經過JVM的解釋執行,所以說Java被稱為半解釋語言( "semi-interpreted" language)。
三、執行時資料區
程式計數器(Program Counter Register)、Java棧(VM Stack)、本地方法棧(Native Method Stack)、方法區(Method Area)、堆(Heap)
image.png
程式計數器(Program Counter Register)
程式計數器是一塊較小的記憶體空間, 可以看作是當前執行緒所執行的位元組碼的行號指示器。 分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。
由於Java 虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)只會執行一條執行緒中的指令。因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間的計數器互不影響,獨立儲存,我們稱這類記憶體區域為 “執行緒私有” 的記憶體。
在JVM規範中規定,如果執行緒執行的是非native方法,則程式計數器中儲存的是當前需要執行的指令的地址;如果執行緒執行的是native方法,則程式計數器中的值是undefined。
由於程式計數器中儲存的資料所佔空間的大小不會隨程式的執行而發生改變,因此,對於程式計數器是不會發生記憶體溢位現象(OutOfMemory)的。
Java棧(VM Stack)
Java棧也稱作虛擬機器棧(Java Vitual Machine Stack),是Java方法執行的記憶體模型。
Java棧中存放的是一個個的棧幀, 每個棧幀對應一個被呼叫的方法,在棧幀中包括區域性變數表(Local Variables)、運算元棧(Operand Stack)、指向當前方法所屬的類的執行時常量池(執行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加資訊。
image.png
當執行緒執行一個方法時,就會隨之建立一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀出棧。
會有兩種異常StackOverFlowError和 OutOfMemoneyError。當執行緒請求棧深度大於虛擬機器所允許的深度就會丟擲StackOverFlowError錯誤;虛擬機器棧動態擴充套件,當擴充套件無法申請到足夠的記憶體空間時候,丟擲OutOfMemoneyError。
它是執行緒私有的,生命週期與執行緒相同。
本地方法棧(Native Method Stack)
本地方法棧與Java棧的作用和原理非常相似。區別只不過是Java棧是為執行Java方法服務的, 而本地方法棧則是為執行本地方法(Native Method)服務的。
在JVM規範中,並沒有對本地方發展的具體實現方法以及資料結構作強制規定,虛擬機器可以自由實現它。在HotSopt虛擬機器中直接就把本地方法棧和Java棧合二為一。
與虛擬機器棧一樣,本地方法棧區域也會丟擲StackOverflowError和OutOfMemoryError異常
方法區(Method Area)
JDK8之後-JVM執行時資料區域
JDK7及之前版本的方法區(Method Area)和Java堆一樣, 是各個執行緒共享的記憶體區域,用於儲存已經被虛擬機器載入的類資訊、常量、靜態常量、即時編譯器編譯後的程式碼等資料。
在方法區中有一個非常重要的部分就是 執行時常量池 ,它是每一個類或介面的常量池的執行時表示形式, 在類和介面被載入到JVM後,對應的執行時常量池就被建立出來。當然並非Class檔案常量池中的內容才能進入執行時常量池,在執行期間也可將新的常量放入執行時常量池中,比如String的intern方法。
package com.jvm;
import java.util.ArrayList;
import java.util.List;
/**
* -XX:MaxPermSize=20M 方法區最大大小20M
* Created by yulinfeng on 7/11/17.
*/
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern()); //不斷建立執行緒
}
}
}
實際上對於以上程式碼,在JDK6、JDK7、JDK8執行結果均不一樣。原因就在於字串常量池在JDK6的時候還是存放在方法區(永久代)所以它會丟擲OutOfMemoryError:Permanent Space;而JDK7後則將字串常量池移到了Java堆中,上面的程式碼不會丟擲OOM,若將堆記憶體改為20M則會丟擲OutOfMemoryError:Java heap space;至於JDK8則是純粹取消了方法區這個概念,取而代之的是”元空間(Metaspace)“,所以在JDK8中虛擬機器引數”-XX:MaxPermSize”也就沒有了任何意義,取代它的是”-XX:MetaspaceSize“和”-XX:MaxMetaspaceSize”等。
在JVM規範中,沒有強制要求方法區必須實現垃圾回收。很多人習慣將方法區稱為“永久代”,是因為HotSpot虛擬機器以永久代來實現方法區,從而JVM的垃圾收集器可以像管理堆區一樣管理這部分割槽域,從而不需要專門為這部分設計垃圾回收機制。不過自從JDK7之後,Hotspot虛擬機器便將執行時常量池從永久代移除了。
堆(Heap)
JVM中所管理記憶體中的最大的一塊。在虛擬機器啟動時被建立。
唯一的目的是存放物件例項,幾乎所有的物件例項和陣列都是在這裡分配記憶體。 (JVM規範中說的是所有的,但是隨著JIT便編譯器的發展和逃逸技術分析的成熟,一些例項可以不在這個區域分配記憶體)
程式設計師基本不用去關心空間釋放的問題,Java的垃圾回收機制會自動進行處理,因此堆是垃圾收集管理的主要區域,所以也會被稱為”GC堆“。
堆是被所有執行緒共享的,在JVM中只有一個堆。
淺堆和深堆
淺堆(Shallow Heap)和深堆(Retained Heap)是兩個非常重要的概念,它們分別表示一個物件結構所佔用的記憶體大小和一個物件被GC回收後,可以真實釋放的記憶體大小。
淺堆(Shallow Heap)是指一個物件所消耗的記憶體。在32位系統中,一個物件引用會佔據4個位元組,一個int型別會佔據4個位元組,long型變數會佔據8個位元組,每個物件需要佔用8個位元組。
深堆(Retained Heap)的概念略微複雜。要理解深堆,首先需要了解保留集(Retained Set)。物件A的保留集指當物件A被垃圾回收後,可以被釋放的所有物件集合(包括物件A本身),即物件A的保留集可以被認為是隻能通過物件A被直接或間接訪問到的所有物件的集合。通俗地說,就是指僅被物件A所持有的物件的集合。深堆是指物件的保留集中所有的物件的淺堆大小之和。
例如:物件A引用了C和D,物件B引用了C和E。那麼物件A的淺堆大小隻是A本身,不含C和D,而A的實際大小為A、C、D三者之和。而A的深堆大小為A與D之和,由於物件C還可以通過物件B訪問到,因此不在物件A的深堆範圍內。