JVM
JVM,也就是 Java 虛擬機,它最主要的作用就是對編譯后的 Java 字節碼文件逐行解釋,翻譯成機器碼指令,并交給對應的操作系統去執行。
JVM 的其他特性有:
- JVM 可以自動管理內存,通過垃圾回收器回收不再使用的對象并釋放內存空間。
- JVM 包含一個即時編譯器 JIT,它可以在運行時將熱點代碼緩存到 codeCache 中,下次執行的時候不用再一行一行的解釋,而是直接執行緩存后的機器碼,執行效率會大幅提高。
注:JIT 和 JNI 的區別:
JIT 是“即時編譯”(Just-In-Time Compilation)的縮寫。
它是一種在程序運行時將字節碼(bytecode)編譯成機器碼的技術,主要用于提高程序的執行效率。JNI 是Java 提供的一種編程框架,允許 Java 代碼與用其他語言(如 C、C++)編寫的本地代碼交互。
通過 JNI,Java 程序可以調用本地方法(native methods),這些方法通常是用 C 或 C++ 實現的,
并編譯為動態鏈接庫(DLL 或 SO 文件)。反過來,本地代碼也可以調用 Java 方法
- 任何可以通過 Java 編譯的語言,比如說 Groovy、Kotlin、Scala 等,都可以在 JVM 上運行。
“通過 Java 編譯”在問題中的含義是指“編譯成 Java 字節碼”,每種語言都有自己的專用編譯器。
JVM的組織架構
JVM 要解釋執行需要進行三個步驟:加載 .class 文件 -> 準備數據 -> 執行
因此 JVM 大致可以劃分為三個部分:類加載器、運行時數據區和執行引擎。
JVM內存模型
Java 虛擬機(JVM)在運行 Java 程序時,會將內存劃分為若干區域,每個區域有其特定的功能。
-
程序計數器(Program Counter Register)
- 作用:記錄當前線程正在執行的字節碼指令的地址,用于控制程序的執行流程。
- 特點:每個線程都有獨立的程序計數器,線程之間互不干擾。
-
Java 虛擬機棧(Java Virtual Machine Stacks)
- 作用:為 Java 方法(非 native 方法)的執行提供內存空間。
- 特點:每個線程擁有自己的虛擬機棧,棧中包含多個棧幀(Stack Frame)。每個棧幀對應一次方法調用,存儲局部變量表、操作數棧、動態鏈接和方法出口等信息。
-
本地方法棧(Native Method Stacks)
- 作用:為 native 方法(用 C/C++ 等語言編寫的方法)提供內存支持。
- 特點:與 Java 虛擬機棧類似,但專用于 native 方法的執行。
-
Java 堆(Java Heap)
- 作用:存儲對象實例和數組。
- 特點:JVM 中最大的內存區域,所有對象實例都在堆上分配內存。堆是垃圾收集器(GC)的主要管理區域。
-
方法區(Method Area)
- 作用:存儲類的元數據信息(如類結構)、靜態變量、常量以及即時編譯器編譯后的代碼。
- 特點:在 JDK 8 及之后,方法區被元空間(Metaspace)取代,元空間使用本地內存而非 JVM 堆內存。
-
運行時常量池(Runtime Constant Pool)
- 作用:存儲編譯期生成的字面量(如字符串常量)和符號引用。
- 特點:是方法區的一部分,包含每個類的常量池表。JDK 8 方法區變元空間,運行時常量池就放在堆上。
-
直接內存(Direct Memory)
- 作用:不屬于 JVM 運行時數據區,但常用于 NIO(如 ByteBuffer)等場景。
- 特點:使用本地內存,不受 JVM 堆大小限制。
內存區域變化
主要是方法區到元空間,以及常量池的變化
字符串常量池,類常量池,運行時常量池存儲的都是什么啊?
1.字符串常量池
- 字符串常量池主要存儲 字符串字面量,也就是在 Java 代碼中用雙引號括起來的字符串,例如 “Hello”、“World” 等。
- 它的設計目的是為了復用這些字符串對象,確保 JVM 中每個唯一的字符串字面量只有一份,從而節省內存。
- String s1 = “Hello”; String s2 = “Hello”;用 s1 == s2 驗證,結果為 true
- 用 new String(“Hello”) 創建字符串,這個對象會分配在堆上,可以通過 String.intern() 方法將它放入字符串常量池。
2.類常量池
- javac 將源文件編譯成 .class 文件,類常量池指的是這個文件的一部分,是在磁盤上的。
- 類常量池存儲了在編譯時生成的 字面量 和 符號引用
- 類加載階段JVM 加載 .class 文件 時,會把類常量池的內容 拷貝到方法區(JDK 8+ 在元空間)
- 在解析階段,邏輯地址會被替換為 實際的內存地址,部分數據進入 運行時常量池。
例如對于一個類:
package example;import utils.Helper; // 🔹 導入外部類public class Main {// 🔹 靜態常量static final double PI = 3.14159;// 🔹 實例變量private String name;// 🔹 構造方法public Main(String name) {this.name = name;}// 🔹 普通方法public void greet() {System.out.println("Hello, " + name);}public static void main(String[] args) {// 🔹 創建對象Main obj = new Main("Alice");obj.greet();// 🔹 調用外部類方法Helper.sayHello();}
}
類常量池為:
CONSTANT_Class example/Main
CONSTANT_Class utils/Helper // 🔹 外部類
CONSTANT_Fieldref example/Main.PI
CONSTANT_Fieldref example/Main.name
CONSTANT_Methodref example/Main.<init> // 🔹 構造方法
CONSTANT_Methodref example/Main.greet // 🔹 方法
CONSTANT_Methodref utils/Helper.sayHello // 🔹 外部類方法
CONSTANT_String "Hello, "
CONSTANT_String "Alice"
CONSTANT_Double 3.14159
3.運行時常量池
- 運行時常量池是 JVM 在運行時為每個類或接口維護的常量池
- 運行時常量池支持動態鏈接和運行時解析,例如將對 System.out.println 的符號引用解析為具體的對象和方法地址。
- 它還能在程序運行時擴展,例如添加新的字符串常量
- 運行時常量池 = 類常量池內容 + 直接引用 + 動態常量
堆內存
堆 是Java虛擬機(JVM)中內存管理的一個重要區域,主要用于存放對象實例和數組。隨著JVM的發展和不同垃圾收集器的實現,堆的具體劃分可能會有所不同,但通常可以分為以下幾個部分:
- 新生代:新生代又被劃分為 Eden 空間和兩個 Survivor 空間(From 和 To)
- 新創建的對象會被分配到 Eden 空間。
- Eden 區填滿時,會觸發一次 Minor GC,清除不再使用的對象。存活下來的對象會從 Eden 區移動到 Survivor 區
- 老年代:對象在新生代中經歷多次 GC 后,如果仍然存活,會被移動到老年代。當老年代內存不足時,會觸發 Major GC,對整個堆進行垃圾回收。
- 大對象區:在某些JVM實現中(如G1垃圾收集器),為大對象分配了專門的區域,這部分區域在老年代。
對象的內存布局
對象的內存布局是由 Java 虛擬機規范定義的,但具體的實現細節各有不同,如 HotSpot 和 OpenJ9 就不一樣。HotSpot:
對象四種引用
四種分別是“強、軟、弱、虛”。
- 強引用:Object obj = new Object(); 只要存在就不回收;
- 軟引用:SoftReference softRef =
new SoftReference<>(new Object())
; 內存不足回收,常用于實現內存敏感的緩存(如圖片緩存),在內存壓力大時自動清理; - 弱引用:WeakReference weakRef =
new WeakReference<>(new Object());
一定回收,臨時引用,避免內存占用,比如 threadlocal 里的 key; - 虛引用:PhantomReference phantomRef =
new PhantomReference<>(new Object(), queue);
通常與 ReferenceQueue 結合使用,當對象被回收時,虛引用會被放入關聯的 ReferenceQueue,在對象被回收時收到通知。虛引用不可達; - Java.lang.ref 包下的類
內存泄漏和內存溢出
用一個比較有味道的比喻來形容就是,內存溢出是排隊去蹲坑,發現沒坑了;內存泄漏,就是有人占著茅坑不拉屎,導致坑位不夠用。
- 內存泄漏舉例:1、靜態屬性導致內存泄露 2、 未關閉的資源 3、 使用ThreadLocal
- 靜態屬性(用 static 修飾的字段)屬于類級別,其生命周期與類的加載和卸載綁定,其超長的生命周期和全局可見性導致更容易內存泄漏
ThreadLocal—java