主頁 > 後端開發 > JVM性能調優(1) —— JVM記憶體模型和類加載運行機制

JVM性能調優(1) —— JVM記憶體模型和類加載運行機制

2020-09-11 14:41:16 後端開發

一、JVM記憶體模型

運行一個 Java 應用程式,必須要先安裝 JDK 或者 JRE 包,因為 Java 應用在編譯后會變成位元組碼,通過位元組碼運行在 JVM 中,而 JVM 是 JRE 的核心組成部分,JVM 不僅承擔了 Java 位元組碼的分析和執行,同時也內置了自動記憶體分配管理機制,這個機制可以大大降低手動分配回識訓制可能帶來的記憶體泄露和記憶體溢位風險,使 Java 開發人員不需要關注每個物件的記憶體分配以及回收,從而更專注于業務本身,

在 Java 中,JVM 記憶體模型主要分為堆、方法區、程式計數器、虛擬機堆疊和本地方法堆疊,其中,堆和方法區被所有執行緒共享,虛擬機堆疊、本地方法堆疊、程式計數器是執行緒私有的,

1、堆

堆是 JVM 記憶體中最大的一塊記憶體空間,該記憶體被所有執行緒共享,幾乎所有物件和陣列都被分配到了堆記憶體中,堆被劃分為新生代和老年代,新生代又被進一步劃分為 Eden 和 Survivor 區,最后 Survivor 由 From Survivor 和 To Survivor 組成,

但需要注意的是,這些區域的劃分因不同的垃圾收集器而不同,大部分垃圾收集器都是基于分代收集理論設計的,就會采用這種分代模型,而一些新的垃圾收集器不采用分代設計,比如 G1 收集器就是把堆記憶體拆分為多個大小相等的 Region,

2、方法區

在 jdk8 之前,HotSopt 虛擬機的方法區又被稱為永久代,由于永久代的設計容易導致記憶體溢位等問題,jdk8 之后就沒有永久代了,取而代之的是元空間(MetaSpace),元空間并沒有處于堆記憶體上,而是直接占用的本地記憶體,因此元空間的最大大小受本地記憶體限制,

方法區與堆空間類似,是所有執行緒共享的,方法區主要是用來存放已被虛擬機加載的型別資訊、常量、靜態變數等資料,方法區是一個邏輯磁區,包含元空間、運行時常量池、字串常量池,元空間物理上使用的本地記憶體,運行時常量池和字串常量池是在堆中開辟的一塊特殊記憶體區域,這樣做的好處之一是可以避免運行時動態生成的常量的復制遷移,可以直接使用堆中的參考,要注意的是,字串常量池在 jvm 中只有一個,而運行時常量池是和型別資料系結的,每個 Class 一個,

1)型別資訊(類或介面):

  • 這個型別的全限定名
  • 這個型別的直接超類的全限定名(只有 java.lang.Object 沒有超類)
  • 這個型別的訪問修飾符(public、abstract、final)
  • 這個型別是介面型別還是型別別
  • 任何直接超介面的的全限定名的有序串列

2)運行時常量池:

  • Class 檔案被裝載進虛擬機后,Class 常量池表中的字面量和符號參考都會存放到運行時常量池中,平時我們說的常量池一般指運行時常量池,
  • 運行時常量池相比Class常量池具備動態性,運行時可以將新的常量放入池中,比如呼叫 String.intern() 方法使字串駐留,

3)欄位資訊:

  • 欄位名
  • 欄位的型別(包括 void)
  • 欄位的修飾符(public、private、protected、static、final、volatile、transient)

4)方法資訊:

  • 方法名
  • 方法的回傳型別
  • 方法引數的數量和型別
  • 方法的修飾符(public、private、protected、static、final、synchronized、native、abstract)
  • 方法的位元組碼
  • 運算元堆疊和該方法的堆疊幀中的區域變數的大小
  • 例外表

5)指向類加載器的參考:

  • jvm 使用類加載器來加載一個類,這個類加載器是和這個型別系結的,因此會在型別資訊中存盤這個類加載器的參考

6)指向 Class 類的參考:

  • 每一個被加載的型別,jvm 都會在堆中創建一個 java.lang.Class 的實體,型別資訊中會存盤 Class 實體的參考
  • 在代碼中,可以使用 Class 實體訪問方法區保存的資訊,如類加載器、類名、介面等

3、虛擬機堆疊

每當啟動一個新的執行緒,虛擬機都會在虛擬機堆疊里為它分配一個執行緒堆疊,執行緒堆疊與執行緒同生共死,執行緒堆疊以 堆疊幀 為單位保存執行緒的運行狀態,虛擬機只會對執行緒堆疊執行兩種操作:以堆疊幀為單位的壓堆疊或出堆疊,每個方法在執行的同時都會創建一個堆疊幀,每個方法從呼叫開始到結束,就對應著一個堆疊幀在執行緒堆疊中壓堆疊和出堆疊的程序,方法可以通過兩種方式結束,一種通過 return 正常回傳,一種通過拋出例外而終止,方法回傳后,虛擬機都會彈出當前堆疊幀然后釋放掉,

當虛擬機呼叫一個Java方法時.它從對應類的型別資訊中得到此方法的區域變數區和運算元堆疊的大小,并據此分配堆疊幀記憶體,然后壓入Java堆疊中,

堆疊幀由三部分組成:區域變數區、運算元堆疊、幀資料區,

1)區域變數區:

  • 區域變數區是一個陣列結構,主要存放對應方法的引數和區域變數,
  • 如果是實體方法,區域變數表第一個引數是一個 reference 參考型別,存放的是當前物件本身 this,

2)運算元堆疊:

  • 運算元堆疊也是一個陣列結構,但并不是通過索引來訪問的,而是堆疊的壓堆疊和出堆疊操作,
  • 運算元堆疊是虛擬機的作業區,大多數指令都要從這里彈出資料、執行運算、然后把結果壓回運算元堆疊,

3)幀資料區:主要保存常量池入口、例外表、正常方法回傳的資訊

  • 常量池入口參考:某些指令要從常量池取資料,獲取類、欄位資訊等
  • 例外表參考:當方法拋出例外時,虛擬機根據例外表來決定如何處理,如果在例外表找到了匹配的 catch 子句,就會把控制權轉交給 catch 子句的代碼,沒有則立即例外中止,然后恢復發起呼叫的方法的堆疊幀,然后在發起呼叫的方法的背景關系中重新拋出同樣的例外,
  • 方法回傳資訊:方法正常回傳時,虛擬機通過這些資訊恢復發起呼叫的方法的堆疊幀,設定PC暫存器指向發起呼叫的方法,方法如果有回傳值,還會把回傳結果壓入到發起呼叫的方法的運算元堆疊,

4、本地方法堆疊

本地方法堆疊與虛擬機堆疊所發揮的作用是相似的,當執行緒呼叫Java方法時,會創建一個堆疊幀并壓入虛擬機堆疊;而呼叫本地方法時,虛擬機會保持堆疊不變,不會壓入新的堆疊幀,虛擬機只是簡單的動態鏈接并直接呼叫指定的本地方法,使用的是某種本地方法堆疊,比如某個虛擬機實作的本地方法介面是使用C連接模型,那么它的本地方法堆疊就是C堆疊,

本地方法可以通過本地方法介面來訪問虛擬機的運行時資料區,它可以做任何他想做的事情,本地方法不受虛擬機控制,

5、程式計數器

每一個運行的執行緒都會有它的程式計數器(PC暫存器),與執行緒的生命周期一樣,執行某個方法時,PC暫存器的內容總是下一條將被執行的地址,這個地址可以是一個本地指標,也可以是在方法位元組碼中相對于該方法起始指令的偏移量,如果該執行緒正在執行一個本地方法,那么此時PC暫存器的值是 undefined,

程式計數器是程式控制流的指示器,分支、回圈、跳轉、例外處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成,多執行緒環境下,為了執行緒切換后能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立存盤,

二、類加載機制

寫好的源代碼,需要編譯后加載到虛擬機才能運行,java 源檔案編譯成 class 檔案后,jvm 通過類加載器把 class 檔案加載到虛擬機,然后經過類連接(類連接又包括驗證、準備、決議三個階段),最后經過初始化,位元組碼就可以被解釋執行了,對于一些熱點代碼,虛擬機還存在一道即時編譯,會把位元組碼編譯成本地平臺相關的機器碼,以提高熱點代碼的執行效率,

裝載、驗證、準備、初始化這幾個階段的順序是確定的,型別的加載程序必須按照這種順序開始,而決議階段可以在初始化階段之后再開始,一般是在第一次使用到這個物件時才會開始決議,這些階段通常都是互相交叉地混合進行的,會在一個階段執行的程序中呼叫、激活另一個階段,比如發現參考了另一個類,那么就會先觸發另一個類的加載程序,

接下來通過如下類和代碼來詳細分析下類加載的程序:

  1 package com.lyyzoo.jvm.test01;
  2 
  3 public class Person<T> {
  4 
  5     public static final String SEX_MAN = "Male";
  6     public static final String SEX_WOMAN = "Female";
  7 
  8     static {
  9         System.out.println("Person static init");
 10         System.out.println("SEX_MAN: " + SEX_MAN);
 11     }
 12 
 13     public void sayHello(T str) {
 14         System.out.println("Person say hello: " + str);
 15     }
 16 }
 17 
 18 
 19 /////////////////////////////////////////////////////////////////////
 20 
 21 
 22 package com.lyyzoo.jvm.test01;
 23 
 24 import java.io.Serializable;
 25 
 26 public class User extends Person<String> implements Serializable {
 27     private static final long serialVersionUID = -4482416396338787067L;
 28 
 29     // 靜態常量
 30     public static final String FIELD_NAME = "username";
 31     public static final int AGE_MAX = 100;
 32 
 33     // 靜態變數
 34     private static String staticName = "Rambo";
 35     private static int staticAge = 20;
 36 
 37     // 類屬性
 38     private String name = "蘭博";
 39     private int age = 25;
 40 
 41     // 靜態代碼塊
 42     static {
 43         System.out.println("user static init");
 44         System.out.println("staticName=" + staticName);
 45         System.out.println("staticAge=" + staticAge);
 46     }
 47 
 48     public User() {
 49     }
 50 
 51     public User(String name, int age) {
 52         this.name = name;
 53         this.age = age;
 54     }
 55 
 56     // 實體方法
 57     public void printInfo() {
 58         System.out.println("name:" + name + ", age:" + age);
 59     }
 60 
 61     // 靜態方法
 62     public static void staticPrintInfo() {
 63         System.out.println("FIELD_NAME:" + FIELD_NAME + ", AGE_MAX:" + AGE_MAX);
 64     }
 65 
 66     // 泛型方法多載
 67     @Override
 68     public void sayHello(String str) {
 69         super.sayHello(str);
 70         System.out.println("User say hello: " + str);
 71     }
 72 
 73     // 方法將拋出例外
 74     public int willThrowException() {
 75         int i = 0;
 76         try {
 77             int r = 10 / i;
 78             return r;
 79         } catch (Exception e) {
 80             System.out.println("catch exception");
 81             return i;
 82         } finally {
 83             System.out.println("finally handle");
 84         }
 85     }
 86 }
 87 
 88 
 89 /////////////////////////////////////////////////////////////////////
 90 
 91 
 92 package com.lyyzoo.jvm.test01;
 93 
 94 public class Main {
 95 
 96     public static void main(String[] args) {
 97         System.out.println("FIELD_NAME: " + User.FIELD_NAME);
 98 
 99         User.staticPrintInfo();
100 
101         User user = new User();
102         user.printInfo();
103     }
104 }
View Code

三、類編譯和Class 檔案結構

*.java 檔案被編譯成 *.class 檔案的程序,這個編譯一般稱為前端編譯,主要使用 javac 來完成前端編譯,Java class檔案是8位位元組的二進制流,資料項按順序存盤在class檔案中,相鄰的項之間沒有任何間隔,這樣可以使class檔案緊湊,class 檔案主要包含 版本資訊、常量池、型別索引、欄位表、方法表、屬性表等資訊,

將 User 類編譯成 class 檔案后,再通過 javap 反編譯 class 檔案,可以看到一個 class 檔案大體包含的結構:

  1 說明:用“【】”標識的是手動添加的注釋
  2 
  3 Mechrevo@hello-world MINGW64 /e/repo-study/test-concurrent/target/classes/com/lyyzoo/jvm/test01
  4 【javap -v 命令反編譯 Class】
  5 $ javap -v User.class
  6 Classfile /E:/repo-study/test-concurrent/target/classes/com/lyyzoo/jvm/test01/User.class
  7   Last modified 2020-9-3; size 2389 bytes 
  8   【魔數】
  9   MD5 checksum ec5a961c2a46926522bafddcb3204fb9
 10   Compiled from "User.java"
 11 public class com.lyyzoo.jvm.test01.User extends com.lyyzoo.jvm.test01.Person<java.lang.String> implements java.io.Serializable
 12   【版本號】
 13   minor version: 0
 14   major version: 52
 15   flags: ACC_PUBLIC, ACC_SUPER
 16 【常量池】
 17 Constant pool:
 18     #1 = Methodref          #29.#76       // com/lyyzoo/jvm/test01/Person."<init>":()V
 19     #2 = String             #77           // 蘭博
 20     #3 = Fieldref           #14.#78       // com/lyyzoo/jvm/test01/User.name:Ljava/lang/String;
 21     #4 = Fieldref           #14.#79       // com/lyyzoo/jvm/test01/User.age:I
 22     #5 = Fieldref           #80.#81       // java/lang/System.out:Ljava/io/PrintStream;
 23     #6 = Class              #82           // java/lang/StringBuilder
 24     #7 = Methodref          #6.#76        // java/lang/StringBuilder."<init>":()V
 25     #8 = String             #83           // name:
 26     #9 = Methodref          #6.#84        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
 27    #10 = String             #85           // , age:
 28    #11 = Methodref          #6.#86        // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
 29    #12 = Methodref          #6.#87        // java/lang/StringBuilder.toString:()Ljava/lang/String;
 30    #13 = Methodref          #88.#89       // java/io/PrintStream.println:(Ljava/lang/String;)V
 31    #14 = Class              #90           // com/lyyzoo/jvm/test01/User
 32    #15 = String             #91           // FIELD_NAME:username, AGE_MAX:100
 33    #16 = Methodref          #29.#92       // com/lyyzoo/jvm/test01/Person.sayHello:(Ljava/lang/Object;)V
 34    #17 = String             #93           // User say hello:
 35    #18 = String             #94           // finally handle
 36    #19 = Class              #95           // java/lang/Exception
 37    #20 = String             #96           // catch exception
 38    #21 = Class              #97           // java/lang/String
 39    #22 = Methodref          #14.#98       // com/lyyzoo/jvm/test01/User.sayHello:(Ljava/lang/String;)V
 40    #23 = String             #99           // Rambo
 41    #24 = Fieldref           #14.#100      // com/lyyzoo/jvm/test01/User.staticName:Ljava/lang/String;
 42    #25 = Fieldref           #14.#101      // com/lyyzoo/jvm/test01/User.staticAge:I
 43    #26 = String             #102          // user static init
 44    #27 = String             #103          // staticName=
 45    #28 = String             #104          // staticAge=
 46    #29 = Class              #105          // com/lyyzoo/jvm/test01/Person
 47    #30 = Class              #106          // java/io/Serializable
 48    #31 = Utf8               serialVersionUID
 49    #32 = Utf8               J
 50    #33 = Utf8               ConstantValue
 51    #34 = Long               -4482416396338787067l
 52    #36 = Utf8               FIELD_NAME
 53    #37 = Utf8               Ljava/lang/String;
 54    #38 = String             #107          // username
 55    #39 = Utf8               AGE_MAX
 56    #40 = Utf8               I
 57    #41 = Integer            100
 58    #42 = Utf8               staticName
 59    #43 = Utf8               staticAge
 60    #44 = Utf8               name
 61    #45 = Utf8               age
 62    #46 = Utf8               <init>
 63    #47 = Utf8               ()V
 64    #48 = Utf8               Code
 65    #49 = Utf8               LineNumberTable
 66    #50 = Utf8               LocalVariableTable
 67    #51 = Utf8               this
 68    #52 = Utf8               Lcom/lyyzoo/jvm/test01/User;
 69    #53 = Utf8               (Ljava/lang/String;I)V
 70    #54 = Utf8               MethodParameters
 71    #55 = Utf8               printInfo
 72    #56 = Utf8               staticPrintInfo
 73    #57 = Utf8               sayHello
 74    #58 = Utf8               (Ljava/lang/String;)V
 75    #59 = Utf8               str
 76    #60 = Utf8               willThrowException
 77    #61 = Utf8               ()I
 78    #62 = Utf8               r
 79    #63 = Utf8               e
 80    #64 = Utf8               Ljava/lang/Exception;
 81    #65 = Utf8               i
 82    #66 = Utf8               StackMapTable
 83    #67 = Class              #90           // com/lyyzoo/jvm/test01/User
 84    #68 = Class              #95           // java/lang/Exception
 85    #69 = Class              #108          // java/lang/Throwable
 86    #70 = Utf8               (Ljava/lang/Object;)V
 87    #71 = Utf8               <clinit>
 88    #72 = Utf8               Signature
 89    #73 = Utf8               Lcom/lyyzoo/jvm/test01/Person<Ljava/lang/String;>;Ljava/io/Serializable;
 90    #74 = Utf8               SourceFile
 91    #75 = Utf8               User.java
 92    #76 = NameAndType        #46:#47       // "<init>":()V
 93    #77 = Utf8               蘭博
 94    #78 = NameAndType        #44:#37       // name:Ljava/lang/String;
 95    #79 = NameAndType        #45:#40       // age:I
 96    #80 = Class              #109          // java/lang/System
 97    #81 = NameAndType        #110:#111     // out:Ljava/io/PrintStream;
 98    #82 = Utf8               java/lang/StringBuilder
 99    #83 = Utf8               name:
100    #84 = NameAndType        #112:#113     // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
101    #85 = Utf8               , age:
102    #86 = NameAndType        #112:#114     // append:(I)Ljava/lang/StringBuilder;
103    #87 = NameAndType        #115:#116     // toString:()Ljava/lang/String;
104    #88 = Class              #117          // java/io/PrintStream
105    #89 = NameAndType        #118:#58      // println:(Ljava/lang/String;)V
106    #90 = Utf8               com/lyyzoo/jvm/test01/User
107    #91 = Utf8               FIELD_NAME:username, AGE_MAX:100
108    #92 = NameAndType        #57:#70       // sayHello:(Ljava/lang/Object;)V
109    #93 = Utf8               User say hello:
110    #94 = Utf8               finally handle
111    #95 = Utf8               java/lang/Exception
112    #96 = Utf8               catch exception
113    #97 = Utf8               java/lang/String
114    #98 = NameAndType        #57:#58       // sayHello:(Ljava/lang/String;)V
115    #99 = Utf8               Rambo
116   #100 = NameAndType        #42:#37       // staticName:Ljava/lang/String;
117   #101 = NameAndType        #43:#40       // staticAge:I
118   #102 = Utf8               user static init
119   #103 = Utf8               staticName=
120   #104 = Utf8               staticAge=
121   #105 = Utf8               com/lyyzoo/jvm/test01/Person
122   #106 = Utf8               java/io/Serializable
123   #107 = Utf8               username
124   #108 = Utf8               java/lang/Throwable
125   #109 = Utf8               java/lang/System
126   #110 = Utf8               out
127   #111 = Utf8               Ljava/io/PrintStream;
128   #112 = Utf8               append
129   #113 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
130   #114 = Utf8               (I)Ljava/lang/StringBuilder;
131   #115 = Utf8               toString
132   #116 = Utf8               ()Ljava/lang/String;
133   #117 = Utf8               java/io/PrintStream
134   #118 = Utf8               println
135 {
136   【欄位表集合】
137   public static final java.lang.String FIELD_NAME;
138     descriptor: Ljava/lang/String;
139     flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
140     ConstantValue: String username
141 
142   public static final int AGE_MAX;
143     descriptor: I
144     flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
145     ConstantValue: int 100
146 
147   【方法表】
148   public com.lyyzoo.jvm.test01.User();
149     【描述符索引】
150     descriptor: ()V
151     【訪問標志】
152     flags: ACC_PUBLIC
153     【方法體代碼指令】
154     Code:
155       【方法堆疊大小】
156       stack=2, locals=1, args_size=1
157          0: aload_0
158          1: invokespecial #1                  // Method com/lyyzoo/jvm/test01/Person."<init>":()V
159          4: aload_0
160          5: ldc           #2                  // String 蘭博
161          7: putfield      #3                  // Field name:Ljava/lang/String;
162         10: aload_0
163         11: bipush        25
164         13: putfield      #4                  // Field age:I
165         16: return
166       【屬性表,方法區域變數】    
167       LineNumberTable:
168         line 27: 0
169         line 17: 4
170         line 18: 10
171         line 28: 16
172       【本地變數表,方法入參】
173       LocalVariableTable:
174         Start  Length  Slot  Name   Signature
175             0      17     0  this   Lcom/lyyzoo/jvm/test01/User;
176 
177   public com.lyyzoo.jvm.test01.User(java.lang.String, int);
178     descriptor: (Ljava/lang/String;I)V
179     flags: ACC_PUBLIC
180     Code:
181       stack=2, locals=3, args_size=3
182          0: aload_0
183          1: invokespecial #1                  // Method com/lyyzoo/jvm/test01/Person."<init>":()V
184          4: aload_0
185          5: ldc           #2                  // String 蘭博
186          7: putfield      #3                  // Field name:Ljava/lang/String;
187         10: aload_0
188         11: bipush        25
189         13: putfield      #4                  // Field age:I
190         16: aload_0
191         17: aload_1
192         18: putfield      #3                  // Field name:Ljava/lang/String;
193         21: aload_0
194         22: iload_2
195         23: putfield      #4                  // Field age:I
196         26: return
197       LineNumberTable:
198         line 30: 0
199         line 17: 4
200         line 18: 10
201         line 31: 16
202         line 32: 21
203         line 33: 26
204       LocalVariableTable:
205         Start  Length  Slot  Name   Signature
206             【可以看出,物件實體方法的第一個引數始終都是 this,這也是為什么我們可以在方法內呼叫 this 的原因】
207             0      27     0  this   Lcom/lyyzoo/jvm/test01/User;
208             0      27     1  name   Ljava/lang/String;
209             0      27     2   age   I
210     MethodParameters:
211       Name                           Flags
212       name
213       age
214 
215   public void printInfo();
216     descriptor: ()V
217     flags: ACC_PUBLIC
218     Code:
219       stack=3, locals=1, args_size=1
220          0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
221          3: new           #6                  // class java/lang/StringBuilder
222          6: dup
223          7: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
224         10: ldc           #8                  // String name:
225         12: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
226         15: aload_0
227         16: getfield      #3                  // Field name:Ljava/lang/String;
228         19: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
229         22: ldc           #10                 // String , age:
230         24: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
231         27: aload_0
232         28: getfield      #4                  // Field age:I
233         31: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
234         34: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
235         37: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
236         40: return
237       LineNumberTable:
238         line 37: 0
239         line 38: 40
240       LocalVariableTable:
241         Start  Length  Slot  Name   Signature
242             0      41     0  this   Lcom/lyyzoo/jvm/test01/User;
243 
244   public static void staticPrintInfo();
245     descriptor: ()V
246     【訪問標志】
247     flags: ACC_PUBLIC, ACC_STATIC
248     Code:
249       stack=2, locals=0, args_size=0
250          0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
251          3: ldc           #15                 // String FIELD_NAME:username, AGE_MAX:100
252          5: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
253          8: return
254       LineNumberTable:
255         line 42: 0
256         line 43: 8
257       【注意,靜態方法第一個引數就不再是 this 了】            
258                 
259 
260   public void sayHello(java.lang.String);
261     descriptor: (Ljava/lang/String;)V
262     flags: ACC_PUBLIC
263     Code:
264       stack=3, locals=2, args_size=2
265          0: aload_0
266          1: aload_1
267          2: invokespecial #16                 // Method com/lyyzoo/jvm/test01/Person.sayHello:(Ljava/lang/Object;)V
268          5: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
269          8: new           #6                  // class java/lang/StringBuilder
270         11: dup
271         12: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
272         15: ldc           #17                 // String User say hello:
273         17: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
274         20: aload_1
275         21: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
276         24: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
277         27: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
278         30: return
279       LineNumberTable:
280         line 48: 0
281         line 49: 5
282         line 50: 30
283       LocalVariableTable:
284         Start  Length  Slot  Name   Signature
285             【第一個引數為 this286             0      31     0  this   Lcom/lyyzoo/jvm/test01/User;
287             0      31     1   str   Ljava/lang/String;
288     MethodParameters:
289       Name                           Flags
290       str
291 
292   public int willThrowException();
293     descriptor: ()I
294     flags: ACC_PUBLIC
295     Code:
296       stack=2, locals=5, args_size=1
297          0: iconst_0
298          1: istore_1
299          2: bipush        10
300          4: iload_1
301          5: idiv
302          6: istore_2
303          7: iload_2
304          8: istore_3
305          9: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
306         12: ldc           #18                 // String finally handle
307         14: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
308         17: iload_3
309         18: ireturn
310         19: astore_2
311         20: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
312         23: ldc           #20                 // String catch exception
313         25: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
314         28: iload_1
315         29: istore_3
316         30: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
317         33: ldc           #18                 // String finally handle
318         35: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
319         38: iload_3
320         39: ireturn
321         40: astore        4
322         42: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
323         45: ldc           #18                 // String finally handle
324         47: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
325         50: aload         4
326         52: athrow
327       【方法例外表】    
328       Exception table:
329          from    to  target type
330              2     9    19   Class java/lang/Exception
331              2     9    40   any
332             19    30    40   any
333             40    42    40   any
334       LineNumberTable:
335         line 54: 0
336         line 56: 2
337         line 57: 7
338         line 62: 9
339         line 57: 17
340         line 58: 19
341         line 59: 20
342         line 60: 28
343         line 62: 30
344         line 60: 38
345         line 62: 40
346         line 63: 50
347       LocalVariableTable:
348         Start  Length  Slot  Name   Signature
349             7      12     2     r   I
350            20      20     2     e   Ljava/lang/Exception;
351             0      53     0  this   Lcom/lyyzoo/jvm/test01/User;
352             2      51     1     i   I
353       StackMapTable: number_of_entries = 2
354         frame_type = 255 /* full_frame */
355           offset_delta = 19
356           locals = [ class com/lyyzoo/jvm/test01/User, int ]
357           stack = [ class java/lang/Exception ]
358         frame_type = 84 /* same_locals_1_stack_item */
359           stack = [ class java/lang/Throwable ]
360 
361   public void sayHello(java.lang.Object);
362     descriptor: (Ljava/lang/Object;)V
363     【多載泛型方法時,會多出 ACC_BRIDGE、ACC_SYNTHETIC 兩個標志,ACC_BRIDGE代表是jvm自動生成的橋接方法,ACC_SYNTHETIC代表是jvm生成的不可見方法】
364     flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
365     Code:
366       stack=2, locals=2, args_size=2
367          0: aload_0
368          1: aload_1
369          2: checkcast     #21                 // class java/lang/String
370          5: invokevirtual #22                 // Method sayHello:(Ljava/lang/String;)V
371          8: return
372       LineNumberTable:
373         line 5: 0
374       LocalVariableTable:
375         Start  Length  Slot  Name   Signature
376             0       9     0  this   Lcom/lyyzoo/jvm/test01/User;
377     MethodParameters:
378       Name                           Flags
379       str                            synthetic
380     
381   【靜態代碼塊】    
382   static {};
383     descriptor: ()V
384     flags: ACC_STATIC
385     Code:
386       stack=3, locals=0, args_size=0
387          0: ldc           #23                 // String Rambo
388          2: putstatic     #24                 // Field staticName:Ljava/lang/String;
389          5: bipush        20
390          7: putstatic     #25                 // Field staticAge:I
391         10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
392         13: ldc           #26                 // String user static init
393         15: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
394         18: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
395         21: new           #6                  // class java/lang/StringBuilder
396         24: dup
397         25: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
398         28: ldc           #27                 // String staticName=
399         30: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
400         33: getstatic     #24                 // Field staticName:Ljava/lang/String;
401         36: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
402         39: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
403         42: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
404         45: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
405         48: new           #6                  // class java/lang/StringBuilder
406         51: dup
407         52: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
408         55: ldc           #28                 // String staticAge=
409         57: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
410         60: getstatic     #25                 // Field staticAge:I
411         63: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
412         66: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
413         69: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
414         72: return
415       LineNumberTable:
416         line 13: 0
417         line 14: 5
418         line 22: 10
419         line 23: 18
420         line 24: 45
421         line 25: 72
422 }
423 Signature: #73                          // Lcom/lyyzoo/jvm/test01/Person<Ljava/lang/String;>;Ljava/io/Serializable;
424 SourceFile: "User.java"
View Code

 

我們也可以安裝 [jclasslib Bytecode viewer] 插件,就可以在IDEA中清晰地看到 Class 包含的資訊:

1、魔數與Class檔案資訊

魔數唯一作用是確定這個檔案是否為一個能被虛擬機接受的Class檔案,使用魔數而不是擴展名來進行識別主要是基于安全考慮,因為檔案擴展名可以隨意改動,

Minior version 是次版本號,Major version 是主版本號,Java的版本號是從45開始的,JDK 1.1之后的每個JDK大版本發布主版本號向上加 1,所以 jdk1.8 的 Major version 是 52,高版本的JDK能向下兼容以前版本的Class檔案,但不能運行以后版本的Class檔案,

Access flags 用于識別類或者介面層次的訪問資訊,比如這個Class是類還是介面;是否定義為public型別;是否定義為abstract型別 等等,

 

2、常量池

虛擬機把常量池組織為入口串列,常量池中的許多入口都指向其他的常量池入口(比如參考了其它類),而且 class 檔案中的許多條目也會指向常量池中的入口,串列中的第一項索引值為1,第二項索引值為2,以此類推,雖然沒有索引值為0的入口,但是 constant_pool_count 會把這一入口也算進去,比如上面的 Constant pool count 為 119,而常量池實際的索引值最大為 118,

常量池主要存放兩大類常量:字面量和符號參考,

  • 字面量:字面量主要是文本字串、final 常量值、類名和方法名的常量等,
  • 符號參考:符號參考對java動態連接起著非常重要的作用,主要的符號參考有:類和介面的全限定名、欄位的名稱和描述符、方法的名稱和描述符等,

常量池中每一項都是一個表,常量表主要有如下17種常量型別,

常量池的專案型別:

再理解下符號參考和直接應用:

  • 符號參考:java 檔案在前端編譯期間,class 檔案并不知道它參考的那些類、方法、欄位的具體地址,不能被class檔案中的位元組碼直接參考,因此使用符號參考來代替,運行時再動態連接到具體參考上,符號參考以一組符號來描述所參考的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可,
  • 直接參考:直接參考是可以直接指向目標的指標、相對偏移量或者是一個能間接定位到目標的句柄,直接參考是和虛擬機實作的記憶體布局直接相關的,如果有了直接參考,那參考的目標必定已經在虛擬機的記憶體中存在,在運行時,Java虛擬機從常量池獲得符號參考,然后在運行時決議參考項的實際地址,

比如看 sayHello 這個方法,首先要呼叫 super.sayHello,即父類 Person 的 sayHello 方法,那么第三個指令就會在常量池尋找 [#16] 這個索引,然后可以從常量池找到這個方法的相關資訊,再通過 [#29] 找到 Person 類資訊,

3、類索引、父類索引與介面索引

Class檔案中由這三項資料來確定該型別的繼承關系,類索參考于確定這個類的全限定名,父類索參考于確定這個類的父類的全限定名,

它們各自指向一個型別為 CONSTANT_Class_info 的常量表,通過 CONSTANT_Class_info 常量中的索引值可以找到定義在 CONSTANT_Utf8_info 型別的常量中的全限定名字串,

4、欄位表

欄位表用于描述介面或者類中宣告的變數,Java語言中的“欄位”包括類級變數以及實體級變數,但不包括在方法內部宣告的區域變數,

描述符:

  • descriptor 是描述符,描述符的作用是用來描述欄位的資料型別、方法的引數串列(包括數量、型別以及順序)和回傳值,
  • 對于陣列型別,每一維度將使用一個前置的 “[” 字符來描述,如一個定義為 “java.lang.String[][]” 型別的二維陣列將被記錄成 “[[Ljava/lang/String;”,一個整型陣列“int[]”將被記錄成 “[I”,
  • 用描述符來描述方法時,按照先引數串列、后回傳值的順序描述,引數串列按照引數的嚴格順序放在一組小括號“()”之內,

描述符標識字符含義:

比如從構造方法的描述符 <Ljava/lang/String;I)V> 可以看出,方法的引數包括物件型別 java.lang.String、基本型別 int,回傳值為 void,

5、方法表

方發表與欄位表類似,方發表用于描述方法的訪問標志、名稱索引、描述符索引、屬性表集合、代碼指令等

1)例外表:

如果方法表有例外捕獲的話,還會有例外表,當方法拋出例外時,就會從例外表查找能處理的例外處理器,

2)多載多出的方法:

如果父類方法在子類中被重寫,那方法表中就會包含父類方法的資訊,如果重寫泛型方法,還會出現編譯器自動添加的橋接方法,

因為泛型編譯后的實際型別為 Object,如果子類泛型不是 Object,那么編譯器會自動在子類中生成一個 Object 型別的橋接方法,橋接方法的內部會先做型別轉換檢查,然后呼叫多載的方法,因為我們在宣告變數時一般是宣告的超類,實際型別為子類,而超類方法的引數是Object型別的,因此就會呼叫到橋接方法,進而呼叫子類多載后的方法,

 而且,當我們通過反射根據方法名獲取方法時,要注意泛型多載可能獲取到橋接方法,此時可以通過 method.isBridge() 方法判斷是否是橋接方法,

3)類構造器和實體構造器:

方法表還包括實體構造方法 <init> 和類構造方法 <clinit> ,<init> 就是對應的實體構造器,<clinit> 是編譯時將類初始化的代碼搜集在一起形成的類初始化方法,如靜態變數賦值、靜態代碼塊,

初始化階段會呼叫類構造器 <clinit> 來初始化類,因此其一定是執行緒安全的,是由虛擬機來保證的,這種機制我們可以用來實作安全的單例模式,列舉類的初始化也是在 <clinit> 方法中初始化的,

6、屬性表

屬性表集合主要是為了正確識別Class檔案而定義的一些屬性,如 Code、Deprecated、ConstantValue、Exceptions、SourceFile 等等,

每一個屬性,它的名稱都要從常量池中參考一個 CONSTANT_Utf8_info 型別的常量來表示,

四、類加載

1、類初始化的時機

類和介面被加載的時機因不同的虛擬機可能不同,但類初始化的觸發時機有且僅有六種情況:

  • 當創建某個類的實體,如 new、反射、克隆、反序列化
  • 當呼叫某個類的靜態方法時
  • 當使用某個介面或類的靜態欄位,或者賦值時(final 修飾的常量除外,它在編譯期把結果放入常量池中了)
  • 使用java.lang.reflect包的方法對型別進行反射呼叫的時候,如果型別沒有進行過初始化,則需要先觸發其初始化
  • 當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化(介面除外)
  • 當虛擬機啟動時,會先初始化要執行的主類(包含main()方法的那個類)

這六種情況稱為對一個型別進行主動參考,除此之外,所有參考型別的方式都不會觸發初始化,稱為被動參考,

觸發介面初始化的情況:

  • 類初始化時并不會觸發其實作的介面的初始化,介面初始化時也不會要求父介面初始化
  • 在介面所宣告的非常量欄位被使用時,該介面才會被初始化
  • 如果介面定義了 default 方法,那子類重寫了這個方法,就會先觸發介面的初始化

1)主動初始化:

從輸出可以看出,對 final 常量的參考不會觸發類的初始化,呼叫靜態方法時觸發了類的初始化,同時,一定會先觸發父類的初始化,而且類只會被初始化一次,

注意初始化的順序是按代碼的順序從上到下初始化:

2)被動初始化,如下被動參考不會觸發類的初始化:

  • 通過子類參考父類的靜態欄位,不會導致子類初始化,對于靜態欄位,只有直接定義這個欄位的類才會被初始化
  • 通過陣列定義來參考類,不會觸發此類的初始化,但是會觸發一個 “[com.lyyzoo.jvm.test01.User” 型別的初始化,即一維陣列型別
  • 參考類的常量不會觸發類的初始化,常量在編譯階段會存入呼叫類的常量池中,本質上沒有直接參考到定義常量的類,因此不會觸發定義常量的類的初始化

3)不難判斷,例子中定義的類的加載順序如下:

2、加載

在加載階段,Java虛擬機必須完成以下三件事情:

  • 通過一個類的全限定名來獲取定義此類的二進制位元組流,
  • 將這個位元組流所代表的靜態存盤結構轉化為方法區的運行時資料結構,
  • 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口,

這個二進制流可以從 Class 檔案中獲取,從JAR包、WAR包中獲取,從網路中獲取,實時生成、還可以從加密檔案中獲取,在加載時再解密(防止Class檔案被反編譯),這個加載是由類加載器加載進虛擬機的,非陣列型別可以使用內置的引導類加載器來加載,也可以使用開發人員自定義的類加載器來加載,我們可以自己控制位元組流的獲取方式,而陣列型別本身不通過類加載器加載,它是由虛擬機直接在記憶體中構造出來的,

加載階段會把 Class 常量池中的各項常量存放到運行時常量池中(下圖中的常量池只挑選了部分常量來展示),加載階段的最終產品就是 Class 類的實體物件,它成為程式與方法區內部資料結構之間的入口,可以通過這個 Class 實體來獲得類的資訊、方法、欄位、類加載器等等,

在裝載程序中,虛擬機還會確認裝載類的所有超類是否都被裝載了,根據 super class 項決議符號參考,這就會導致超類的裝載、連接和初始化,

3、驗證

這一階段的目的是確保Class檔案的位元組流中包含的資訊符合《Java虛擬機規范》的全部約束要求,保證這些資訊被當作代碼運行后不會危害虛擬機自身的安全,

驗證階段會完成下面四個階段的檢驗:

  • 檔案格式驗證:保證輸入的位元組流能正確地決議并存盤于方法區之內,通過這個階段的驗證之后,這段位元組流會進入Java虛擬機記憶體的方法區中進行存盤,后面的驗證就是基于方法區的存盤結構而進行了,
  • 元資料驗證:對類的元資料資訊進行語意校驗,如這個類是否有父類(除 java.lang.Object 外,所有的類都有父類)、是否繼承了 final 的類、實作了 final 的方法等,
  • 位元組碼驗證:通過資料流分析和控制流分析,確定程式語意是合法的、符合邏輯的,對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為,
  • 符號參考驗證:最后一個階段的校驗發生在虛擬機將符號參考轉化為直接參考的時候,這個轉化動作將在連接的第三階段——決議階段中發生,

4、準備

準備階段是為類中定義的變數(即靜態變數,被static修飾的變數)分配記憶體并設定類變數初始值的階段,初始值是指這個資料型別的零值,而賦值的程序是放在 <clinit> 方法中,在初始化階段執行的,注意實體變數是在創建實體物件時才初始化值的,

基本資料型別的零值:

準備階段還會為常量欄位(final 修飾的常量,即欄位表中有 ConstantValue 屬性的欄位)分配記憶體并直接賦值為定義的字面值,

User 類經過準備階段后:

5、決議

決議程序就是根據符號參考查找到物體,再把符號參考替換成一個直接參考的程序,因為所有的符號參考都保存在常量池中,所以這個程序常被稱作常量池決議,

1)靜態決議與動態連接:

所有方法呼叫的目標方法在Class檔案里面都是一個常量池中的符號參考,位元組碼中的方法呼叫指令就以常量池里指向方法的符號參考作為引數,這些符號參考一部分會在類加載階段或者第一次使用的時候就被轉化為直接參考,這種轉化被稱為靜態決議,另外一部分將在運行期間用到時轉化為直接參考,這部分稱為動態連接,

靜態決議的前提是:方法在程式真正運行之前就有一個可確定的呼叫版本,并且這個方法的呼叫版本在運行期是不可改變的,這類方法包含 靜態方法、私有方法、實體構造器、父類方法以及被 final 修飾的方法,這5種方法呼叫會在類加載的時候就把符號參考決議為該方法的直接參考(有可能是在初始化的時候去決議的),

動態連接這個特性給Java帶來了更強大的動態擴展能力,比如使用運行時物件型別,因為要到運行期間才能確定具體使用的型別,這也使得Java方法呼叫程序變得相對復雜,某些呼叫需要在類加載期間,甚至到運行期間才能確定目標方法的直接參考,

2)符號參考決議:

對于符號參考型別如 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等,會查找到對應的型別資料、方法地址、欄位地址的直接參考,然后將符號參考替換為直接參考,

對于 CONSTANT_String _info 型別指向的字面量,虛擬機會檢查字串常量池中是否已經有相同字串的參考,有則替換為這個字串的參考,否則在堆中創建一個新的字串物件,并將物件的參考放到字串常量池中,然后替換常量池中的符號參考,

對于數值型別的常量,如 CONSTANT_Long_info、CONSTANT_Integer_info,并不需要決議,虛擬機會直接使用那些常量值,

6、初始化

直到初始化階段,Java虛擬機才真正開始執行類中撰寫的Java程式代碼,初始化階段就是執行類構造器 <clinit> 方法的程序,

1)<clinit> 方法:

  • <clinit> 方法是由編譯器自動收集類中的所有類變數的賦值陳述句和靜態代碼塊合并產生的,代碼執行的順序就是源檔案中的順序,
  • Java虛擬機會保證在子類的 <clinit> 方法執行前,父類的 <clinit> 方法會先執行完畢,即先初始化直接超類,
  • <clinit> 方法對于類或介面來說不是必需的,如果一個類中沒有靜態陳述句塊,也沒有對變數的賦值操作,那么編譯器可以不為這個類生成 <clinit> 方法,
  • 介面中不能使用靜態陳述句塊,但仍然有變數初始化的賦值操作,因此介面與類一樣都會生成 <clinit> 方法,
  • 執行介面的 <clinit> 方法不需要先執行父介面的 <clinit> 方法,因為只有當父介面中定義的變數被使用時,父介面才會被初始化,此外,介面的實作類在初始化時也一樣不會執行介面的 <clinit> 方法,
  • Java虛擬機會保證一個類的 <clinit> 方法在多執行緒環境中被正確地加鎖同步,<clinit> 一定是執行緒安全的,

2)User 類初始化后:

一個類被裝載、連接和初始化完成后,它就隨時可以使用了,程式可以訪問它的靜態欄位,呼叫它的靜態方法,或者創建它的實體,

7、即時編譯

初始化完成后,類在呼叫執行程序中,執行引擎會把位元組碼轉為機器碼,然后在作業系統中才能執行,在位元組碼轉換為機器碼的程序中,虛擬機中還存在著一道編譯,那就是即時編譯,

最初,虛擬機中的位元組碼是由解釋器( Interpreter )完成編譯的,當虛擬機發現某個方法或代碼塊的運行特別頻繁的時候,就會把這些代碼認定為“熱點代碼”,為了提高熱點代碼的執行效率,在運行時,即時編譯器(JIT)會把這些代碼編譯成與本地平臺相關的機器碼,并進行各層次的優化,然后保存到記憶體中,這樣可以減少解釋器的中間損耗,獲得更高的執行效率,如果沒有即時編譯,每次運行相同的代碼都會使用解釋器編譯,

五、類加載器

1、類加載器子系統

在Java虛擬機中,負責查找并裝載型別的那部分被稱為類加載器子系統,類加載器子系統會負責整個類加載的程序:裝載、驗證、準備、決議、初始化,

1)Java 虛擬機有兩種類加載器,啟動類加載器和用戶自定義類加載器:

  • 啟動類加載器:是Java虛擬機實作的一部分,啟動類加載器主要用來加載受信任的Java API 的 Class 檔案,
  • 用戶自定義類加載器:是Java程式的一部分,用戶自定義的類加載器都是 java.lang.ClassLoader 的子類實體,開發人員可以自己控制位元組流的加載方式,

2)類唯一性:

對于任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間,由不同的類加載器加載的類將被放在虛擬機內部的不同命名空間中,比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class檔案,被同一個Java虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等,這就是有時候我們測驗代碼時發現明明是同一個Class,卻報強轉失敗之類的錯誤,

2、雙親委派模型

Java 1.8 之前采用三層類加載器、雙親委派的類加載架構,三層類加載器包括啟動類加載器、擴展類加載器、應用程式類加載器,

1)三層類加載器

  • 啟動類加載器(Bootstrap ClassLoader):負責將 $JAVA_HOME/lib 或者 -Xbootclasspath 引數指定路徑下面的檔案(按照檔案名識別,如rt.jar、tools.jar,名字不符合的類別庫即使放在lib目錄中也不會被加載) 加載到虛擬機記憶體中,它用來加載 Java 的核心庫,是用原生代碼實作的,并不繼承自 java.lang.ClassLoader,啟動類加載器無法直接被 java 代碼參考,
  • 擴展類加載器(Extension ClassLoader):負責加載 $JAVA_HOME/lib/ext 目錄中的檔案,或者 java.ext.dirs 系統變數所指定的路徑的類別庫,它用來加載 Java 的擴展庫,
  • 應用程式類加載器(Application ClassLoader):一般是系統的默認加載器,也稱為系統類加載器,它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類,一般 Java 應用的類都是由它來完成加載的,可以通過 ClassLoader.getSystemClassLoader() 來獲取它,

2)雙親委派模型

除了啟動類加載器之外,所有的類加載器都有一個父類加載器,應用程式類加載器的父類加載器是擴展類加載器,擴展類加載器的父類加載器是啟動類加載器,一般來說,開發人員自定義的類加載器的父類加載器一般是應用程式類加載器,

雙親委派模型:類加載器在嘗試去查找某個類的位元組代碼并定義它時,會先代理給其父類加載器,由父類加載器先去嘗試加載這個類,如果父類加載器沒有,繼續尋找父類加載器,依次類推,如果到啟動類加載器都沒找到才從自身查找,這個類加載程序就是雙親委派模型,

首先要明白,Java 虛擬機判定兩個 Java 類是否相同,不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣,只有兩個類來源于同一個Class檔案,并且被同一個類加載器加載,這兩個類才相等,不同類加載器加載的類之間是不兼容的,

雙親委派模型就是為了保證 Java 核心庫的型別安全的,所有 Java 應用都至少需要參考 java.lang.Object 類,也就是說在運行的時候,java.lang.Object 這個類需要被加載到 Java 虛擬機中,如果這個加載程序由 Java 應用自己的類加載器來完成或者自己定義了一個 java.lang.Object 類的話,很可能就存在多個版本的 java.lang.Object 類,而這些類之間是不兼容的,通過雙親委派模型,對于 Java 核心庫的類加載作業由引導類加載器來統一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的,有了雙親委派模型,就算自己定義了一個 java.lang.Object 類,也不會被加載,

3)ClassLoader

類加載器之間的父子關系一般不是以繼承的關系來實作的,通常是使用組合、委托關系來復用父加載器的代碼,ClassLoader 中有一個 parent 屬性來表示父類加載器,如果 parent 為 null,就會呼叫本地方法直接使用啟動類加載器來加載類,類加載器在成功加載某個類之后,會把得到的 java.lang.Class 類的實體快取起來,下次再請求加載該類的時候,類加載器會直接使用快取的類的實體,而不會嘗試再次加載,

3、執行緒背景關系類加載器

執行緒背景關系類加載器可通過 java.lang.Thread 中的方法 getContextClassLoader() 獲得,可以通過 setContextClassLoader(ClassLoader cl) 來設定執行緒的背景關系類加載器,如果沒有通過 setContextClassLoader(ClassLoader cl) 方法進行設定的話,執行緒將繼承其父執行緒的背景關系類加載器,Java 應用運行的初始執行緒的背景關系類加載器是應用程式類加載器,在執行緒中運行的代碼可以通過此類加載器來加載類和資源,執行緒上線文類加載器使得父類加載器可以去請求子類加載器完成類加載的行為,這在一定程度上是違背了雙親委派模型的原則,

六、物件及其生命周期

1、實體化物件

1)實體化一個類有四種途徑:

  • 明確地使用 new 運算子
  • 呼叫 Class 或者 java.lang.reflcct.Constructor 物件的 newInstance() 方法
  • 呼叫任何現有物件的 clone() 方法
  • 通過 java.io.ObjectInputStream 類的 getObject() 方法反序列化

2)實體化物件的程序:

  • 1、當虛擬機要實體化一個物件時,首先從常量池中找到這個類的符號參考,并檢查這個符號參考代表的類是否已被加載、決議和初始化過,如果沒有,就會觸發相應的類加載程序,
  • 2、在類加載檢查通過后,虛擬機將為新生物件分配記憶體,為物件分配空間就是把一塊確定大小的記憶體塊從Java堆中劃分出來,
  • 3、記憶體分配完成之后,虛擬機必須將分配到的記憶體空間(不包括物件頭)都初始化為零值,這步操作保證了物件的實體欄位在Java代碼中可以不賦初始值就直接使用,使程式能訪問到這些欄位的資料型別所對應的零值,
  • 4、接下來,虛擬機還要對物件進行必要的設定,例如這個物件的型別資訊、元資料地址、物件的哈希碼、物件的GC分代年齡等資訊,這些資訊存放在物件的物件頭之中,根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,物件頭會有不同的設定方式,
  • 5、最后開始執行物件的建構式,即Class檔案中的 <init> 方法,按照開發人員的意圖對物件進行初始化,這樣一個真正可用的物件才算完全被構造出來,

2、物件的記憶體布局

在 HotSpot 虛擬機里,物件在堆記憶體中的存盤布局可以劃分為三個部分:物件頭(Header)、實體資料(Instance Data)和對齊填充(Padding),

1)物件頭:

物件頭主要由兩部分組成:Mark Word 和型別指標,如果是陣列物件,還會包含一個陣列長度,

  • Mark Word:用于存盤物件自身的運行時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,synchronized 鎖升級就依賴鎖標志、偏向執行緒等鎖資訊,垃圾回收新生代物件轉移到老年代則依賴于GC分代年齡,
  • 型別指標:物件指向它的型別元資料的指標,Java虛擬機通過這個指標來確定該物件是哪個類的實體,
  • 陣列長度:有了陣列長度,虛擬機就可以通過普通Java物件的元資料資訊確定Java物件的大小,如果陣列的長度是不確定的,將無法通過元資料中的資訊推斷出陣列的大小,

這三部分資料的長度在32位和64位的虛擬機(未開啟壓縮指標)中分別為32個位元和64個位元,64 位虛擬機中,為了節約記憶體可以使用選項 +UseCompressedOops 開啟指標壓縮,某些資料會由 64位壓縮至32位,

2)實體資料:

實體資料部分是物件真正存盤的有效資訊,即物件的各個欄位資料,無論是從父類繼承下來的,還是在子類中定義的欄位都必須記錄起來,

3)對齊填充:

對齊填充僅僅起著占位符的作用,由于HotSpot虛擬機的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,就是任何物件的大小都必須是8位元組的整數倍,物件頭部分已經被設計成正好是8位元組的倍數,因此,如果物件實體資料部分沒有對齊的話,就需要通過對齊填充來補全,

4)計算物件占用記憶體大小:

從上面的內容可以看出,一個物件對記憶體的占用主要分兩部分:物件頭和實體資料,在64位機器上,物件頭中的 Mark Word 和型別指標各占 64 位元,就是16位元組,實體資料部分,可以根據型別來判斷,如 int 占 4 個位元組,long 占 8 個位元組,字串中文占3個位元組、數字或字母占1個位元組來計算,就大概能計算出一個物件占用的記憶體大小,當然,如果是陣列、Map、List 之類的物件,就會占用更多的記憶體,

3、物件訪問定位

創建物件后,這個參考變數會壓入堆疊中,即一個 reference,它是一個指向物件的參考,這個參考定位的方式主要有兩種:使用句柄訪問物件和直接指標訪問物件,

1)通過句柄訪問物件:

使用句柄訪問的話,Java堆中將可能會劃分出一塊記憶體來作為句柄池,reference中存盤的就是物件的句柄地址,而句柄中包含了物件實體資料與型別資料各自具體的地址資訊,

使用句柄來訪問的最大好處就是 reference 中存盤的是穩定句柄地址,在物件被移動(垃圾收集時移動物件)時只會改變句柄中的實體資料指標,而 reference 本身不需要被修改,

2)通過直接指標訪問物件:

如果使用直接指標訪問的話,Java堆中物件的記憶體布局就必須放置訪問型別資料的相關資訊(Mark Word 中記錄了型別指標),reference 中存盤的直接就是物件地址,如果只是訪問物件本身的話,就不需要多一次間接訪問的開銷,

使用直接指標來訪問最大的好處就是速度更快,它節省了一次指標定位的時間開銷,HotSpot 虛擬機主要就是使用這種方式進行物件訪問,

4、垃圾收集

當物件不再被程式所參考時,它所使用的堆空間就需要被回收,以便被后續的新物件所使用,JVM 的記憶體分配管理機制會自動幫我們回收無用的物件,它知道如何確定物件不再被參考,什么時候去回收這些垃圾物件,使用什么回收策略來回收更高效,以及如何管理記憶體,這部分就是JVM的垃圾收集相關的內容了,

參考

本文是學習、參考了如下書籍和課程,再通過自己的總結和實踐總結而來,如果想了解更多深入的細節,建議閱讀原著,

《深入JAVA虛擬機 第二版》

《深入理解Java虛擬機:JVM高級特性與最佳實踐 第三版》

《極客時間:Java性能調優實戰》

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/5952.html

標籤:Java

上一篇:為什么 Redis 要比 Memcached 更火?

下一篇:Spring Boot 集成阿里云 OSS 進行檔案存盤

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more