深入淺出JVM性能調優——JVM內存模型和類加載運行機制

2020-09-13 瀟灑的程式設計師

一、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 = &34;; 6 public static final String SEX_WOMAN = &34;; 7 8 static { 9 System.out.println(&34;); 10 System.out.println(&34; + SEX_MAN); 11 } 12 13 public void sayHello(T str) { 14 System.out.println(&34; + 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 = &34;; 31 public static final int AGE_MAX = 100; 32 33 // 靜態變量 34 private static String staticName = &34;; 35 private static int staticAge = 20; 36 37 // 類屬性 38 private String name = &34;; 39 private int age = 25; 40 41 // 靜態代碼塊 42 static { 43 System.out.println(&34;); 44 System.out.println(&34; + staticName); 45 System.out.println(&34; + 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(&34; + name + &34; + age); 59 } 60 61 // 靜態方法 62 public static void staticPrintInfo() { 63 System.out.println(&34; + FIELD_NAME + &34; + AGE_MAX); 64 } 65 66 // 泛型方法重載 67 @Override 68 public void sayHello(String str) { 69 super.sayHello(str); 70 System.out.println(&34; + 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(&34;); 81 return i; 82 } finally { 83 System.out.println(&34;); 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(&34; + User.FIELD_NAME); 98 99 User.staticPrintInfo();100 101 User user = new User();102 user.printInfo();103 }104 }

三、類編譯和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 &34; 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 29.34;<init>&2 = String 3 = Fieldref 78 // com/lyyzoo/jvm/test01/User.name:Ljava/lang/String; 21 14.5 = Fieldref 81 // java/lang/System.out:Ljava/io/PrintStream; 23 82 // java/lang/StringBuilder 24 6.34;<init>&8 = String 9 = Methodref 84 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 27 85 // , age: 28 6.12 = Methodref 87 // java/lang/StringBuilder.toString:()Ljava/lang/String; 30 88.14 = Class 15 = String 16 = Methodref 92 // com/lyyzoo/jvm/test01/Person.sayHello:(Ljava/lang/Object;)V 34 93 // User say hello: 35 94 // finally handle 36 95 // java/lang/Exception 37 96 // catch exception 38 97 // java/lang/String 39 14.23 = String 24 = Fieldref 100 // com/lyyzoo/jvm/test01/User.staticName:Ljava/lang/String; 42 14.26 = String 27 = String 28 = String 29 = Class 30 = Class 31 = Utf8 serialVersionUID 49 33 = Utf8 ConstantValue 51 36 = Utf8 FIELD_NAME 53 38 = String 39 = Utf8 AGE_MAX 56 41 = Integer 100 58 43 = Utf8 staticAge 60 45 = Utf8 age 62 47 = Utf8 ()V 64 49 = Utf8 LineNumberTable 66 51 = Utf8 this 68 53 = Utf8 (Ljava/lang/String;I)V 70 55 = Utf8 printInfo 72 57 = Utf8 sayHello 74 59 = Utf8 str 76 61 = Utf8 ()I 78 63 = Utf8 e 80 65 = Utf8 i 82 67 = Class 68 = Class 69 = Class 70 = Utf8 (Ljava/lang/Object;)V 87 72 = Utf8 Signature 89 74 = Utf8 SourceFile 91 76 = NameAndType 47 // &34;:()V 93 78 = NameAndType 37 // name:Ljava/lang/String; 95 45:80 = Class 81 = NameAndType 111 // out:Ljava/io/PrintStream; 98 83 = Utf8 name:100 112:85 = Utf8 , age:102 112:87 = NameAndType 116 // toString:()Ljava/lang/String;104 117 // java/io/PrintStream105 118:90 = Utf8 com/lyyzoo/jvm/test01/User107 92 = NameAndType 70 // sayHello:(Ljava/lang/Object;)V109 94 = Utf8 finally handle111 96 = Utf8 catch exception113 98 = NameAndType 58 // sayHello:(Ljava/lang/String;)V115 100 = NameAndType 37 // staticName:Ljava/lang/String;117 43:102 = Utf8 user static init119 104 = Utf8 staticAge=121 106 = Utf8 java/io/Serializable123 108 = Utf8 java/lang/Throwable125 110 = Utf8 out127 112 = Utf8 append129 114 = Utf8 (I)Ljava/lang/StringBuilder;131 116 = Utf8 ()Ljava/lang/String;133 118 = Utf8 println135 {136 【欄位表集合】137 public static final java.lang.String FIELD_NAME;138 descriptor: Ljava/lang/String;139 flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL140 ConstantValue: String username141 142 public static final int AGE_MAX;143 descriptor: I144 flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL145 ConstantValue: int 100146 147 【方法表】148 public com.lyyzoo.jvm.test01.User();149 【描述符索引】150 descriptor: ()V151 【訪問標誌】152 flags: ACC_PUBLIC153 【方法體代碼指令】154 Code:155 【方法棧大小】156 stack=2, locals=1, args_size=1157 0: aload_0158 1: invokespecial 34;<init>&2 // String 蘭博161 7: putfield 4 // Field age:I165 16: return166 【屬性表,方法局部變量】 167 LineNumberTable:168 line 27: 0169 line 17: 4170 line 18: 10171 line 28: 16172 【本地變量表,方法入參】173 LocalVariableTable:174 Start Length Slot Name Signature175 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)V179 flags: ACC_PUBLIC180 Code:181 stack=2, locals=3, args_size=3182 0: aload_0183 1: invokespecial 34;<init>&2 // String 蘭博186 7: putfield 4 // Field age:I190 16: aload_0191 17: aload_1192 18: putfield 4 // Field age:I196 26: return197 LineNumberTable:198 line 30: 0199 line 17: 4200 line 18: 10201 line 31: 16202 line 32: 21203 line 33: 26204 LocalVariableTable:205 Start Length Slot Name Signature206 【可以看出,對象實例方法的第一個參數始終都是 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 I210 MethodParameters:211 Name Flags212 name213 age214 215 public void printInfo();216 descriptor: ()V217 flags: ACC_PUBLIC218 Code:219 stack=3, locals=1, args_size=1220 0: getstatic 6 // class java/lang/StringBuilder222 6: dup223 7: invokespecial 34;<init>&8 // String name:225 12: invokevirtual 3 // Field name:Ljava/lang/String;228 19: invokevirtual 10 // String , age:230 24: invokevirtual 4 // Field age:I233 31: invokevirtual 12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;235 37: invokevirtual 5 // Field java/lang/System.out:Ljava/io/PrintStream;251 3: ldc 13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V253 8: return254 LineNumberTable:255 line 42: 0256 line 43: 8257 【注意,靜態方法第一個參數就不再是 this 了】 258 259 260 public void sayHello(java.lang.String);261 descriptor: (Ljava/lang/String;)V262 flags: ACC_PUBLIC263 Code:264 stack=3, locals=2, args_size=2265 0: aload_0266 1: aload_1267 2: invokespecial 5 // Field java/lang/System.out:Ljava/io/PrintStream;269 8: new 7 // Method java/lang/StringBuilder.&34;:()V272 15: ldc 9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;274 20: aload_1275 21: invokevirtual 12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;277 27: invokevirtual 5 // Field java/lang/System.out:Ljava/io/PrintStream;306 12: ldc 13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V308 17: iload_3309 18: ireturn310 19: astore_2311 20: getstatic 20 // String catch exception313 25: invokevirtual 5 // Field java/lang/System.out:Ljava/io/PrintStream;317 33: ldc 13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V319 38: iload_3320 39: ireturn321 40: astore 4322 42: getstatic 18 // String finally handle324 47: invokevirtual 21 // class java/lang/String370 5: invokevirtual 23 // String Rambo388 2: putstatic 25 // Field staticAge:I391 10: getstatic 26 // String user static init393 15: invokevirtual 5 // Field java/lang/System.out:Ljava/io/PrintStream;395 21: new 7 // Method java/lang/StringBuilder.&34;:()V398 28: ldc 9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;400 33: getstatic 9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;402 39: invokevirtual 13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V404 45: getstatic 6 // class java/lang/StringBuilder406 51: dup407 52: invokespecial 34;<init>&28 // String staticAge=409 57: invokevirtual 25 // Field staticAge:I411 63: invokevirtual 12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;413 69: invokevirtual 73 // Lcom/lyyzoo/jvm/test01/Person<Ljava/lang/String;>;Ljava/io/Serializable;424 SourceFile: &34;

我們也可以安裝 [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 方法,那麼第三個指令就會在常量池尋找 [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的垃圾收集相關的內容了。

相關焦點

  • JVM性能調優——JVM內存模型和類加載運行機制
    JVM 不僅承擔了 Java 字節碼的分析和執行,同時也內置了自動內存分配管理機制。這個機制可以大大降低手動分配回收機制可能帶來的內存洩露和內存溢出風險,使 Java 開發人員不需要關注每個對象的內存分配以及回收,從而更專注於業務本身。
  • 大白話談JVM的class類加載機制
    前言我們很多小夥伴平時都是做JAVA開發的,那麼作為一名合格的工程師,你是否有仔細的思考過JVM的運行原理呢。如果懂得了JVM的運行原理和內存模型,像是一些JVM調優、垃圾回收機制等等的問題我們才能有一個更清晰的概念。
  • Github上都在瘋找的京東內部「JVM調優筆記」終於來了
    性能調優在很大程度上是一門藝術。解決的 GC 性能問題越多,技藝才會越精湛。我們不只要關心 GC 的持續演進,也要積極地去了解它的設計原理和設計目標。針對Java程序的性能優化一定不可能避免針對JVM的調優,隨著JVM的不斷發展,我們的應對措施也在不斷地跟隨、變化,內存的使用逐漸變得越來越複雜。所有高級語言都需要垃圾回收機制的保護,所以GC就是這麼重要。
  • Java面試總結之JVM
    有5種方法可以完成初始化:1.調用new方法,2.使用Class類的newInstance方法(反射機制),3.使用Constructor類的newInstance方法(反射機制),4.使用Clone方法創建對象,5.使用(反)序列化機制創建對象使用:完成類的初始化後,就可以對類進行實例化,在程序中進行使用了卸載:當類被加載,連接和初始化後,它的生命周期就始了,當代表類的
  • 大廠面試系列(一)::JVM基礎
    jvm內存模型,內存屏障對象一定分配在堆棧對象不一定分配在堆上,JIT可以實現棧上分配Java線程模型和jvm線程模型區分Java堆的內存結構? 在什麼地方會發生OOM? 如何分析OOM發生的原因?JVM 運行時區域 常見的堆內存溢出情況棧溢出的情形(遞歸,調節-Xss類加載器什麼是雙親委派模型?
  • JVM加載class文件的原理機制詳解
    回到頂部3、JVM加載class文件的原理機制    Java中的所有類,都需要由類加載器裝載到JVM中才能運行。類加載器本身也是一個類,而它的工作就是把class文件從硬碟讀取到內存中。  類裝載方式,有兩種   1.隱式裝載, 程序在運行過程中當碰到通過new 等方式生成對象時,隱式調用類裝載器加載對應的類到jvm中,   2.顯式裝載, 通過class.forname()等方法,顯式加載需要的類   隱式加載與顯式加載的區別:兩者本質是一樣?
  • Java性能調優:JVM性能監控常用方法
    一、前言本小節會介紹JVM性能監控,掌握幾種常用的監控工具輔助我們更好的了解JVM的性能狀態。生產環境中監控JVM性能,分析監控數據,可以知道何時需要JVM調優,可見監控是非常重要的。JVM的監控範圍包括垃圾收集、JIT編譯以及類加載。那其中具體都包含哪些?如何監控呢?
  • Java多線程 JVM內存結構 Java對象模型
    整體方向上的區別jvm內存結構: 和Java虛擬機的運行時區域有關.Java內存模型: 和Java的並發編程有關Java對象模型: 和Java對象在虛擬機中的表現形式有關.JVM內存結構Java代碼是運行在jvm虛擬機上的, 並且分為了不同的區域.
  • JVM經典面試問題(內存溢出和內存洩露)解答及調優實戰分析
    雖然方法區中的回收收益一般都不高,但是也是會被GC的,而方法區中被回收的最主要的就是對廢棄常量和無用類的回收,判定一個廢棄常量比較簡單,但是判定一個類是無用類是比較困難的,那麼方法區中的怎麼判斷一個類是無用類呢?
  • 關於JVM類加載機制,看這一篇就夠了
    類的加載機制Java虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這個過程被稱作虛擬機的類加載機制。()區別Class.forName(): 將類的.class文件加載到jvm中之外,還會對類進行解釋,執行類中的static塊;ClassLoader.loadClass(): 只幹一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance才會去執行static塊。
  • JVM面試題之你能說說JVM有哪些內存區域嗎
    面試題你能說說JVM內存模型嗎?面試題分析面試官問這樣的問題,主要考察你對JVM內存模型是否了解。只有了解了內存模型,才能去搞明白GC原理,這個問題是網際網路公司面試最常問的了。方法區:jdk1.8後改成了元數據區,主要存放加載的類信息;堆內存:存放創建的實例對象,是所有線程共享的內存區域;虛擬機棧
  • JVM入門(一)類的加載過程
    如果我們是以jar的方式進行運行java程序,那麼執行 :java -jar *.jar com.xx.Hello 起中 Hello是這個jar中包含了main方法的類的名稱。無論是那種方式,當我們執行的時候,就會啟動jvm虛擬機去加載所要執行的Hello.class文件到虛擬機中。
  • JVM 運行時數據區 - 多圖預警、萬字內存模型解讀
    對於 java 程式設計師來說,在虛擬機自動內存管理機制的幫助下,不容易出現內存洩漏和內存溢出。有虛擬機管理內存,這一切看起來都很美好。但是,也正因為java把內存控制的權力給了java虛擬機,一旦出現內存洩漏和溢出方面的問題。如果不了解虛擬機是怎麼樣使用內存的,那麼排查錯誤將會成為一項異常艱難的工作。
  • JVM的基礎知識點Java的內存模型
    今天梳理一下JVM的基礎知識點Java的內存模型!本地方法棧是什麼:本地方法棧的作用和虛擬機棧非常像是,只不過本地方法棧是native方法的內存模型,每一個native方法從調用到執行完成就對應著一個棧幀在本地方法棧中的入棧和出棧。
  • 概述:JVM內存模型、線程隔離數據區、線程共享數據區
    在程序運行的這一過程中,jvm會將其管理的內存空間劃分為不同的區域,這些區域各有各的用途,我們將其分為五類:方法區堆虛擬機棧本地方法棧1.3 垃圾回收策略另外值得一提的是,堆往往和垃圾回收問題一起出現,所以這裡也簡單的介紹一下內存分配和回收的策略:由於jvm內存回收機制採用了分代收集算法,所以java堆中還分為新生代和老年代,新生代中又分為佔大部分控制項的
  • 常見的jvm調優策略
    一般來說,jvm的調優策略是沒有一種固定的方法,只有依靠我們的知識和經驗來對項目中出現的問題進行分析,正如吉德林法則那樣當你已經把問題清楚寫出來,就已經解決了一半。雖然JVM調優中沒有固定的策略,但是本文會介紹幾種比較常見的調優策略。
  • JVM內存以及GC的執行機制全解析,滿是乾貨,助你深入開發
    了解C語言的同學都知道,在C語言中內存的開闢和釋放都是由我們自己來管理的,每一個new操作都要對於一個delete操作,否則就會參數內存洩漏和溢出的問題,導致非常糟糕的後果。但在Java開發過程中,則完全不需要擔心這個問題。因為jvm提供了自動內存管理的機制。內存管理的工作由jvm幫我們完成。
  • JVM&G1 GC實戰來了!深入淺出的機制,深度把握JVM高級特性和實踐
    針對Java程序的性能優化一定不可能避免針對JVM的調優,隨著JVM的不斷發展,我們的應對措施也在不斷地跟隨、變化,內存的使用逐漸變得越來越複雜。所有高級語言都需要垃圾回收機制的保護,所以GC就是這麼重要。
  • java線程前傳——jvm內存結構、內存模型和cpu結構
    ,這個過程是少不了的一個線程肯定是要運行在一個核上的,多個線程可以運行在不同的核上,這個時候,因為緩存的存在,如果沒有同步機制,那一個線程修改了緩存的數據,另一個線程也修改了緩存的數據,這個時候這兩個線程修改後的數據都需要寫入到內存當中,就會出現問題jvm為了方便,將這些緩存抽象出來,構造了自己的內存模型,即主內存和工作內存的數據交互,即java 內存模型(jmm)
  • 面試官:別的我不管,這個JVM虛擬機內存模型你必須知道
    前言說jvm的內存模型前先了解一下物理計算機的內存處理。物理計算器上用戶磁碟和cpu的交互,由於cpu讀寫速度速度遠遠大於磁碟的讀寫速度速度,所以有了內存(高速緩存區)。類裝載子系統將字節碼文件加載進運行時數據區。