對基本原理的瞭解,動手是最好的;html
例子java
1package com.java.study.jvm;
2
3/**
4 * @author zhangpeng
5 * @since 2020/1/15 3:33 下午
6 */
7public class JvmHello {
8 public static final int i = 2020;
9
10 public static void main(String[] args) {
11 JvmHello jvmHello = new JvmHello();
12 int a = 1;
13 int b = 2;
14 int c = jvmHello.calculate1(a, b);
15 int d = jvmHello.calculate2(a, b);
16 }
17
18 private int calculate2(int a, int b) {
19 int x = 666;
20 return x / (a + b);
21 }
22
23 private int calculate1(int a, int b) {
24 return (a + b) * 2333;
25 }
26}
複製代碼
這段代碼我就不解釋了 直接編譯字節碼搞起git
1# 編譯生成 JvmHello.class文件
2javac JvmHello.java
3# 反編譯字節碼內容
4javap -verbose -p JvmHello.class
複製代碼
記得以前書裏提到的,編譯一次處處執行,那麼首先文件要被加載進來,運行在一個環境裏面;因此咱們有了初步的圖github
JvmHello.java -> JvmHello.class -> 類裝載系統加載進來 -> 在虛擬機環境執行web
接着咱們看下JVMHello.class的內容shell
1Classfile /Users/zhangpeng/workspacke/mytest/study/src/main/java/com/java/study/jvm/JvmHello.class
2 Last modified 2020-1-15; size 530 bytes
3 MD5 checksum d1725552383bf6c86a00f1517d2b4c51
4 Compiled from "JvmHello.java"
5public class com.java.study.jvm.JvmHello
6 minor version: 0
7 major version: 52
8 flags: ACC_PUBLIC, ACC_SUPER
9Constant pool:
10 #1 = Methodref #6.#22 // java/lang/Object."<init>":()V
11 #2 = Class #23 // com/java/study/jvm/JvmHello
12 #3 = Methodref #2.#22 // com/java/study/jvm/JvmHello."<init>":()V
13 #4 = Methodref #2.#24 // com/java/study/jvm/JvmHello.calculate1:(II)I
14 #5 = Methodref #2.#25 // com/java/study/jvm/JvmHello.calculate2:(II)I
15 #6 = Class #26 // java/lang/Object
16 #7 = Utf8 i
17 #8 = Utf8 I
18 #9 = Utf8 ConstantValue
19 #10 = Integer 2020
20 #11 = Utf8 <init>
21 #12 = Utf8 ()V
22 #13 = Utf8 Code
23 #14 = Utf8 LineNumberTable
24 #15 = Utf8 main
25 #16 = Utf8 ([Ljava/lang/String;)V
26 #17 = Utf8 calculate2
27 #18 = Utf8 (II)I
28 #19 = Utf8 calculate1
29 #20 = Utf8 SourceFile
30 #21 = Utf8 JvmHello.java
31 #22 = NameAndType #11:#12 // "<init>":()V
32 #23 = Utf8 com/java/study/jvm/JvmHello
33 #24 = NameAndType #19:#18 // calculate1:(II)I
34 #25 = NameAndType #17:#18 // calculate2:(II)I
35 #26 = Utf8 java/lang/Object
36{
37 public static final int i;
38 descriptor: I
39 flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
40 ConstantValue: int 2020
41
42 public com.java.study.jvm.JvmHello();
43 descriptor: ()V
44 flags: ACC_PUBLIC
45 Code:
46 stack=1, locals=1, args_size=1
47 0: aload_0
48 1: invokespecial #1 // Method java/lang/Object."<init>":()V
49 4: return
50 LineNumberTable:
51 line 7: 0
52
53 public static void main(java.lang.String[]);
54 descriptor: ([Ljava/lang/String;)V
55 flags: ACC_PUBLIC, ACC_STATIC
56 Code:
57 stack=3, locals=6, args_size=1
58 0: new #2 // class com/java/study/jvm/JvmHello
59 3: dup
60 4: invokespecial #3 // Method "<init>":()V
61 7: astore_1
62 8: iconst_1
63 9: istore_2
64 10: iconst_2
65 11: istore_3
66 12: aload_1
67 13: iload_2
68 14: iload_3
69 15: invokespecial #4 // Method calculate1:(II)I
70 18: istore 4
71 20: aload_1
72 21: iload_2
73 22: iload_3
74 23: invokespecial #5 // Method calculate2:(II)I
75 26: istore 5
76 28: return
77 LineNumberTable:
78 line 11: 0
79 line 12: 8
80 line 13: 10
81 line 14: 12
82 line 15: 20
83 line 16: 28
84
85 private int calculate2(int, int);
86 descriptor: (II)I
87 flags: ACC_PRIVATE
88 Code:
89 stack=3, locals=4, args_size=3
90 0: sipush 666
91 3: istore_3
92 4: iload_3
93 5: iload_1
94 6: iload_2
95 7: iadd
96 8: idiv
97 9: ireturn
98 LineNumberTable:
99 line 19: 0
100 line 20: 4
101
102 private int calculate1(int, int);
103 descriptor: (II)I
104 flags: ACC_PRIVATE
105 Code:
106 stack=2, locals=3, args_size=3
107 0: iload_1
108 1: iload_2
109 2: iadd
110 3: sipush 2333
111 6: imul
112 7: ireturn
113 LineNumberTable:
114 line 24: 0
115}
116SourceFile: "JvmHello.java"
複製代碼
描述了類的基本信息數組
運行時常量池bash
咱們先分析下第一個常量,位於JVMHello.class第10行,咱們會發現後面有關聯項 一塊兒放進來oracle
1 #1 = Methodref #6.#22 // java/lang/Object."<init>":()V
2 #6 = Class #26 // java/lang/Object
3 #11 = Utf8 <init>
4 #12 = Utf8 ()V
5 #22 = NameAndType #11:#12 // "<init>":()V
6 #26 = Utf8 java/lang/Object
複製代碼
Methodref表示方法定義,右側的註釋內容(表示是由這幾行組合起來的)app
1java/lang/Object."<init>":()V
複製代碼
這段能夠理解爲該類的實例父類構造器的聲明,此處也說明了JvmHello類的直接父類是Object.該方法默認返回值是V,也就是void,無返回值
同理分析下第二個常量,位於JVMHello.class第12行
1 #2 = Class #23 // com/java/study/jvm/JvmHello
2 #3 = Methodref #2.#22 // com/java/study/jvm/JvmHello."<init>":()V
3 #11 = Utf8 <init>
4 #12 = Utf8 ()V
5 #22 = NameAndType #11:#12 // "<init>":()V
6 #23 = Utf8 com/java/study/jvm/JvmHello
複製代碼
這裏描述的是默認的構造器JvmHello(),由於後面在main()方法裏面new了對象 因此這裏會初始化到常量池
同理分析下第三個常量,位於JVMHello.class第13行
1 #2 = Class #23 // com/java/study/jvm/JvmHello
2 #4 = Methodref #2.#24 // com/java/study/jvm/JvmHello.calculate1:(II)I
3 #18 = Utf8 (II)I
4 #19 = Utf8 calculate1
5 #24 = NameAndType #19:#18 // calculate1:(II)I
複製代碼
這裏描述的是JvmHello類裏面calculate1方法的定義
1 com/java/study/jvm/JvmHello.calculate1:(II)I
複製代碼
(II) 表示入參爲兩個基本類型int
(II)I 右邊的這個I表示返回值也是基本類型int
連起來講就是 calculate1方法入參是兩個int,返回值是int
那麼同理可得 位於JVMHello.class第14行的變量表示的是 calculate2方法入參也是兩個int,返回值也是int
上述就是運行時常量池信息的分析,常量池用於存放編譯期生成的各類字面量和符號引用,常量池是被劃分在了方法區這個裏面,方法區用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據到這咱們補充一下咱們的jvm圖
方法表集合
先看下靜態常量的定義,位於JVMHello.class
1 public static final int i;
2 descriptor: I
3 flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
4 ConstantValue: int 2020
複製代碼
1 public com.java.study.jvm.JvmHello();
2 descriptor: ()V
3 flags: ACC_PUBLIC
4 Code:
5 stack=1, locals=1, args_size=1
6 0: aload_0
7 1: invokespecial #1 // Method java/lang/Object."<init>":()V
8 4: return
9 LineNumberTable:
10 line 7: 0
複製代碼
這裏咱們產生了另外一個概念棧,方法執行會進行壓棧出棧
main方法分析
1 public static void main(java.lang.String[]);// main方法
2 descriptor: ([Ljava/lang/String;)V // 入參String[],出參V(void)
3 flags: ACC_PUBLIC, ACC_STATIC // 公共的、靜態的
4 Code:
5 stack=3, locals=6, args_size=1 // 操做數棧3,局部變量6 Slot,參數個數爲1
6 0: new #2 // class com/java/study/jvm/JvmHello new對象
7 3: dup // 複製棧頂部一個字長內容
8 4: invokespecial #3 // Method "<init>":()V 執行JvmHello構造器
9 7: astore_1 // 將returnAddress類型(引用類型)存入到局部變量[1]
10 8: iconst_1 // 將int類型常量[1]壓入到操做數棧
11 9: istore_2 // 將int類型值存入局部變量[2]
12 10: iconst_2 // 將int類型常量[2]壓入到操做數棧
13 11: istore_3 // 將int類型值存入局部變量[3]
14 12: aload_1 // 從局部變量[1]中裝載引用類型值
15 13: iload_2 // 從局部變量[2]中裝載int類型值
16 14: iload_3 // 從局部變量[3]中裝載int類型值
17 15: invokespecial #4 // Method calculate1:(II)I 執行calculate1方法
18 18: istore 4 // 將int類型值存入局部變量[4]
19 20: aload_1 // 從局部變量[1]中裝載引用類型值
20 21: iload_2 // 從局部變量[2]中裝載int類型值
21 22: iload_3 // 從局部變量[3]中裝載int類型值
22 23: invokespecial #5 // Method calculate2:(II)I 執行calculate2方法
23 26: istore 5 // 將int類型值存入局部變量[5]
24 28: return // void返回
25 LineNumberTable:
26 line 11: 0
27 line 12: 8
28 line 13: 10
29 line 14: 12
30 line 15: 20
31 line 16: 28
複製代碼
從第一行new對象提及
1 JvmHello jvmHello = new JvmHello();
2 // 這裏的jvmHello就是局部變量[1];
複製代碼
那麼new出來的對象放在哪裏的,看過jvm相關內容的同窗都知道對象是分配在堆裏面的
關於堆的定義
對於大多數應用來講,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。這一點在Java虛擬機規範中的描述是:全部的對象實例以及數組都要在堆上分配,可是隨着JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換 優化技術將會致使一些微妙的變化發生,全部的對象都分配在堆上也漸漸變得不是那麼「絕對」了。
接着看下面兩行
1 int a = 1;
2 int b = 2;
3 // 1.這裏先將常量[1] = 1壓入到操做數棧
4 // 2.再將整個常量[1]的int類型的值賦值給 局部變量[2]也就是 a = 1;
5 // 同理 b=2也是一樣的過程
複製代碼
而後看執行calculate一、calculate2方法
1int c = jvmHello.calculate1(a, b);
2int d = jvmHello.calculate2(a, b);
3// 1.從局部變量[1]中裝載引用類型值 即jvmHello的值
4// 2.從局部變量[2]中裝載int類型值 即值爲2
5// 3.從局部變量[3]中裝載int類型值 即值爲2
6// 4.使用jvmHello執行calculate1方法
7// 同理 calculate2執行過程相似
複製代碼
上面這段咱們知道,jvm在執行代碼的時候,是基於棧的執行,也就是操做棧 每一個棧裏面有局部變量,局部變量是分配在局部變量表裏面
關於java棧的定義,他有兩個棧:java虛擬機棧和本地方法棧
Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀(Stack Frame )用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
本地方法棧(Native Method Stack)與虛擬機棧所發揮的做用是很是類似的,它們之間的區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務
既然虛擬機棧裏面提到線程,那麼這裏順便介紹下程序計數器
程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,各類虛擬機可能會經過一些更高效的方式去實現),字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
知道了堆和棧,繼續補充一下咱們的圖
咱們繼續分析下calculate1和calculate2
1private int calculate2(int, int);
2 descriptor: (II)I // 入參2個int類型 出參int類型
3 flags: ACC_PRIVATE
4 Code:
5 stack=3, locals=4, args_size=3 // 操做數棧3,局部變量4 Slot,參數個數3個
6 0: sipush 666 // 將16位帶符號整數(這裏指666)壓入棧
7 3: istore_3 // 將int類型值(即666)存入局部變量[3]
8 4: iload_3 // 從局部變量[3]中裝載int類型值
9 5: iload_1 // 從局部變量[1]中裝載int類型值
10 6: iload_2 // 從局部變量[3]中裝載int類型值
11 7: iadd // 執行int類型的加法,即 1+2
12 8: idiv // 執行int類型的除法,即 666/3
13 9: ireturn // 返回int類型的值
14 LineNumberTable:
15 line 19: 0
16 line 20: 4
17
18 private int calculate1(int, int);
19 descriptor: (II)I // 入參2個int類型 出參int類型
20 flags: ACC_PRIVATE // 私有的
21 Code:
22 stack=2, locals=3, args_size=3 // 操做數棧2,局部變量3 Slot,參數個數3個
23 0: iload_1 // 從局部變量[1]中裝載int類型值
24 1: iload_2 // 從局部變量[2]中裝載int類型值
25 2: iadd // 執行int類型的加法,即 1+2
26 3: sipush 2333 // 將16位帶符號整數(這裏指2333)壓入棧
27 6: imul // 執行int類型的乘法 3*2333
28 7: ireturn // 返回int類型的值
29 LineNumberTable:
30 line 24: 0
複製代碼
其實到這裏我有個疑問 爲何calculate1和calculate2的入參明明只有2個,反編譯後會顯示2個呢?我去搜了下
原來在計算args_size時,有判斷方法是否爲static方法,若是不是static方法,則會在方法原有參數數量上再加一,這是由於非static方法會添加一個默認參數到參數列表首位:方法的真正執行者,即方法所屬類的實例對象。那對應咱們這多出來的參數就是 jvmHello了
最後關於操做棧的過程 這裏我以calculate1爲例
上面提到的虛擬機棧的概念也提過,方法執行的同時會建立棧幀,存儲局部變量表、操做數棧、動態連接、方法出口;因此上圖就是一個棧幀在虛擬機中入棧到出棧的過程.基於這點最後補充一下棧裏面的信息內容
表示源文件JvmHello.java
經過分析字節碼,能夠加深對虛擬機內存結構,java代碼從編譯到加載,和運行的整個過程,而不是去死記書裏的那些概念。
喜歡的記得一鍵三連
原文git