<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      深入學習JVM-內存架構圖(二)

      JVM深入學習-內存架構圖篇

      本篇聚焦于對JVM內存架構圖的深度總結與解析。文中將逐一詳盡介紹內存架構圖中的各部分,并深入理解JVM運行機制與內存管理策略。

      內存架構圖

      image-20231102083110541

      JVM架構圖中包含了 類加載子系統(tǒng)(上篇JVM詳細介紹了類加載系統(tǒng))、運行時數(shù)據(jù)區(qū)、執(zhí)行引擎、本地接口、本地方法庫。

      • 對于JVM內存模型,在jdk1.8時做了調整,將Method Area(方法區(qū)/永久代)轉換為元空間并放在本地內存中。
      • 類加載子系統(tǒng)負責將類的字節(jié)碼加載至運行時數(shù)據(jù)區(qū)中的方法區(qū)中,并在堆內存中生成Class對象作為類信息的訪問入口。

      方法區(qū)

      方法區(qū)(Method Area)是Java虛擬機內存結構中的一個重要組成部分,它是線程共享的區(qū)域。這意味著多個線程可以同時訪問方法區(qū)中的信息。

      方法區(qū)的主要作用是存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器(JIT)編譯后的代碼等數(shù)據(jù)。它就像一個知識庫,為后續(xù)Java程序的運行提供各種信息以確保程序可以運行。

      • 類信息
        • 類全限定名:完整類名(包含包名和類名),用于在JVM中表示唯一的標識一個類。如com.example.MyClass這個類,com.example是包名,MyClass是類名,這個全限定名可以幫助 JVM 在眾多類中準確地定位和區(qū)分不同的類。
        • 字段信息:包含變量名稱、類型、訪問修飾符。用于JVM能夠了解類中包含那些字段。
        • 方法信息:包含方法的名稱、返回類型、參數(shù)列表(參數(shù)的類型和順序)以及方法的訪問修飾符等。JVM 通過這些信息來確定如何調用方法以及方法的調用規(guī)則。
        • 接口調用:如果一個類實現(xiàn)了接口,方法區(qū)會存儲接口的相關信息,包括接口的全限定名、接口中定義的方法簽名等。這有助于 JVM 檢查類是否正確地實現(xiàn)了接口以及在運行時實現(xiàn)接口相關的多態(tài)調用。
      • 靜態(tài)變量:類相關的靜態(tài)變量是在類初始化階段完成的。并且這些靜態(tài)變量在整個程序的生命周期內都存儲在方法區(qū)中,并可以被類的例實訪問
      • JIT優(yōu)化代碼:JIT(Just - In - Time)編譯是一種優(yōu)化技術,JVM 在運行過程中會對頻繁執(zhí)行的熱點代碼(Hot - Spot Code)進行動態(tài)編譯。這些被 JIT 編譯后的代碼會存儲在方法區(qū)中。JIT 編譯將字節(jié)碼轉換為機器碼,提高了代碼的執(zhí)行效率。例如,對于一個頻繁執(zhí)行的循環(huán)體或者經(jīng)常調用的方法,JVM 可能會對其進行 JIT 編譯,使得后續(xù)的執(zhí)行可以直接使用編譯后的機器碼,而不是每次都進行字節(jié)碼解釋。
      • 運行時常量池:是方法區(qū)的一部分,它是在類加載過程中由字節(jié)碼文件中的常量池轉換而來的。常量池中的信息包括字面量和符號引用,在類加載后,這些信息會被解析并存儲到運行時常量池中。
        • 字面量:字面量是在代碼中直接出現(xiàn)的常量值。例如,在String str = "hello";中,"hello"就是一個字面量。在類加載過程中,字面量會被存儲到運行時常量池中,并將其轉換為在內存中的實際表示形式,通常是一個指向常量值的內存地址。這樣,在程序運行過程中,當需要訪問這個字面量時,可以通過這個內存地址快速獲取。
        • 符號引用:符號引用是一種在編譯階段對類、方法、字段等的引用表示方式。例如,在代碼中的一個方法調用myMethod(),在編譯階段這只是一個符號引用,它表示需要調用一個名為myMethod的方法,但并沒有確定這個方法的實際內存地址。在類加載過程中,符號引用會被解析并轉換為直接引用,也就是確定方法的實際內存位置,這個過程也是在運行時常量池中完成的。這些直接引用信息會被存儲在運行時常量池中,以便在程序運行時能夠準確地調用相應的方法或訪問相應的字段。

      元空間替代永久代?

      image-20241205163305081

      內存大小:在jdk1.8之前,永久代是放在堆內存中的,也就是說在JVM內存中,但是隨著項目的復雜度、框架使用、三方庫的使用,永久代中需要存儲的類越來越多,導致固定內存大小的永久代無法適用,所以這里把永久代轉換成元空間,并把它放到本地內存中,不占用jvm內存空間。

      垃圾回收:永久代的垃圾回收相對復雜。因為它里面存儲的類信息等數(shù)據(jù)的生命周期和普通對象不同,垃圾回收器很難準確判斷哪些類信息是可以回收的。例如,一個類加載后,即使沒有任何實例對象存在了,只要這個類還在被其他類引用(比如通過反射),它在永久代中的信息就不能被回收。這種復雜的回收機制導致永久代的內存清理效率較低。

      性能問題:永久代中的類數(shù)據(jù)和字符串常量池等內容混在一起,當進行垃圾回收或者內存整理時,會對整個永久代進行操作。永久代的垃圾回收會觸發(fā) Full GC,這是非常耗時的過程,在高負載系統(tǒng)中影響較大。而元空間獨立于堆內存,大大減少了永久代相關的 Full GC 次數(shù),因此在運行時減少了長時間的中斷。

      方法區(qū)和其他內存結構的關系?

      • 方法區(qū)與 Java 虛擬機棧(Java Virtual Machine Stack)也有關聯(lián)。在方法調用過程中,Java 虛擬機棧中的棧幀會包含對方法區(qū)中方法信息的引用。例如,當一個方法被調用時,棧幀中的動態(tài)鏈接部分會根據(jù)方法的符號引用在方法區(qū)中查找并確定方法的實際執(zhí)行版本,從而實現(xiàn)方法的正確調用。
      • 方法區(qū)與堆區(qū)(Heap)關系:堆區(qū)是對象實例的存儲之地。當執(zhí)行new操作創(chuàng)建對象時,JVM 需要依據(jù)方法區(qū)中存儲的類信息來構建對象。在垃圾回收過程中,方法區(qū)中的類信息對堆區(qū)對象的回收策略起到關鍵作用。堆區(qū)對象的可達性分析(判定對象是否可被回收的重要步驟)不僅考慮對象之間的引用關系,還涉及對象與方法區(qū)中類信息的關聯(lián)。例如,當垃圾回收器在堆區(qū)回收對象時,它需要根據(jù)對象的類信息(如是否有finalize方法等)來確定回收的方式和順序。

      棧區(qū)

      棧區(qū)是JVM內存結構中的一個重要組成部分,負責管理方法調用和執(zhí)行時的數(shù)據(jù)存儲。

      在Java虛擬機中,棧與線程密切相關。每個線程在創(chuàng)建時都會分配一個JVM棧。這個棧用于存儲方法調用時的相關信息,包括局部變量表、操作數(shù)棧、動態(tài)鏈接、方法返回地址。

      JVM棧中的每個方法調用都會創(chuàng)建一個棧幀(Stack Frame),棧幀是棧中的基本存儲單位,用于存放方法調用時的信息。

      image-20240522075754906

      下面我將以一段代碼簡述棧幀中存儲的各個部分的含義:

      package focus.total;
      
      public class JVMStudy {
          private static final int initData = 6;
          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) {
              JVMStudy jvmStudy = new JVMStudy();
              jvmStudy.compute();
              System.out.println("計算完成");
          }
      }
      

      當上述程序被執(zhí)行時,JVM虛擬機會分配一個Main主線程,并為該線程分配一個棧內存空間,并創(chuàng)建main棧幀,程序計數(shù)器初始化到main方法字節(jié)碼指令的第一條,并按照順序執(zhí)行,當執(zhí)行到compute()方法時,創(chuàng)建compute方法的棧幀。在cmopute方法中我將對棧幀中的存儲結構進行逐個介紹。

      image-20240522083230466

      反匯編JVMStudy.class代碼觀察JVM執(zhí)行的指令

      下面關于字節(jié)碼指令的解釋其實就已經(jīng)把局部變量表和操作數(shù)棧的含義解釋清楚了。

      • 字節(jié)碼文件本身是 Java 源代碼經(jīng)過編譯后生成的中間表示形式,它包含了一系列按照特定順序排列的指令碼,這些指令碼就是 Java 程序在 JVM 中運行的基礎。
      • 程序計數(shù)器的核心作用就是記錄當前線程正在執(zhí)行的字節(jié)碼指令的地址。在上述反匯編得到的字節(jié)碼執(zhí)行順序指令碼中,程序計數(shù)器會依據(jù)字節(jié)碼的執(zhí)行流程依次指向對應的指令位置。
      Compiled from "JVMStudy.java"
      public class focus.total.JVMStudy {
        public static focus.total.User user;
      
        public focus.total.JVMStudy();
          Code:
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
      
        public int compute();
          Code:
          //iconst_1 : 將int類型的1壓入操作數(shù)棧中
          //istore_1 : 將int類型的值存入局部變量表中
          //兩個組合起來其實就是我們首先在我們的操作數(shù)棧中存儲int值1,然后在局部變量表中存入a,然后將操作數(shù)棧中的值取出,賦給局部變量表中的a  ----  int a=1;
             0: iconst_1
             1: istore_1
          //iconst_2 : 同上,區(qū)別是值為2
          //istore_2 : 同上
          // 這里思考一個問題,如果此時發(fā)生了線程切換,那么當重新返回這個線程時,如何知道從哪里繼續(xù)執(zhí)行?
          //程序計數(shù)器:記錄下一行即將運行的代碼的內存地址。
          // 程序計數(shù)器是每個程序在運行時都會給他分配的一段內存空間代碼,存放下一次即將運行的指令內存地址。
          // 那么當我們程序在執(zhí)行3的這行指令時,來了一個優(yōu)先級高的線程將cpu搶占過去,那么此時該線程會執(zhí)行完3指令之后,在程序計數(shù)器+1,然后讓出CPU,并掛起。當搶占的線程運行完畢之后,該線程重新拿到cpu使用權,此時就會按照程序計數(shù)器中存儲的位置區(qū)執(zhí)行。
             2: iconst_2
             3: istore_2
         // iload_1 : 將局部變量表中第1個位置的整數(shù)值加載到操作數(shù)棧頂
         // iload_2 : 從局部變量2中裝載int類型值
         // iadd :執(zhí)行int類型的加法
         // 這三個指令其實就是 ,從局部變量表中分別取出a的值和b的值放入操作數(shù)棧,然后調用iadd命令,將兩個操作數(shù)取出操作數(shù)棧并完成加法指令。把結果重新壓回我們的操作數(shù)棧。
         // 此時我們的操作數(shù)棧中存放了 int 值 3
             4: iload_1
             5: iload_2
             6: iadd
          // bipush :向操作數(shù)棧中放入 int 值 10
          // imul : 乘法 3 * 10
             7: bipush        10
             9: imul
          // istore_3 : 將棧頂?shù)恼麛?shù)值存儲到局部變量表的第3個位置。
          // iload_3 :  將局部變量表中第3個位置的整數(shù)值加載到操作數(shù)棧頂。
            10: istore_3
            11: iload_3
            12: ireturn
      
        public static void main(java.lang.String[]);
          Code:
             0: new           #2                  // 創(chuàng)建一個新的JVMStudy對象
             3: dup                                // 復制棧頂?shù)腏VMStudy對象引用
             4: invokespecial #3                  // 調用JVMStudy的構造方法 "<init>":()V
             7: astore_1                           // 將棧頂?shù)腏VMStudy對象引用存儲到局部變量1
             8: aload_1                            // 將局部變量1中的JVMStudy對象引用加載到棧頂
             9: invokevirtual #
                 
                 
                 4                  // 調用JVMStudy對象的compute方法,返回一個int
            12: pop                                // 彈出棧頂?shù)膇nt返回值(不使用)
            13: getstatic     #5                  // 獲取System類的out字段(PrintStream對象)
            16: ldc           #6                  // 將字符串 "計算完成" 壓入棧頂
            18: invokevirtual #7                  // 調用PrintStream的println方法,打印字符串
            21: return                             // 從main方法返回
      
        static {};
          Code:
             0: new           #8                  // 創(chuàng)建一個新的User對象
             3: dup                                // 復制棧頂?shù)腢ser對象引用
             4: invokespecial #9                  // 調用User的構造方法 "<init>":()V
             7: putstatic     #10                 // 將棧頂?shù)腢ser對象引用存儲到靜態(tài)字段user
            10: return                             // 從靜態(tài)初始化塊返回
      

      動態(tài)鏈接:在 Java 虛擬機的運行機制中起著關鍵作用。在之前闡述對方法區(qū)的理解時,就已經(jīng)涉及到棧與方法區(qū)之間存在的動態(tài)鏈接關系。我們都知道,在類加載階段的解析過程中,會完成符號引用到直接引用的轉換,這一轉換實際上就是將方法區(qū)中的常量池轉變?yōu)檫\行時常量池的過程。而這里所說的動態(tài)鏈接,其核心操作便是把方法的符號引用借助動態(tài)鏈接這種方式,準確地鏈接到方法在內存中的實際地址,從而為方法的成功調用奠定基礎,確保在程序運行過程中,當需要調用某個方法時,能夠通過這種動態(tài)鏈接機制迅速定位到方法的實際執(zhí)行代碼所在的內存位置并順利執(zhí)行。

      那么此時就有一個疑問,在類加載階段就已經(jīng)完成轉換了,為什么這里還需要進行轉換?

      那是因為這里的動態(tài)鏈接,主要用于處理在編譯時無法確定具體調用目標類型的情況,特別是在多態(tài)(虛方法調用)下發(fā)揮作用。

      多態(tài)場景下的動態(tài)鏈接示例(以Animal為例)

      • 假設存在一個Animal類,它有一個虛方法makeSound()。有兩個子類DogCat,它們分別重寫了makeSound()方法。

      • 當我們在代碼中有這樣的語句:Animal animal = new Dog(); animal.makeSound();,在編譯階段,編譯器看到的是通過Animal類型的引用animal調用makeSound()方法,它生成的字節(jié)碼中對于這個調用只有一個符號引用,這個符號引用指向Animal類的makeSound()方法。

      • 但是在運行時,因為animal實際指向的是Dog對象,動態(tài)鏈接就會發(fā)揮作用。它會根據(jù)對象頭確定animal指向的是Dog類型,然后查找Dog類的虛方法表,在虛方法表中找到Dog類重寫后的makeSound()方法的實際內存地址(直接引用),最后執(zhí)行Dog類的makeSound()方法。這就是動態(tài)鏈接在多態(tài)場景下將符號引用轉換為直接引用的具體過程,確保了根據(jù)對象的實際類型調用正確的方法。

      動態(tài)鏈接與其他概念的關聯(lián)

      • 動態(tài)鏈接與棧幀密切相關。在每個棧幀中,都有一個指向運行時常量池該棧幀所屬方法的引用,這個引用用于支持動態(tài)鏈接。當一個方法被調用時,會創(chuàng)建棧幀,棧幀中的動態(tài)鏈接部分參與到尋找實際要調用的方法的過程中。
      • 動態(tài)鏈接也和類加載過程相互補充。類加載過程中的解析階段主要處理那些在編譯期就能確定唯一調用版本的方法(如靜態(tài)方法、私有方法等),將它們的符號引用轉換為直接引用。而動態(tài)鏈接側重于在運行時處理虛方法和接口方法等需要根據(jù)實際情況確定調用版本的方法的符號引用到直接引用的轉換。

      image-20241205171601514

      方法返回地址

      就如下,我們調用compute方法時,就會在方法返回地址中記錄,用于存儲compute方法返回后的地址。

      public static void main(String[] args) {
          JVMStudy jvmStudy = new JVMStudy();
          jvmStudy.compute();
          System.out.println("計算完成");
      }
      

      程序計數(shù)器

      程序計數(shù)器是JVM內存模型中的一個重要部分,它是線程私有的,也就是說每個線程都會按照自己的程序計數(shù)器指向指令去按順序執(zhí)行。

      程序計數(shù)器的主要職責是告訴JVM接下來應該執(zhí)行哪條字節(jié)碼指令。

      在類加載階段,程序計數(shù)器尚未發(fā)揮作用。而當程序啟動時,主線程的程序計數(shù)器會初始化為指向 Main 方法字節(jié)碼指令的首條。隨后,隨著字節(jié)碼指令的逐步執(zhí)行,它持續(xù)更新,每執(zhí)行完一條指令,便精準指向下一條指令地址,以此確保方法中的字節(jié)碼指令依序執(zhí)行。若遭遇 if - else 分支語句,程序計數(shù)器會依據(jù)判斷結果跳轉至相應的字節(jié)碼指令地址;當遇到方法調用時,它會先留存當前方法當前字節(jié)碼指令的地址,而后跳轉至被調用方法內繼續(xù)執(zhí)行,待被調用方法執(zhí)行完畢,再重新回到之前留存的位置,從而保障程序執(zhí)行流程的連貫性與準確性。

      堆區(qū)

      堆區(qū)(Heap Area)是JVM內存模型中的一個重要部分,它是線程共享的,這意味著多個線程可以同時訪問該區(qū)域,獲取對象、數(shù)組等相關信息。

      堆區(qū)主要是用于存儲Java對象實例(包括數(shù)組對象),在程序執(zhí)行過程中,通過new關鍵字創(chuàng)建的對象都會在堆區(qū)種分配內存空間。

      示例代碼:

      Person person = new Person(); 
      person.setName("張三")
      person.setAge(22);
      

      當程序運行完這段代碼后,就會在堆種存儲person對象及name和age屬性信息,在棧種存儲Person類型的對象引用,該引用指向堆內存種實際的存儲地址。

      關于堆的內存模型:

      在Java8中可以看到堆內存被劃分為年輕代和老年代

      • 年輕代被劃分為三部分,Eden區(qū)和兩個大小嚴格相同的Survivor區(qū),根據(jù)JVM策略,在經(jīng)過幾次垃圾回收之后,任然存活魚Survivor區(qū)的對象將會被移動到老年代中。
      • 老年代:主要保存生命周期常的對象,一般是經(jīng)歷過多次gc都沒有被回收的對象。
      • 年輕代和老年代(1:2),其中年輕代又分為 Eden 、s0 、s1(8:1:1)

      image-20240522133556348

      簡述對象在堆中的一個簡單歷程:

      當程序new一個新的對象,就會把它放在堆中的Eden區(qū),但是當Eden區(qū)域放滿之后,就需要進行GC -- (minor gc)

      image-20240522153328985

      這個gc是由執(zhí)行引擎后臺發(fā)起一個垃圾收集線程,去堆Eden中的對象進行回收(可達性算法),在回收的過程中如果仍有對象被引用那么就將這些對象復制到 幸存區(qū)(其中一個空的,這兩者肯定有一個或者兩個都是空的) ,然后就這樣gc回收,如果一個對象在經(jīng)過 15 次垃圾回收后依然存活于幸存區(qū)中,那么就會將這個對象放到老年代中。此后GC -- (full gc)也會對老年代的垃圾進行回收。

      一段代碼觀察堆內存溢出的情況

      下面這段代碼肯定會內存溢出,因為我們新new的對象都是存放在lists集合中,而lists又是在main方法棧幀中的變量,是一個GC Root,所以這些新new的對象都不會被回收!!

      public class HeapTest{
      	public static void main(String[] args){
      		ArrayList<HeapTest> lists = new ArrayList<>();
      		while(true){
      			lists.add(new HeapTest());
      			Thread.sleep(5);
      		}
      	}
      }
      

      使用jvisualvm進行檢測

      本地方法棧

      在Java虛擬機(JVM)中,本地方法棧(Native Method Stack)是專門為本地方法(native methods)服務的內存區(qū)域。當一個線程調用本地方法時,會使用本地方法棧來執(zhí)行這些方法。

      當Java程序調用本地方法時,JVM會保存當前棧幀,然后在本地方法棧空間中創(chuàng)建當前本地方法的棧幀,通過JNI調用本地方法,本地方法執(zhí)行完畢之后,JVM回到之前的棧幀,繼續(xù)執(zhí)行Java代碼。

      簡單一句話就是執(zhí)行本地方法的。

      示例:本地方法棧的使用
      以下是一個簡單的本地方法示例,展示了如何使用本地方法棧:

      public class NativeExample {
          // 聲明本地方法
          public native void nativePrint();
      
          static {
              // 加載本地庫
              System.loadLibrary("NativeExample");
          }
      
          public static void main(String[] args) {
              new NativeExample().nativePrint();
          }
      }
      
      
      假設對應的C代碼如下:
      #include <jni.h>
      #include <stdio.h>
      #include "NativeExample.h"
      
      // 實現(xiàn)本地方法
      JNIEXPORT void JNICALL Java_NativeExample_nativePrint(JNIEnv *env, jobject obj) {
          printf("Hello from native code!\n");
      }
      

      執(zhí)行引擎

      執(zhí)行引擎中包含了解釋器、JIT即時編譯器、垃圾回收

      解釋器:解釋器是執(zhí)行引擎的一個重要組成部分,它的主要工作方式是逐行讀取字節(jié)碼指令并進行解釋執(zhí)行。例如,當遇到字節(jié)碼指令中的iload(將局部變量加載到操作數(shù)棧)時,解釋器會根據(jù)指令的參數(shù),從局部變量表中找到對應的變量并將其加載到操作數(shù)棧中,這個過程是一個一個指令依次進行的。

      JIT即時編譯器:JIT 即時編譯器是為了提高 Java 程序的執(zhí)行效率而引入的。它會在程序運行過程中,對那些頻繁執(zhí)行的熱點代碼(通過一些動態(tài)監(jiān)測機制確定)進行編譯。這個編譯過程是將字節(jié)碼轉換為機器碼,這樣在后續(xù)執(zhí)行這些代碼時,就可以直接執(zhí)行已經(jīng)編譯好的機器碼,而不是每次都通過解釋器解釋字節(jié)碼。

      垃圾回收:這塊主要是針對堆內存中的垃圾對象回收,以免隨著程序的運行對象越來越多導致OOM,具體的GC會在下一篇JVM深入學習中提到。

      posted @ 2024-12-05 18:09  Liberty碼農志  閱讀(333)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 国产精品亚洲二区在线看| 一区二区三区日本久久九| 久久精品熟女亚洲av麻| 亚洲AV无码久久精品成人| 婷婷综合久久狠狠色成人网| 综合偷自拍亚洲乱中文字幕| 熟女人妻精品一区二区视频| 成人网站网址导航| 亚洲人成网站18禁止无码| 亚洲欧洲美洲在线观看| 国产亚洲精品第一综合另类| 国产无吗一区二区三区在线欢 | 午夜av福利一区二区三区| 四虎成人精品永久免费av| 国产偷窥厕所一区二区| 精品国产一区二区在线视| 久久久久国产精品人妻电影 | 成人拍拍拍无遮挡免费视频| 久久人妻精品国产| 午夜福利国产盗摄久久性| 灌云县| 久久亚洲精品天天综合网| 亚洲高潮喷水无码AV电影| 久久天天躁狠狠躁夜夜网站| 亚洲人成网站观看在线观看| 亚洲色av天天天天天天| 饥渴少妇高潮正在播放| 韩国无码AV片午夜福利| 日韩高清在线亚洲专区不卡| 四虎精品免费永久免费视频| 国产精品线在线精品| 东京热人妻丝袜无码AV一二三区观| 成人亚洲狠狠一二三四区| 成人午夜大片免费看爽爽爽 | 亚洲国产美女精品久久久| 加勒比无码人妻东京热| 蜜臀av无码一区二区三区| 国产精品中文字幕免费| 镇巴县| 日韩中文字幕一区二区不卡| 日韩有码国产精品一区|