背景
前一陣跟着宋紅康的視頻學了學JVM,視頻沒有更新完,所以也沒學完,這裏記錄一下筆記
JVM概述
JVM位置: 運行在操作系統之上
相對於java語言,JVM的位置如下所示
對於安卓的Davlik虛擬機,他分佈在安卓運行時內存區
整體結構:
以HotSpot VM爲例,它採用解釋器與即時編譯器(JIT)並存的架構
運行時數據區和執行引擎的交互如下圖所示:
java代碼執行流程
把java源碼編譯成class字節碼文件的編譯器稱爲前端編譯器,即時編譯器稱爲後端編譯器,把反覆執行的代碼(稱爲熱點代碼)的字節碼指令編譯爲機器指令,並緩存起來。
解釋器響應快,但執行起來慢;JIT響應慢,但執行起來快
架構模型
基於棧(Hotspot VM)
實現簡單,適用於資源受限的系統
由於只有入棧出棧操作,所以避開了寄存器的分配難題,採用零地址指令分配
指令集小,編譯器容易實現
不需要硬件支持,可以執行更好
指令多
基於寄存器(PC、Android的Davlik)
完全依賴硬件,可移植性差
性能優秀,執行高效
指令少
指令集以一地址、二地址和三地址爲主
生命週期
啓動:通過引導類加載器創建的初始類來完成,這個類由具體的JVM具體實現
執行:執行java程序的過程就是JVM的執行過程,java程序執行完畢,對應的JVM進程隨即終止;
退出:程序正常結束;程序遇到了異常或錯誤而異常終止;操作系統出現錯誤;Runtime或System的exit()方法,以及Runtime的halt()方法;JNI也可以加載卸載JVM
類加載器子系統
職責
負責從文件系統或網絡中加載class文件
class文件在文件頭有特定的文件標識(CA FE BA BE),鏈接的驗證階段會進行驗證
類加載器只負責class文件的加載,至於能否運行,由執行引擎決定。
加載好的類信息(包括對應的類加載器的引用)存放於方法區,此外,方法區還保存運行時常量池信息(字符串字面量和數字常量等)
字節碼中的常量池實例
以下面代碼爲例
public class PCTest {
public static void main(String[] args) {
int a = 0;
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
int b = 1;
}
}
對應字節碼中的常量池如下圖所示
左邊的#數字是符號引用,=後面的是常量類型(方法引用、屬性引用、類、UTF8爲字符串字面量等)
類加載器的角色
類加載過程
加載->鏈接(驗證->準備->解析)->初始化
流程圖如下所示(目標類HelloLoader)
1、加載階段:
通過類的全限定名獲取定義此類的二進制字節流,然後把字節流所代表的靜態存儲結構存入方法區,最後在內存中生成代表這個類的Class對象,作爲方法區這個類的訪問入口
2、鏈接階段:
3、初始化階段:
<clinit>()是針對靜態代碼的,如果類裏沒有靜態部分,就沒有<clinit>()方法。由於<clinit>()的順序執行,因此下面程序的輸出結果爲sA = 2, sB = 1
private static int sA = 1;
static {
sA = 2;
sB = 2;
}
private static int sB = 1;
public static void main(String[] args) {
System.out.println("sA = " + sA);
System.out.println("sB = " + sB);
}
以sB爲例,鏈接的準備階段被初始化爲0,初始化階段先被賦值爲2,再被賦值爲1
另外,由於<clinit>()的同步加鎖,因此多個線程加載一個類時,這個類的靜態塊只會被加載一次。比如以下代碼
public class PCTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
new MyThread();
System.out.println(Thread.currentThread().getName() + "運行結束");
}, "t1");
Thread t2 = new Thread(() -> {
new MyThread();
System.out.println(Thread.currentThread().getName() + "運行結束");
}, "t2");
t1.join();
t2.join();
t1.start();
t2.start();
}
}
class MyThread {
static {
while (true) {
System.out.println(Thread.currentThread().getName() + "初始化當前類");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
} finally {
break;
}
}
}
}
t1和t2都會加載類MyThread,但是static塊只會運行一次,故而輸出如下所示
class文件的獲取方式
本地文件系統、網絡(web applet)、zip壓縮包(jar、war)、運行時計算(動態代理)、其他文件生成(JSP)、專有數據庫、加密文件(安卓防止反編譯)
類加載器的分類
引導類(Bootstrap)加載器和自定義加載器(Extension、Application、自定義)
這四者的關係是包含關係,不是父類子類關係。
以sun.misc.Launcher(一個JVM的入口應用)爲例,它裏面類加載器類圖如下所示
用戶自定義類都用AppClassLoader,如下所示
ClassLoader classLoader = StackStructTest.class.getClassLoader();
// 用戶自定義類使用應用加載器加載
System.out.println("StackStructTest的類加載器爲:" + classLoader);
系統api的類加載器爲引導類加載器,由於這是由C實現的,所以java獲取到的值爲null,如下所示
classLoader = String.class.getClassLoader();
// 系統核心類庫(String)使用引導類加載器加載
System.out.println("String的類加載器爲:" + classLoader);
而默認的系統類加載器就是應用類加載器,而其父加載器爲擴展類加載器,擴展類加載器的父類就是引導類加載器,如下所示
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系統類加載器爲:" + systemLoader);
ClassLoader extLoader = systemLoader.getParent();
System.out.println("系統類加載器的父加載器爲:" + extLoader);
ClassLoader bootstrapLoader = extLoader.getParent();
System.out.println("擴展類加載器的父加載器爲:" + bootstrapLoader);
我們可以獲取引導類加載器的加載路徑,如下所示
// 獲取引導類加載器能加載的路徑
System.out.println("引導類加載器的加載路徑:");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url.toString());
}
可見都是系統類庫
我們也可以獲取擴展類加載器的加載路徑,如下所示
System.out.println("擴展類加載器的加載路徑");
String extDirs = System.getProperty("java.ext.dirs");
for (String s : extDirs.split(";")) {
System.out.println(s);
}
也就是ext目錄下的類
用戶自定義類加載器
存在意義:隔離加載類(模塊和中間件的同步)、修改類加載方式、擴展加載源、防止源碼泄露
實現步驟:繼承URLClassLoader類,如果需要實現比較複雜的業務邏輯,就繼承寫ClassLoader類,覆寫findClass()方法
class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = getClassFromFilePath(name);
if (bytes == null) {
throw new FileNotFoundException();
} else {
return defineClass(name, bytes, 0, bytes.length);
}
} catch (Exception e) {
e.printStackTrace();
}
throw new ClassNotFoundException();
}
private byte[] getClassFromFilePath(String name) {
// 略
return null;
}
}
其中我們可以在getClassFromFilePath()方法中自定義類加載邏輯,如果類文件是加密文件,可以在這裏寫上解密邏輯。
自定義類加載器的使用如下所示
Class clazz = Class.forName("className", true, new MyClassLoader());
Object object = clazz.newInstance();
雙親委派機制
類加載器在加載一個類時,會先讓最頂層的引導類加載器加載類。如果上層類加載器可以加載類,那麼就讓那一層類加載器加載,否則再讓下層加載。
也就是說,加載一個類調用的類加載器先後順序爲:引導類->擴展類->應用(系統)類->用戶自定義類
這樣可以保護系統API的安全性,同時也是一種沙箱安全機制
兩個類對象爲同一個類的條件:類的全限定名一樣;類的加載器一樣
因此,兩個類對象即便來源於同一個class文件,被同一個JVM加載,如果加載它們的加載器不一樣,這兩個類對象也不是一個類。
類的使用方式
主動使用:創建類的實例、訪問類的靜態部分(變量、方法)、反射、初始化類的子類、JVM啓動時被標明爲啓動類、動態語言支持(REF_getStatic、REF_putStatic、REF_invokeStatic對應的類)
被動使用:除了上面七種方式,剩下都是類的被動使用,不會導致類的初始化
PC寄存器
JVM中的PC寄存器是對物理PC寄存器的抽象模擬,用於存儲下一條指令的地址,線程獨立
它很小,運行速度最快。如果當前線程正在執行的方法是java方法,他會存儲對應方法的JVM指令地址(也就是下一條指令地址);如果執行的是本地方法,則會存儲未指定值(undefined)
它是程序控制流的指示器,字節碼解釋工作制通過改變PC寄存器的值來選取下一條要執行的字節碼指令,不存在OOM
例如如下代碼
public static void main(String[] args) {
int a = 0;
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
int b = 1;
}
對應的字節碼指令爲
左邊的一排數字就是字節碼指令地址,當執行到第22行goto 4時,就會跳轉到第4行的iload_2,即取出JVM棧當前棧幀中局部變量表裏第2個元素(i)的值
保存字節碼指令地址是爲了方便線程切換時能夠知道從哪兒繼續執行。
虛擬機棧
概述
線程私有的,保存方法的局部變量(8種基本數據類型+對象的引用)、部分結果,參與方法的調用和返回。
生命週期
生命週期和線程一致
特點
快速有效,訪問速度僅次於PC;操作只有進棧出棧;不存在GC問題
相關異常
如果採用固定大小的棧,當線程請求的棧容量超標,則拋出StackOverFlow異常(遞歸調用main()方法)
如果採用動態分配大小的棧,當擴展容量時沒有足夠的內存,則拋出OutOfMemory異常
設置
設置棧大小:-Xss256k(設置棧大小爲256K)
內部結構
如上圖所示,內部單位爲棧幀,一個棧幀對應一個方法的調用。
一個時間點上,只有棧頂棧幀是活動的,是所謂當前棧幀,對應的方法和類分別是當前方法和當前類
不同棧的棧幀不能相互引用
方法的結束方式
1、正常結束return
2、方法執行中出現未被捕獲的異常,以拋出異常的方式結束
棧幀的內部結構
局部變量表
定義爲一個數字數組,用於存儲方法參數和方法體內的局部變量。
數據類型包括:八種基本數據類型、對象引用和返回地址
4字節以內的只佔一個slot(包括返回地址類型和引用類型),8字節的佔用兩個slot(long和double)
slot槽:局部變量表的存儲單元,也可以重用,以如下代碼爲例
public void methodB() {
int a = 1;
{
int b = 0;
}
int c = a;
}
對應局部變量表如下
可見c用的是已經被銷燬的b的槽
局部變量表所需大小在編譯期確定
具體可見javap -v中顯示的方法LocalVariableTable中的內容,以下爲main()方法的局部變量表:
如果是非靜態方法,那麼局部變量表裏會多一個this
局部變量表中的變量也是重要的垃圾回收根結點,只要被局部變量表直接或間接引用的對象,都不會被回收
方法返回地址
方法返回地址就是調用此方法的PC寄存器的值
如果方法正常退出,那麼返回地址就是調用此方法的指令的下一條指令的地址;如果異常退出,那麼返回地址由異常表確定,棧幀中不保存這種信息
正常退出的指令根據返回類型決定,有return(void返回類型、構造方法、static代碼塊)、ireturn(int、char、boolean、byte、short返回類型)、dreturn(double返回類型)、freturn(float返回類型)、lreturn(long返回類型)和areturn(引用返回類型)
異常退出(捕獲異常)則根據異常表確定返回地址,如以下代碼:
public void showException() {
try {
int a = 1 / 0;
} catch (Exception e) {
System.out.println(e.toString());
}
int b = 9;
}
產生的字節碼如下所示
綠色方框內就是異常表,裏面的數字是字節碼指令在字節碼指令表中的地址,也就是是第0條到第4條指令出現Exception類型的異常,返回地址就是第7條指令,即astore_1,把異常對象壓入局部變量表中
操作數棧
在字節碼指令執行過程中進行出棧進棧的棧,用以保存計算的中間結果,並作爲變量的臨時存儲空間。某些字節碼指令把值壓入棧,另外一些字節碼指令把值從棧頂彈出,然後把計算結果壓入棧中。
JVM的執行引擎就是基於操作數棧的執行引擎。
以下面代碼爲例
public void methodC() {
int i = 15;
byte j = 9;
int k = i + j;
}
對應字節碼指令如下
代碼追蹤過程如下
bipush 15把15壓入操作數棧裏,istore_1把操作數棧中的數存入局部變量表中1的槽裏
bipush 8和istore_2一樣的道理
iload_1和iload_2分別把局部變量表中索引爲1和2對應變量的值彈到操作數棧中
iadd指令把操作數棧中的數彈出相加,把結果23入棧。istore_3再把操作數棧中的數壓入局部變量表中索引爲3的槽中。
其中istore、iload、iadd的前綴i表示數據類型爲int(byte、char、boolean、short都以int類型被解釋)
而bipush表示把byte類型的數據壓入操作數棧中,如果數值超過byte,則用更大的類型存儲。例如int a = 200,對應的指令就是sipush 800
如果涉及方法的返回值,例如如下代碼
public void methodC() {
int i = 15;
byte j = 9;
int k = i + j;
int m = methodD();
}
public int methodD() {
int i = 15;
byte j = 9;
int k = i + j;
return k;
}
對應字節碼指令如下
methodC()中aload_0把this裝載到操作數棧裏,然後invokeVirtual()調用棧頂元素this裏#3對應的方法methodD。methodD()中最後的ireturn把操作數棧中的值返回到操作數棧裏,methodC()的istore 4把操作數棧頂元素存入局部變量表的4號槽中
4字節的類型佔用一個棧單位深度,8字節的佔用兩個棧單位深度。
如果方法有返回值,返回值也會被壓入當前棧幀的操作數棧中。
動態鏈接
每一個棧幀都包含一個指向運行時常量池中此棧幀所屬方法的引用,以便當前方法代碼的動態鏈接
在java源文件編譯到class文件的過程中,所有變量和方法都會作爲符號引用保存到class文件的常量池中,如下所示
動態鏈接的作用就是把符號引用轉換爲調用方法的直接引用(如#3轉換爲StackFrameTest.methodD:()I),如下圖所示
常量池的作用就是提供符號和常量,便於指令的識別
附加信息
可選,略
變量分類
成員變量:使用前都經歷過默認初始化賦值
類變量:鏈接的準備階段,會給類變量默認賦值;在初始化階段,會顯式賦值,並執行靜態代碼塊
實例變量:隨着對象的創建,會在堆空間中分配空間,賦默認值
局部變量:必須顯式賦值
棧頂緩存技術
把棧頂元素全部緩存到CPU寄存器中,以降低對內存的IO次數。
方法的調用
符號引用轉換爲直接引用與方法的綁定機制有關。
靜態鏈接:目標方法編譯期可知
動態鏈接:目標方法運行期纔可知
綁定機制
早期綁定:靜態鏈接對應早期綁定
晚期綁定(類的多態、接口):動態鏈接對應晚期綁定
以以下代碼爲例
public class DynamicLinkTest {
public void showEat(Animal animal) {
animal.eat();
}
public void showHunt(Huntable huntable) {
huntable.hunt();
}
}
class Animal {
public void eat() {
System.out.println("Animal eat");
}
public final void showFinal () {
System.out.println("Animal show finally");
}
public static void showStatic() {
System.out.println("Animal show statically");
}
}
interface Huntable{
public void hunt();
}
class Cat extends Animal implements Huntable {
public Cat() {}
public Cat(int n) {
this();
}
@Override
public void eat() {
super.eat(); // 早期綁定
System.out.println("Cat eating...");
showFinal();
super.showFinal();
showStatic();
}
@Override
public void hunt() {
System.out.println("Cat hunting...");
}
}
class Dog extends Animal implements Huntable {
@Override
public void eat() {
System.out.println("Dog eating...");
}
@Override
public void hunt() {
System.out.println("Dog hunting...");
}
}
由於多態,showEat()和showHunt()中的方法調用都用的是晚期綁定,因爲編譯期無法確定到底是哪個類的對應方法;而Cat類的eat()方法中的super.eat()用的是早期綁定,因爲在編譯期就確定是Animal類的eat()方法
四種調用方法的指令
invokevirtual、invokeinerface、invokespecial、invokestatic和invokedynamic
調用父類方法定對應的字節碼爲invokevirtual
如果是接口方法,則更爲特殊,指令是invokeinterface
而調用確定類的方法綁定對應字節碼爲invokespecial
靜態方法則爲invokestatic
對於父類的final方法,如果直接調用,則爲invokevirtual;如果使用super調用,則爲invokespecial
invokedynamic則是爲實現動態類型語言而做的一種改進,例如lambda表達式
public void showDynamic() {
Huntable huntable = () -> {
System.out.println("lambda huntable");
};
}
對應字節碼爲
非虛方法與虛方法
非虛方法:靜態方法、私有方法、final方法、構造方法、父類方法,這些方法都爲早期綁定,編譯期就知道調用的是哪個類的方法
由invokestatic和invokespecial指令調用的都是非虛方法,其餘的(final除外)都是虛方法
java中方法覆寫的本質
1、找到操作數棧頂元素所執行對象的實際類型,記作C
2、如果在類型C找到與常量池中描述符和簡單名稱都相同的方法,則進行訪問權限校驗,通過則返回此方法的直接引用;不通過則拋出IllegalAccessError異常
3、否則,按照繼承關係從下往上一次對C的各個父類進行第2步的搜索和驗證過程
4、如果始終沒有找到合適的方法,則拋出AbstractMethodError異常
爲了提高上述過程的性能,JVM會在類鏈接階段,在方法區爲所有的虛方法構造一個虛方法表,存放各個虛方法的實際入口位置。類初始化後,類的所有方法表都會初始化完成
逃逸分析
問題:
方法中的變量是否線程安全?
答案:
不一定。例如,下面兩種情況的stringBuilder是線程安全的
public void methodA() {
StringBuilder s = new StringBuilder();
s.append("a").append("b");
}
public String methodB() {
StringBuilder s = new StringBuilder();
s.append("a").append("b");
return s.toString();
}
但下面兩種就不安全了,發生了逃逸
public void methodC(StringBuilder s) {
s.append("a").append("b");
}
public StringBuilder methodD() {
StringBuilder s = new StringBuilder();
s.append("a").append("b");
return s;
}
本地方法棧
本地方法
本地方法就是java調用非java代碼的接口
存在意義:與java外環境交互、與OS交互、Sun`s java
本地方法棧
用於管理本地方法的調用,這是和JVM棧唯一的區別
當一個線程調用本地方法時,他就進入了一個全新的不再受虛擬機限制的世界,他和JVM有同樣的權限。
本地方法可以訪問JVM運行時數據區,可以直接使用寄存器,可以直接從堆中分配任意數量的內存。
Hotspot虛擬機中,直接把本地方法棧和虛擬機棧二合一