站長資訊網
        最全最豐富的資訊網站

        JVM學習之 Java內存結構

        JVM學習之 Java內存結構

        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內存結構
        JVM體系結構如圖所示
        JVM學習之 Java內存結構
        一個類從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.棧的特點

        棧類似現實生活中的子彈夾。棧最重要的特點是后進先出。
        JVM學習之 Java內存結構
        如圖,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.棧的問題辨析

        1. 垃圾回收是否涉及棧內存?
          不涉及,垃圾回收只是回收堆內存中的無用對象,棧內存不需要對它執行垃圾回收,隨著方法的調用結束,棧內存就釋放了。
        2. 棧內存分配越大越好嗎?
          首先棧內存可以指定:-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();     }}

        運行結果如下
        JVM學習之 Java內存結構

        JVM學習之 Java內存結構
        這里報了錯誤StackOverflowError
        總共進行了22846次遞歸調用

        idea中設置棧內存大小:
        JVM學習之 Java內存結構
        將棧內存設置的小一點,發現5000多次遞歸調用就溢出了。

        棧內存溢出代碼演示2(第三方依賴庫出現):
        JVM學習之 Java內存結構
        本案例可以使用JsonIgnore注解解決循環依賴,數據轉換時,只讓部門類去關聯員工類,員工類不再關聯部門類,在員工類的部門屬性(dept)上加@JsonIgnore注解。具體使用詳情可以點擊此處查看

        3.6.線程運行診斷

        3.6.1.案例1:cpu占用過多(linux系統為例)

        排查步驟:

        1.在linux中使用top命令,去查看后臺進程對cpu的占用情況
        注意,在這之前我們運行了一道Java程序
        JVM學習之 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
        JVM學習之 Java內存結構
        當線程數太多,排查不方便的話,我們可以用grep pid來進行篩選,過濾掉不感興趣的進程
        ps H -eo pid,tid,%cpu |grep 32655

        3.定位到是哪個線程占用內存過高后,再使用Jdk提供的命令(jstack+進程id)去查看進程中各線程的運行信息,需要把第二步中查到的線程id(十進制)轉為十六進制,然后進行比較查詢到位置后判斷異常信息
        JVM學習之 Java內存結構
        thread1,thread2,thread3是我們自己定義的線程。
        可以根據線程id,找到有問題的線程,進一步定位到問題代碼的源碼行號
        JVM學習之 Java內存結構

        3.6.2.案例2:線程診斷_遲遲得不到結果

        仍然通過jdk提供的 jstack+進程id的方式,去查看進程中各個線程的運行信息
        JVM學習之 Java內存結構
        JVM學習之 Java內存結構

        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);         }}

        JVM學習之 Java內存結構
        報了錯誤java.lang.OutOfMemoryError
        代碼中每次都拼接一個hello,由于定義的list集合創建在try語句里面,所以在for循環不斷執行過程中,list集合是不會被回收的,只要程序還沒到catch之前,它就一直有效。而字符串對象都被追加到了集合內部,字符串對象由于一直被使用,所以不會被回收。
        我們可以通過-Xmx來設置堆空間大小。
        JVM學習之 Java內存結構
        我們把堆內存改成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異常。

        文原文關于虛擬機的定義:

        JVM學習之 Java內存結構

        6.2.定義

        jdk1.8之前,方法區是用的堆內存,1.8之后,方法區用的操作系統內存。
        這塊不是太清晰,可以參考下此篇博客點擊查看
        常量池分為靜態常量池和動態常量池,下圖中的常量池指的是動態常量池,因為它們已經被讀入內存中去,而靜態常量池存在于class文件中
        JVM學習之 Java內存結構

        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.常量池

        JVM學習之 Java內存結構

        常量池,就是一張表,虛擬機指令根據這站常量表找到要執行的類名、方法名、參數類型、字面量信息(如字符串常量、true和false)
        運行時常量池,常量池是.class文件中的,當該類被加載,它的常量池信息就會放入運行時常量池,并把里面的符號地址變為真實地址*。

        public class HelloWorld { 	public static void main(String[] args) { 		System.out.println("hello,world"); 	}}

        以上是一個helloworld程序,helloworld要運行,肯定要先編譯成一個二進制字節碼。
        二進制字節碼由類的基本信息、常量池、類方法定義(包含了虛擬機指令)
        反編譯HelloWorld(之前需要運行將.java文件編譯成.class文件)
        使用idea工具
        JVM學習之 Java內存結構

        F:IDEAprojectsjvm>javap -v F:IDEAprojectsjvmoutproductionuntitledHelloWorld.class

        F:IDEAprojectsjvmoutproductionuntitled是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 等等
        所以常量池的作用就是給我們指令提供一些常量符號,根據這些常量符號,我們就可以根據查表的方式去找到它,這樣虛擬機才能成功的執行它。

        相關免費學習推薦:java基礎教程

        贊(0)
        分享到: 更多 (0)
        網站地圖   滬ICP備18035694號-2    滬公網安備31011702889846號
        主站蜘蛛池模板: 亚洲国产精品无码久久一线| 国产高清在线精品一区| 国内精品久久久久久久97牛牛| 国产精品户外野外| 国产成人亚洲综合无码精品| 人人妻人人澡人人爽精品欧美| 久久精品国产99国产精品澳门| 亚洲av无码乱码国产精品| 日本欧美国产精品第一页久久| 99久久精品国产毛片| 精品无码一区二区三区爱欲九九| 亚洲av无码国产精品色在线看不卡| 国产成人亚洲精品影院| 久久91精品国产91久久小草| 国产亚洲精品岁国产微拍精品| 亚洲精品国产首次亮相| 亚洲国产精品自产在线播放| 91探花国产综合在线精品| 久久99国产精品二区不卡| 国产精品白浆在线观看免费| 久久99国产综合精品免费| 亚洲AV无码国产精品色午友在线| 亚洲国产精品日韩专区AV| 日韩精品一区二区三区视频| 欧美 日韩 精品 另类视频| 国产精品户外野外| 成人国产精品动漫欧美一区| 日本精品卡一卡2卡3卡四卡| 99热这里只有精品在线| 99精品视频在线观看re| 91在线视频精品| 97精品一区二区视频在线观看 | 欧美日韩精品一区二区三区不卡 | 久久精品国产91久久综合麻豆自制| 精品国精品国产自在久国产应用| 精品永久久福利一区二区| 国产色婷婷五月精品综合在线 | 国产精品毛片久久久久久久| 国产精品一区在线播放| 精品亚洲综合在线第一区| 伊人久久大香线蕉精品|