一. java結構體系
Description of Java Conceptual Diagram(java結構)

我們經常說到JVM調優,JVM和JDK到底什么關系,大家知道么?這是java基礎。
這幅圖很重要,一定要了解其結構。這是jdk的結構圖。從結構上可以看出java結構體系, JDK主要包含兩部分:
第一部分:是java 工具(Tools&Tool APIs)
比如java, javac, javap等命令. 我們常用的命令都在這里
第二部分: JRE(全稱:Java Runtime Enveriment), jre是Java的核心,。
jre里面定義了java運行時需要的核心類庫, 比如:我們常用的lang包, util包, Math包, Collection包等等.這里還有一個很重要的部分JVM(最后一部分青色的) java 虛擬機, 這部分也是屬於jre, 是java運行時環境的一部分. 下面來詳細看看:
- 最底層的是Java Virtual Machine: java虛擬機
- 常用的基礎類庫:lang and util。在這里定義了我們常用的Math、Collections、Regular Expressions(正則表達式),Logging日志,Reflection反射等等。
- 其他的擴展類庫:Beans,Security,Serialization序列化,Networking網絡,JNI,Date and Time,Input/Output等。
- 集成一體化類庫:JDBC數據庫連接,jndi,scripting等。
- 用戶接口工具:User Interface Toolkits。
- 部署工具:Deployment等。
從上面就可看出,jvm是整個jdk的最底層。jvm是jdk的一部分。
二. java語言的跨平台特性
1. java語言是如何實現跨平台的?

跨平台指的是, 程序員開發出的一套代碼, 在windows平台上能運行, 在linux上也能運行, 在mac上也能運行. 我們知道, 機器最終運行的指令都是二進制指令. 同樣的代碼, 在windows上生成的二進制指令可能是0101, 但是在linux上是1001, 而在mac上是1011。這樣同樣的代碼, 如果要想在不同的平台運行, 放到相應的平台, 就要修改代碼, 而java卻不用, 那么java這種跨平台特性是怎么做到的呢?
原因在於jdk, 我們最終是將程序編譯成二進制碼,把他丟在jvm上運行的, 而jvm是jre的一部分. 我們在不同的平台下載的jdk是不同的. windows平台要選擇下載適用於windows的jdk, linux要選擇適用於linux的jdk, mac要選擇適用於mac的jdk. 不同平台的jvm針對該平台有一個特定的實現, 正是這種特點的實現, 讓java實現了跨平台。
2. 延伸思考
通過上面的分析,我們知道能夠實現跨平台是因為jvm封裝了變化。我們經常說進行jvm調優,那么在不同平台的調優參數可以通用么?顯然是不可以的。不同平台的jvm尤其個性化差異。
封裝變化的部分是JDK中的jvm,JVM的整體結構是怎樣的呢?來看下面一個部分。
三. JVM整體結構和內存模型
1.JVM由三部分組成:
- 類裝載子系統
- 運行時數據區(內存模型)
- 字節碼執行引擎

其中類裝載子系統是C++實現的, 他把類加載進來, 放入到虛擬機中. 這一塊就是之前分析過的類加載器加載類,采用雙親委派機制,把類加載進來放入到jvm虛擬機中。
然后, 字節碼執行引擎去虛擬機中讀取數據. 字節碼執行引擎也是c++實現的. 我們重點研究運行時數據區。
2.運行時數據區的構成
運行時數據區主要由5個部分構成: 堆,棧,本地方法棧,方法區,程序計數器。
3.JVM三部分密切配合工作
下面我們來看看一個程序運行的時候, 類裝載子系統, 運行時數據區, 字節碼執行引擎是如何密切配合工作的?
我們舉個例子來說一下:
package com.lxl.jvm;
public class Math {
public static int initData = 666;
public static User user = new User();
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
當我們在執行main方法的時候, 都做了什么事情呢?
第一步: 類加載子系統加載Math.class類, 然后將其丟到內存區域, 這個就是前面博客研究的部分,類加載的過程, 我們看源碼也發現,里面好多代碼都是native本地的, 是c++實現的
第二步: 在內存中處理字節碼文件, 這一部分內容較多, 也是我們研究的重點, 后面會對每一個部分詳細說
第三步: 由字節碼執行引擎執行java虛擬機中的內存代碼, 而字節碼執行引擎也是由c++實現的
這里最核心的部分是第二部分運行時數據區(內存模型), 我們后面的調優, 都是針對這個區域來進行的.
下面詳細來說內存區域

這是java的內存區域, 內存區域干什么呢?內存區域其實就是放數據的,各種各樣的數據j放在不同的內存區域
四. 棧
棧是用來存放變量的
4.1. 棧空間
還是用Math的例子來說,當程序運行的時候, 會創建一個線程, 創建線程的時候, 就會在大塊的棧空間中分配一塊小空間, 用來存放當前要運行的線程的變量
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
比如,這段代碼要運行,首先會在大塊的棧空間中給他分配一塊小空間. 這里的math這個局部變量就會被保存在分配的小空間里面.
在這里面我們運行了math.compute()方法, 我們看看compute方法內部實現
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
這里面有a, b, c這樣的局部變量, 這些局部變量放在那里呢? 也放在上面分配的棧小空間里面.

效果如上圖, 在棧空間中, 分配一塊小的區域, 用來存放Math類中的局部變量
如果再有一個線程呢? 我們就會再次在棧空間中分配一塊小的空間, 用來存放新的線程內部的變量

同樣是變量, main方法中的變量和compute()方法中的變量放在一起么?他們是怎么放得呢?這就涉及到棧幀的概念。
4.2. 棧幀
1.什么是棧幀呢?
package com.lxl.jvm;
public class Math {
public static int initData = 666;
public static User user = new User();
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
還是這段代碼, 我們來看一下, 當我們啟動一個線程運行main方法的時候, 一個新的線程啟動,會現在棧空間中分配一塊小的棧空間。然后在棧空間中分配一塊區域給main方法,這塊區域就叫做棧幀空間.
當程序運行到compute()計算方法的時候, 會要去調用compute()方法, 這時候會再分配一個棧幀空間, 給compute()方法使用.
2.為什么要將一個線程中的不同方法放在不同的棧幀空間里面呢?
一方面: 我們不同方法里的局部變量是不能相互訪問的. 比如compute的a,b,c在main里不能被訪問到。使用棧幀做了很好的隔離作用。
另一方面: 方便垃圾回收, 一個方法用完了, 值也返回了, 那他里面的變量就是垃圾了, 后面直接回收這個棧幀就好了.

如下圖, 在Math中兩個方法, 當運行到main方法的時候, 會將main方法放到一塊棧幀空間, 這里面僅僅是保存main方法中的局部變量, 當執行到compute方法的時候, 這時會開辟一塊compute棧幀空間, 這部分空間僅存放compute()方法的局部變量.
不同的方法開辟出不同的內存空間, 這樣方便我們各個方法的局部變量進行管理, 同時也方便垃圾回收.
3.java內存模型中的棧算法
我們學過棧算法, 棧算法是先進后出的. 那么我們的內存模型里的棧和算法里的棧一樣么?有關聯么?
我們java內存模型中的棧使用的就是棧算法, 先進后出.舉個例子, 還是這段代碼
package com.lxl.jvm;
public class Math {
public static int initData = 666;
public static User user = new User();
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public int add() {
int a = 1;
int b = 2;
int c = a + b;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
math.add(); // 注意這里調用了兩次compute()方法
}
}
這時候加載的內存模型是什么樣呢?

- 最先進入棧的是main方法, 會首先在線程棧中分配一塊棧幀空間給main方法。
- main方法里面調用了compute方法, 然后會在創建一個compute方法的棧幀空間, 我們知道compute方法后加載,但是他卻會先執行, 執行完以后, compute中的局部變量就會被回收, 那么也就是出棧.
- 然后在執行add方法,給add方法分配一塊棧幀空間。add執行完以后出棧。
- 最后執行完main方法, main方法最后出棧. 這個算法剛好驗證了先進后出. 后加載的方法會被先執行. 也符合程序執行的邏輯。
4.3 棧幀的內部構成
我們上面說了, 每個方法在運行的時候都會有一塊對應的棧幀空間, 那么棧幀空間內部的結構是怎樣的呢?
棧幀內部有很多部分, 我們主要關注下面這四個部分:
1. 局部變量表
2. 操作數棧
3. 動態鏈接
4. 方法出口
4.2.1 局部變量表: 存放局部變量
局部變量表,顧名思義,用來存放局部變量的。
4.2.2 操作數棧
那么操作數棧,動態鏈接, 方法出口他們是干什么的呢? 我們用例子來說明操作數棧

那么這四個部分是如何工作的呢?
我們用代碼的執行過程來對照分析.
我們要看的是jvm反編譯后的字節碼文件, 使用javap命令生成反編譯字節碼文件.
javap命令是干什么用的呢? 我們可以查看javap的幫助文檔

主要使用javap -c和javap -v
javap -c: 對代碼進行反編譯
javap -v: 輸出附加信息, 他比javap -c會輸出更多的內容
下面使用命令生成一個Math.class的字節碼文件. 我們將其生成到文件
javap -c Math.class > Math.txt
打開Math.txt文件, 如下. 這就是對java字節碼反編譯成jvm匯編語言.
Compiled from "Math.java"
public class com.lxl.jvm.Math {
public static int initData;
public static com.lxl.jvm.User user;
public com.lxl.jvm.Math();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int compute();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/lxl/jvm/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: return
static {};
Code:
0: sipush 666
3: putstatic #5 // Field initData:I
6: new #6 // class com/lxl/jvm/User
9: dup
10: invokespecial #7 // Method com/lxl/jvm/User."<init>":()V
13: putstatic #8 // Field user:Lcom/lxl/jvm/User;
16: return
}
這就是jvm生成的反編譯字節碼文件.
要想看懂這里面的內容, 我們需要知道jvm文檔手冊. 現在我們不會沒關系, 參考文章(https://www.cnblogs.com/ITPower/p/13228166.html)最后面的內容, 遇到了就去后面查就行了
我們以compute()方法為例來說說這個方法是如何在在棧中處理的
源代碼
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
反編譯后的jvm指令
public int compute();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
jvm的反編譯代碼是什么意思呢? 我們對照着查詢手冊
0: iconst_1 將int類型常量1壓入操作數棧, 這句話的意思就是先把int a=1;中的1先壓入操作數棧

1: istore_1 將int類型值存入局部變量1-->意思是將int a=1; 中的a變量存入局部變量表
注意: 這里的1不是變量的值, 他指的是局部變量的一個下標. 我們看手冊上有局部變量0,1,2,3

0表示的是this, 1表示將變量放入局部變量的第二個位置, 2表示放入第三個位置.
對應到compute()方法,0表示的是this, 1表示的局部變量a, 2表示局部變量b,3表示局部變量c
1: istore_1 將int類型值存入局部變量1-->意思是將int a=1; 中的a放入局部變量表的第二個位置, 然后讓操作數棧中的1出棧, 賦值給a


2: iconst_2 將int類型常量2壓入棧-->意思是將int b=2;中的常量2 壓入操作數棧

3: istore_2 將int類型值存入局部變量2 -->意思是將int b=2;中的變量b存入局部變量表中第三個位置, 然后讓操作數棧中的數字2出棧, 給局部變量表中的b賦值為2

4: iload_1 從局部變量1中裝載int類型值--->這句話的意思是, 將操作數1從操作數棧取出, 轉入局部變量表中的a, 現在局部變量表中a=1
要想更好的理解iload_1,我們要先來研究程序計數器。
程序計數器
在JVM虛擬機中,程序計數器是其中的一個組成部分。

程序計數器是每一個線程獨有的, 他用來存放馬上要執行的那行代碼的內存位置, 也可以叫行號. 我們看到jvm反編譯代碼里,都會有0 1 2 3這樣的位置(如下圖), 我們可以將其認為是一個標識.而程序計數器可以簡單理解為是記錄這些數字的. 而實際上這些數字對應的是內存里的地址

當字節碼執行引擎執行到第4行的時候,將執行到4: iload_1, 我們可以簡單理解為程序計數器記錄的代碼位置是4. 我們的方法Math.class是放在方法區的, 由字節碼執行引擎執行, 每次執行完一行代碼, 字節碼執行引擎都會修改程序計數器的位置, 讓其向下移動一位

java虛擬機為什么要設計程序計數器呢?
因為多線程。當一個線程正在執行, 被另一個線程搶占了cpu, 這時之前的線程就要掛起, 當線程2執行完以后, 再執行線程1. 那么線程1之前執行到哪里了呢? 程序計數器幫我們記錄了.
下面執行這句話
4: iload_1 從局部變量1中裝載int類型值--> 意思是從局部變量表的第二個位置取出int類型的變量值, 將其放入到操作數棧中.此時程序計數器指向的是4

5: iload_2 從局部變量2中裝載int類型值-->意思是將局部變量中的第三個int類型的元素b的值取出來, 放到操作數棧, 此時程序計數器指向的是5

6: iadd 執行int類型的加法 ---> 將兩個局部變量表中的數取出, 進行加法操作, 此操作是在cpu中完成的, 將執行后的結果3在放入到操作數棧 ,此時程序計數器指向的是6


7: bipush 10 :將一個8位帶符號整數壓入棧 --> 這句話的意思是將10壓入操作數棧

我們發現這里的位置是7, 但是下一個就變成了9, 那8哪里去了呢? 其實這里的0 1 2 3 ...都是對應的內存地址, 我們的乘數10也會占用內存空間, 所以, 8的位置存的是乘數10
9: imul 執行int類型的乘法 --> 這個和iadd加法一樣, 首先將操作數棧中的3和10取出來, 在cpu里面進行計算, 將計算的結果30在放回操作數棧
乘法操作是在cpu的寄存器中進行計算的. 我們這里說的都是保存在內存中.

10: istore_3 將int類型值存入局部變量表中 ---> 意思是是將c這個變量放入局部變量表, 然后讓操作數棧中的30出棧, 賦值給變量c

11: iload_3 從局部變量3中裝載int類型值 --> 將局部變量表中取出第4個位置的值30, 裝進局部變量表

12: ireturn 從方法中返回int類型的數據 --> 最后將得到的結果c返回.
這個方法中的變量是如何在操作數棧和局部變量表中轉換的, 我們就知道了. 現在應該可以理解操作數棧和局部變量表了吧~~~
總結:什么是操作數棧?**
在運算的過程中, 常數1, 2, 10, 也需要有內存空間存放, 那么它存在哪里呢? 就保存在操作數棧里面
操作數棧就是在運行的過程中, 一塊臨時的內存中轉空間
4.3.3 動態鏈接
在之前說過什么是動態鏈接: 參考文章: https://www.cnblogs.com/ITPower/p/13197220.html 搜索:動態鏈接
靜態鏈接是在程序加載的時候一同被加載進來的. 通常用靜態常量, 靜態方法等, 因為他們在內存地址中只有一份, 所以, 為了性能, 就直接被加載進來了
而動態鏈接, 是使用的時候才會被加載進來的鏈接, 比如compute方法. 只要在執行到math.compute()方法的時候才會真的進行加載.
4.3.4 方法出口
當我們運行完compute()方法以后, 還要返回到main方法的math.comput()方法的位置, 那么他怎么返回回來呢?返回回來以后該執行哪一句代碼了呢?在進入compute()方法之前,就在方法出口里記錄好了, 我應該如何返回,返回到哪里. 方法出口就是記錄一些方法的信息的.
五. 堆和棧的關系
上面研究了compute()方法的棧幀空間,再來看一下main方法的棧幀空間。整體來說,都是一樣的,但有一塊需要說明一下,那就是局部變量表。來看看下面的代碼
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
main方法的局部變量和compute()有什么區別呢? main方法中的math是一個對象. 我們知道通常對象是被創建在堆里面的. 而math是在局部變量表中, 記錄的是堆中new Math對象的地址。
說的明白一些,math里存放的不是具體的內容,而是實例對象的地址。

那么棧和堆的關系就出來了, 如果棧中有很多new對象, 這些對象是創建在堆里面的. 棧里面存的是這些堆中創建的對象的內存地址。
六. 方法區
我們可以通過javap -v Math.class > Math.txt命令, 打印更詳細的jvm反編譯后的代碼

這次生成的代碼,和使用javap -c生成的代碼的區別是多了Constant pool常量池。這些常量池是放在哪里的呢?放在方法區。這里看到的常量池叫做運行時常量池。還有很多其他的常量池,比如:八大數據類型的對象常量池,字符串常量池等。
這里主要理解運行時常量池。運行時常量池放在方法區里。
方法區主要有哪些元素呢?
常量 + 靜態變量 + 類元信息(就是類的代碼信息)
在Math.class類中, 就有常量和靜態常量
public static int initData = 666;
public static User user = new User();
他們就放在方法區里面. 這里面 new User()是放在堆里面的, 在堆中分配了一個內存地址,而user對象是放在方法區里面的. 方法區中user對象指向了在堆中分配的內存空間。
堆和方法區的關系是: 方法區中對象引用的是堆中new出來的對象的地址
類元信息: Math.class整個類中定義的內容就是類元信息, 也放在方法區。
七. 本地方法棧
本地方法棧是有c++代碼實現的方法. 方法名帶有native的代碼.
比如:
new Thread().start();
這里的start()調用的就是本地方法

這就是本地方法
本地方法棧: 運行的時候也需要有內存空間去儲存, 這些內存空間就是本地方法棧提供的

每一個線程都會分配一個棧空間,本地方法棧和程序計數器。如上圖main線程:包含線程棧,本地方法棧,程序計數器。