Java記憶體結構
- 1.JVM概述
- 2.程式計數器
- 2.1.定義
- 2.2.作用及特點解釋
- 3.虛擬機堆疊
- 3.1.堆疊的特點
- 3.2.堆疊的演示
- 3.3.堆疊的問題辨析
- 3.4.堆疊的執行緒安全問題
- 3.5.堆疊記憶體溢位(StackOverflowError)
- 3.6.執行緒運行診斷
- 3.6.1.案例1:cpu占用過多(linux系統為例)
- 3.6.2.案例2:執行緒診斷_遲遲得不到結果
- 4.本地方法堆疊
- 5.堆
- 5.1.定義
- 5.2.堆記憶體溢位(OutOfMemoryError:Java heap space)
- 5.3.堆記憶體診斷
- 6.方法區
- 6.1.定義
- 6.2.定義
- 6.3.方法區記憶體溢位(OutOfMemoryError: Metaspace)
- 6.4.常量池
1.JVM概述
定義:
JVM全稱是Java Virtual Machine-java程式的運行環境(java二進制位元組碼的運行環境)
好處:
- 一次撰寫,到處運行(跨平臺)
- 自動記憶體管理,垃圾回收功能
- 陣列下標越界檢查
- 多型
比較JVM,JRE,JDK之間的聯系和區別,我們可以用一張圖來解釋

JVM體系結構如圖所示

一個類從Java源代碼(.java檔案)編譯成了Java二進制位元組碼以后,必須經過類加載器才能被加載到JVM里面才能運行,
我們一般把類放在方法區里,類將來創建的物件放在堆的部分,而堆里面的物件在呼叫方法時會用到虛擬機堆疊和程式計數器以及本地方發展,
方法執行時每行代碼是由執行引擎中的解釋器逐行進行執行的,方法里面的熱點代碼也就是頻繁呼叫的代碼,由即時編譯器來編譯執行,GC會對垃圾進行回收,
我們可以通過本地方法介面來呼叫作業系統提供的功能,
JVM的記憶體結構包括:
1.方法區
2.程式計數器
3.虛擬機堆疊
4.本地方法堆疊
5.堆
2.程式計數器
2.1.定義
Program Counter Register程式計數器(暫存器)
作用:
??是記住下一條jvm指令的執行地址
特點
??是執行緒私有的
??不會存在記憶體溢位(記憶體結構中唯一一個不會記憶體溢位的結構)
在2.2中我們將會解釋程式計數器的作用及特點,
2.2.作用及特點解釋
二進制位元組碼 JVM指令 Java源代碼
0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // -
4: aload_1 // out.println(1);
5: iconst_1 // -
6: invokevirtual #26 // -
9: aload_1 // out.println(2);
10: iconst_2 // -
11: invokevirtual #26 // -
14: aload_1 // out.println(3);
15: iconst_3 // -
16: invokevirtual #26 // -
19: aload_1 // out.println(4);
20: iconst_4 // -
21: invokevirtual #26 // -
24: aload_1 // out.println(5);
25: iconst_5 // -
26: invokevirtual #26 // -
29: return
我們可以看到這些代碼,第一行System.out賦值給了一個變數,在4:中去呼叫println()方法,然后依次列印1,2,3,4,5,這些指令不能直接交給CPU來執行,必須經過解釋器的作用,它負責把一條一條的位元組碼指令解釋成機器碼,然后機器碼就可以交給CPU來執行,
也就是
二進制位元組碼->解釋器->機器碼->CPU
實際上程式計數器的作用就是在指令的執行程序中,記住下一條JVM指令的執行地址,
上面我們二進制位元組碼前面的數字0,3,4…我們可以把其理解為地址,根據這些地址資訊,我們就可以找到命令來執行,
在每次拿到指令交給CPU執行之后,程式計數器就會把下一條指令的地址放入到程式計數器中,等一條指令執行完成之后,解釋器就會到程式計數器中取到下一條指令的地址,再把其經過解釋器解釋成機器碼然后交給CPU執行,然后一直重復這樣的程序,
在物理上,實作程式計數器是通過暫存器來實作的,暫存器是CPU組件里讀取最快的存盤單元,
程式計數器是執行緒私有的
假如說上述代碼都在執行緒1中運行,同時運行的還有執行緒2和執行緒3,多個執行緒運行的時候,CPU會給每個執行緒分配時間片,給執行緒1分配時間片,如果執行緒1在指定的時間沒有運行完,它就會把狀態暫存,切換到執行緒2,執行緒2執行自己的代碼,執行緒2執行完了,再繼續執行執行緒1的代碼,在執行緒切換的程序中,我們要記住下一條指令的執行地址,就需要用到程式計數器,假如說執行緒1剛開始執行到第9行代碼,恰好這個時候時間片用完,CPU切換到執行緒2去執行,這時它就會把下一條指令的地址10記錄到程式計數器里面,而且程式計數器是執行緒私有的,它是屬于執行緒1的,等執行緒2代碼執行完了,執行緒1搶到了時間片,它就會從自己的程式計數器里面取出下一行代碼,每個執行緒都有自己的程式計數器
3.虛擬機堆疊
3.1.堆疊的特點
堆疊類似現實生活中的子彈夾,堆疊最重要的特點是后進先出,

如圖,1是最先進入堆疊中的,3是最后進入堆疊中的,但是在出堆疊的時候,3最先出堆疊,1最后出堆疊,即他們按照1,2,3的順序入堆疊,按照3,2,1的順序出堆疊
虛擬機堆疊就是我們執行緒運行時需要的記憶體空間,一個執行緒運行時需要一個堆疊,如果將來有多個執行緒的話,它就會有多個虛擬機堆疊,
每個堆疊可以看成是由多個堆疊幀組成,例如上圖中每個元素1,2,3都可以看成是堆疊幀,
一個堆疊幀就對應著Java中一個方法的呼叫,即堆疊幀就是每個方法運行時需要的記憶體,每個方法運行時需要的記憶體一般有引數,區域變數,回傳地址,這些都需要占用記憶體,所以每個方法執行時,都要預先把這些記憶體分配好,
當我們呼叫第一個方法堆疊幀時,它就會給第一個方法分配堆疊幀空間,并且壓入堆疊內,當這個方法執行完了,就會把這個方法堆疊幀出堆疊,釋放這個方法所占用的記憶體,
一個堆疊內可能有多個堆疊幀存在,
總結
Java Virtual Machine Stacks(Java虛擬機堆疊)
- 每個執行緒運行時所需要的記憶體,稱為虛擬機堆疊
- 每個堆疊由多個堆疊幀(Frame)組成,對應著每次方法呼叫時所占用的記憶體
- 每個執行緒只能有一個活動堆疊幀,對應著當前正在執行的那個方法(位于堆疊頂)
活動堆疊幀表示執行緒正在執行的方法,
3.2.堆疊的演示
public class teststacks {
public static void main(String[] args) throws InterruptedException{
method1();
}
public static void method1(){
method2(1,2);
}
public static int method2(int a,int b){
int c=a+b;
return c;
}
}
可以自行除錯以上代碼來觀察堆疊中的變化情況,
入堆疊順序:main->method1->method2
出堆疊順序:method2->method1->main
3.3.堆疊的問題辨析
- 垃圾回收是否涉及堆疊記憶體?
不涉及,垃圾回收只是回收堆記憶體中的無用物件,堆疊記憶體不需要對它執行垃圾回收,隨著方法的呼叫結束,堆疊記憶體就釋放了, - 堆疊記憶體分配越大越好嗎?
首先堆疊記憶體可以指定:-Xss size(如果不指定堆疊記憶體大小,不同系統會有一個不同的默認值)
其次由于電腦記憶體一定,假如有100Mb,如果給堆疊記憶體指定為2Mb,則最多只能存在50個執行緒,所以并不是越大越好,堆疊記憶體較大一般是可以進行較多次的方法遞回呼叫,而不會增強執行緒效率,反而會使執行緒數量減少,一般使用默認大小,
3.4.堆疊的執行緒安全問題
看一個變數是否執行緒安全,首先就是看這個變數對多個執行緒是共享的還是私有的,共享的變數需要考慮執行緒安全,
其次區域變數也不能保證是執行緒安全的,需要看此變數是否逃離了方法的作用范圍(作為引數和回傳值逃出方法作用范圍時需要考慮執行緒安全問題)
例如:
以下代碼中區域變數是私有的,是執行緒安全的
//多個執行緒同時執行該方法,會不會造成x值混亂呢?
//不會,因為x是方法內的區域變數,是執行緒私有的,互不干擾
static void m1(){
int x=0;
for(int i=0;i<5000;i++){
x++;
}
System.out.println(x);
}
但是如果我們把變數的型別改為static,此時就大不一樣了,x是靜態變數,執行緒1和執行緒2同時擁有同一個x,static變數針對多個執行緒是一個共享的,不加安全保護的話,就會出現執行緒安全問題,
static void m1(){
static int x=0;
for(int i=0;i<5000;i++){
x++;
}
System.out.println(x);
}
我們再看幾個方法
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
}
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
m1是執行緒安全的:m1中的sb是執行緒中的區域變數,它是屬于執行緒私有的
m2執行緒不安全:sb它是方法的引數,有可能有其它的執行緒訪問到它,它就不再是執行緒私有的了,它對多個執行緒是共享的,
m3不是執行緒安全的:它被當成回傳結果回傳了,回傳了有可能其它的執行緒拿到這個物件,從而并發的修改,
3.5.堆疊記憶體溢位(StackOverflowError)
什么情況下會導致堆疊記憶體溢位吶?
1.堆疊幀過多導致堆疊記憶體溢位(一般遞回呼叫次數太多,進堆疊太多導致溢位)
這里最容易出現的場景是函式的遞回呼叫,
2.堆疊幀過大導致堆疊記憶體溢位(不太容易出現)
堆疊記憶體溢位代碼演示1(自己開發):
測驗以下的程式,其中遞回函式沒有遞回邊界
public class Demo1_2 {
private static int count;
public static void main(String[] args) {
try {
method1();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(count);
}
}
private static void method1() {
count++;
method1();
}
}
運行結果如下

…

這里報了錯誤StackOverflowError,
總共進行了22846次遞回呼叫
idea中設定堆疊記憶體大小:

將堆疊記憶體設定的小一點,發現5000多次遞回呼叫就溢位了,
堆疊記憶體溢位代碼演示2(第三方依賴庫出現):

本案例可以使用JsonIgnore注解解決回圈依賴,資料轉換時,只讓部門類去關聯員工類,員工類不再關聯部門類,在員工類的部門屬性(dept)上加@JsonIgnore注解,具體使用詳情可以點擊此處查看
3.6.執行緒運行診斷
3.6.1.案例1:cpu占用過多(linux系統為例)
排查步驟:
1.在linux中使用top命令,去查看后臺行程對cpu的占用情況
注意,在這之前我們運行了一道Java程式

Java代碼占用了CPU的99.3%.top命令只能定位到行程,而無法定位到執行緒,
2.查看執行緒對cpu的占用情況:ps H -eo pid,tid,%cpu
如果顯示過多,可使用ps H -eo pid,tid,%cpu | grep 行程id,過濾掉不想看的部分行程
注意:ps不僅可以查看行程,也可以查看執行緒對CPU的占用情況,H把行程中的執行緒所有資訊都展示出來,-eo規定輸出感興趣的內容,這里我們想看看pid,tid和CPU的占用情況%cpu

當執行緒數太多,排查不方便的話,我們可以用grep pid來進行篩選,過濾掉不感興趣的行程
ps H -eo pid,tid,%cpu |grep 32655
3.定位到是哪個執行緒占用記憶體過高后,再使用Jdk提供的命令(jstack+行程id)去查看行程中各執行緒的運行資訊,需要把第二步中查到的執行緒id(十進制)轉為十六進制,然后進行比較查詢到位置后判斷例外資訊,

thread1,thread2,thread3是我們自己定義的執行緒,
可以根據執行緒id,找到有問題的執行緒,進一步定位到問題代碼的原始碼行號

3.6.2.案例2:執行緒診斷_遲遲得不到結果
仍然通過jdk提供的 jstack+行程id的方式,去查看行程中各個執行緒的運行資訊


4.本地方法堆疊
含義:Java虛擬機呼叫本地方法時,需要給本地方法提供的一些記憶體空間
本地方法不是由Java撰寫的代碼,由于Java有時不能直接和作業系統打交道,所以需要用C/C++語言來與作業系統打交道,那么Java就可以通過呼叫本地方法來獲得這些功能,本地方法非常的多,如Object類的clone(),hashCode方法,wait方法,notify方法等
public native int hashCode();
5.堆
5.1.定義
1.虛擬機堆疊,程式計數器,本地方法堆疊,這些都是執行緒私有的,而堆和方法區,是執行緒公用的一塊記憶體區域,
2.通過new關鍵字創建的物件都會使用堆記憶體
3.由于堆是執行緒共享的,堆內的物件都要考慮執行緒安全問題(也有一些例外)
4.堆有垃圾回識訓制,不再被參考的物件會被回收
5.2.堆記憶體溢位(OutOfMemoryError:Java heap space)
物件一直存在于堆中未被回收,且占用記憶體越來越大,最終導致堆記憶體溢位(雖然堆中有垃圾回識訓制,但垃圾回識訓制不是回收所有的物件)
我們可以看看下面的代碼
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a); // hello, hellohello, hellohellohellohello ...
a = a + a; // hellohellohellohello
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}

報了錯誤java.lang.OutOfMemoryError
代碼中每次都拼接一個hello,由于定義的list集合創建在try陳述句里面,所以在for回圈不斷執行程序中,list集合是不會被回收的,只要程式還沒到catch之前,它就一直有效,而字串物件都被追加到了集合內部,字串物件由于一直被使用,所以不會被回收,
我們可以通過-Xmx來設定堆空間大小,

我們把堆記憶體改成8M(之前記憶體是4G),此時只運行了17次,
5.3.堆記憶體診斷
1.jps工具:jps,查看當前行程中有哪些Java行程,并將行程id顯示出來(idea中通過terminal命令列輸入命令)
2.jmap工具:jmap -heap 行程id 查詢某一個時刻堆記憶體的占用情況
3.jconsole工具:圖形界面的,多功能監測工具,可連續監測,使用流程圖如下(1-2-3):
6.方法區
6.1.定義
方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,他用于存盤已被虛擬機加載的類資訊、常量、靜態常量、即時編譯器編譯后的代碼等資料,(與類有關的資訊),雖然Java虛擬機規范把方法區描述為堆的一個邏輯部分,但是他卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來,方法區在虛擬機啟動時創建,
對于習慣在HotSpot虛擬機上開發、部署程式的開發者來說,很多都更愿意把方法取稱為“永久代”(Permanent Generation),本質上兩者并不等價,僅僅是因為HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實作方法區而已,這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分記憶體,能夠省去專門為方法區撰寫記憶體管理代碼的作業,對于其他虛擬機(如BEA JRockit、IBM J9等)來說是不存在永久代的概念的,原則上,如何實作方法區屬于虛擬機實作細節,不受虛擬機規范約束,但使用永久代來實作方法區,現在看來并不是一個好主意,因為這樣更容易遇到記憶體溢位問題(永久代有-XX:MaxPermSize的上限,J9和JRockit只要沒有觸碰到行程可用記憶體的上限,例如32位系統中的4GB,就不會出現問題),而且有極少數方法(例如String.intern())會因這個原因導致不同虛擬機下有不同的表現,因此,對于HotSpot虛擬機,根據官方發布的路線圖資訊,現在也已放棄永久代并逐步改為采用Navtive Memory來實作方法區的規劃,在JDK1.7的HostSpot中,已經把原本放在永久代的字串常量池移出,jdk1.8中后稱作元空間,用的作業系統記憶體,
Java虛擬機規范對方法區的限制非常寬松,除了和Java堆一樣不需要連續的記憶體和可以喧囂而固定大小或者可擴展外,還可以選擇不實作垃圾收集,相對而言,垃圾收集行為在這個區域是比較少出現的,但并非資料進入了方法區就如永久代的名字一樣“永久”存在了,這區域的記憶體回收目標主要是針對常量池的回收和對型別的卸載,一般來說,這個區域的回收“成績”比較難以令人滿意,尤其是型別的卸載,條件相當苛刻,但是這部磁區域的回收確實是必要的,在Sun公司的BUG串列中,曾出現過的若干個嚴重的BUG就是由于低版本的HotSpot虛擬機對此區域未完全回收而導致記憶體泄漏,
根據Java虛擬機規范的規定,當方法區無法滿足記憶體分配需求時,將拋出OutOfMemoryError例外,
文原文關于虛擬機的定義:

6.2.定義
jdk1.8之前,方法區是用的堆記憶體,1.8之后,方法區用的作業系統記憶體,
這塊不是太清晰,可以參考下此篇博客點擊查看
常量池分為靜態常量池和動態常量池,下圖中的常量池指的是動態常量池,因為它們已經被讀入記憶體中去,而靜態常量池存在于class檔案中

6.3.方法區記憶體溢位(OutOfMemoryError: Metaspace)
1.8以前會導致永久代記憶體溢位
1.8以后會導致元空間記憶體溢位
/**
* 演示元空間記憶體溢位 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
*/
public class Demo1_8 extends ClassLoader { // 可以用來加載類的二進制位元組碼
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 10000; i++, j++) {
// ClassWriter 作用是生成類的二進制位元組碼
ClassWriter cw = new ClassWriter(0);
//引數:版本號, public, 類名, 包名, 父類, 介面
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 生成類,二進制位元組碼用byte來表示,回傳 byte[]
byte[] code = cw.toByteArray();
// 執行了類的加載
test.defineClass("Class" + i, code, 0, code.length); // Class 物件
}
} finally {
System.out.println(j);
}
}
}
jdk1.8以后, 默認情況下,方法區用的是系統記憶體,所以加大還是不會導致記憶體溢位,回圈很多次都運行成功,
當設定了-XX:MaxMetaspaceSize=8m,到了5411次就溢位了,報的是java.lang.OutOfMemoryError: Metaspace錯誤
而1.8以前永久代溢位報的錯誤是java.lang.OutOfMemoryError:PermGen space
6.4.常量池

常量池,就是一張表,虛擬機指令根據這站常量表找到要執行的類名、方法名、引數型別、字面量資訊(如字串常量、true和false),
運行時常量池,常量池是.class檔案中的,當該類被加載,它的常量池資訊就會放入運行時常量池,并把里面的符號地址變為真實地址*,
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello,world");
}
}
以上是一個helloworld程式,helloworld要運行,肯定要先編譯成一個二進制位元組碼,
二進制位元組碼由類的基本資訊、常量池、類方法定義(包含了虛擬機指令),
反編譯HelloWorld(之前需要運行將.java檔案編譯成.class檔案)
使用idea工具

F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\HelloWorld.class
F:\IDEA\projects\jvm\out\production\untitled\是HelloWorld.class所在的路徑
顯示類的詳細資訊
Classfile /F:/IDEA/projects/jvm/out/production/untitled/HelloWorld.class
Last modified 2021-1-30; size 533 bytes
MD5 checksum 82d075eb7217b4d23706f6cfbd44f8f1
Compiled from "HelloWorld.java"
public class HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
可以看到類的檔案,最后修改時間,簽名,以及版本等等,有的還有訪問修飾符、父類和介面等詳細資訊,
顯示常量池
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello,world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LHelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello,world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
顯示方法定義
{
public HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LHelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello,world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
第一個方法是public HelloWorld();它是編譯器自動為我們構造的無參構造方法,
第二個是public static void main(java.lang.String[]);即main方法
方噶里面就包括了虛擬機的指令了,
getstatic獲取一個靜態變數,即獲取System.out靜態變數
ldc是加載一個引數,引數是字串hello,world
invokevirtual虛方法呼叫,println方法
return執行結束,
我們getstatic、ldc、invokevirtual后面都有一個#2,#3,#4,在解釋器翻譯這些虛擬機指令的時候,它會把這些#2,#3,#4進行一個查表翻譯,比如getstatic #2,就去查常量池的表,在常量池中
#2 = Fieldref #21.#22 參考的是成員變數#21,#22.
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
然后再去找#28.29,30
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
所以現在我就知道了,我是要找到java.lang.system類下叫out的成員變數,型別是java/io,
同理,ldc是找#3 = String #23 Utf8 hello,world,它是虛擬機常量池的一個字串,把helloworld常量變成字串物件加載進來,
invokevirtual #4 Methodref #24.#25 等等
所以常量池的作用就是給我們指令提供一些常量符號,根據這些常量符號,我們就可以根據查表的方式去找到它,這樣虛擬機才能成功的執行它,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/255200.html
標籤:java
