目錄
1. 區分JDK,JRE 和 JVM
1.1 JVM
1.2 JRE
1.3 JDK
1.4 關系總結
2.?跨平臺性
3. JVM中的內存劃分
4. JVM的類加載機制
5. 雙親委派模型
6. 垃圾回收機制(GC)
6.1 識別垃圾
6.1.1 單個引用
6.1.2 多個引用
6.2 釋放垃圾
6.2.1 標記—釋放
6.2.2 復制算法
6.2.3 標記—整理
6.2.4 分代回收
1. 區分JDK,JRE 和 JVM
1.1 JVM
JVM是?Java 程序運行的核心,負責執行 Java 字節碼(.class文件),使其能在不同操作系統上運行。
特點:
- 可以將.class文件編譯為機器碼執行(跨平臺能力)
- 自動垃圾回收機制(GC),管理堆、棧、方法區等內存區域。
- 不包含開發工具或核心類庫,僅負責運行編譯后的程序。
1.2 JRE
JRE 是 Java 程序運行的最小環境,包含 JVM 和運行所需的核心類庫(如?java.lang、java.util、java.io)。
特點:
- 僅支持運行已編譯的 Java 程序(.jar 或 .class?文件)。
- 不包含編譯器(javac)或開發工具,無法用于開發。
1.3 JDK
JDK是Java開發的完整工具包,包含JRE和一些開發工具
特點:
- 支持編寫、編譯、調試和運行 Java 程序(.java?→?.class?→ 執行)。
- 提供開發工具如 javac(編譯器,可以將 .java文件編譯為 .class 文件),javadoc(文檔生成),jbd(調試器)等。
1.4 關系總結
- JDK = JRE + 開發工具
- JRE = JVM + 核心類庫
- JVM 是執行引擎,依賴 JRE 提供的類庫運行程序
- JRE是Java程序運行的最小環境配置
2.?跨平臺性
JVM類似一個翻譯官,是實現跨平臺性的關鍵
java文件先通過 javac 編譯為 .class 文件,JVM又會將 .class文件轉換為cpu可以識別的機器指令
因此,發布一個java程序,本質上發布 .class文件即可,JVM拿到 .class文件,就會自動進行轉換
- windows上的JVM,就會把 .class文件轉換為 windows 操作系統可以識別的機器指令
- Linux?上的JVM,就會把 .class文件轉換為 Linux?操作系統可以識別的機器指令
- 不同操作系統上面的JVM是不同的
3. JVM中的內存劃分
JVM在運行的過程中,相當于一個進程,進程在運行的過程中,需要從操作系統中申請一塊空間(內存資源,CPU資源等),這些內存空間,支撐了后續java程序的執行,比如定義變量需要內存空間,這里獲取的內存空間,并不是直接給操作系統要,而是JVM給的
JVM從操作系統中申請一大塊內存空間,給java程序使用,這一大塊內存空間又會根據實際的使用用途劃分出不同的空間出來,這樣的好處就是便于管理和提供調用效率
JVM將申請到的空間,進行區域劃分,不同的區域具有不同的作用
堆
- 代碼中new出來的對象,都存放在堆中
- 堆也是垃圾回收的主要區域
虛擬機棧
- 虛擬機棧的生命周期和線程相同,線程啟動時創建,線程結束時銷毀
- 負責管理Java方法的調用和執行
- 由棧幀組成,每個方法調用對應一個棧幀
- 棧幀中存儲局部變量表、操作數 、動態鏈接、方法返回地址等信息。
棧幀
- 局部變量表:存放方法參數和局部變量。
- 操作數棧:執行方法時的工作區(先進后出)
- 動態鏈接:指向運?時常量池的方法引用(在方法中調用另一個方法)
- 方法返回地址:PC寄存器的地址
本地方法棧
和虛擬機棧的功能類似,只不過java虛擬機站是給JVM使用,本地方法棧是給本地方法使用。(常見的本地接口JNI調用非java代碼)
程序計數器
只會占用較小的空間,用于存儲下一條要執行的java指令的地址
元數據區
元數據一般指的是一些輔助性質的數據
元數據區:存儲被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據的。
一個程序有哪些類,每個類中有哪些方法,每個方法包含哪些指令都會被記錄在元數據區中
區分一個變量在那個內存區域中,主要根據變量的類型
- 成員變量——存儲在堆中
- 靜態成員變量——存儲在元數據區中? ?
- 局部變量——存儲在虛擬機棧中? ? ?
- 方法——存儲在元數據區
- 方法在運行時——存儲在虛擬機棧中
4. JVM的類加載機制
類加載:java進程在運行的時候,需要把 .class 文件從硬盤,讀取到內存中,并進行一系列的校驗解析,最終轉換成可執行代碼的過程的過程
類加載的過程大體分為5個步驟,加載、驗證、準備、解析、初始化五個階段
1)加載?
- 將硬盤上的 .class文件找到并打開,讀取文件中的內容(二進制字節流)
- 將字節流表示的靜態數據結構轉為方法區中的運行數據結構
- 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區中訪問這個類中各種數據的入?。
2)驗證
保證讀取到的數據是合法的,符合虛擬機的約束要求,確保這些數據被運行后不回危害虛擬機自身的安全
3)準備
給讀取到的類對象(static變量),分配內存空間,這時候內存空間中,默認值都為0或null
4)解析
主要針對類中的字符串常量進行處理,主要是將符號引用轉換為直接引用
- 在硬盤存儲中,并沒有地址這個概念,雖然沒有地址這個概念,但是存在類似地址“偏移量”的概念,也可以表示數據在文件中的具體位置
- 當數據被加載到內存中,此時就會分配地址,這時候會從符號引用變為直接引用
5)初始化
Java虛擬機真正執行類中編寫的Java程序代碼,完成后續的類對象初始化(觸發父類的加載,初始化靜態成員,執行靜態代碼塊等)
5. 雙親委派模型
雙親委派模型是Java類加載機制中的一種重要設計原則,它定義了類加載器如何協作加載類的規則。
在Java中,存在一個模塊專門執行類加載操作,稱為“類加載器”,主要負責將.class文件加載到內存中,并轉換為可執行的java.lang.Class類的實例
JVM中的類加載器默認存在三個,也可自定義一個
-
啟動類加載器(Bootstrap ClassLoader):負責查找標準庫的目錄
-
擴展類加載器(Extension ClassLoader):? 負責查找擴展庫的目錄
-
應用程序類加載器(Application/System ClassLoader):負責加載用戶編寫的程序代碼和查找第三方庫的目錄
這三個類加載器,存在類似二叉樹的父子關系(不是父子繼承關系)
雙親委派模型的工作流程:
- 從應用程序類加載器作為入口,開始工作,先加載用戶編寫的程序代碼,但是不會立即搜索自己負責的目錄,會把搜索任務交給父親(擴展類加載器)
- 代碼進入擴展類加載器的范疇,但是也不會立即開始搜索任務,而是將自己的搜索任務交給父親(啟動類加載器)
- 代碼進入啟動類加載器的范疇,但是也不會立即開始搜索工作,而是將自己的搜索任務交給父親
- 但是啟動類加載器不存在父親,那么就會真正的開始搜索工作,嘗試在標準庫中查找符合的.class文件,如果找到了,就進入后續的驗證,解析等流程中,如何沒有找到,就會回到孩子(擴展類加載器)中繼續嘗試加載
- 擴展類加載器收到父類返回的任務后,開始在擴展類中查找,如果找到了,就進入后續的驗證,解析等流程中,如何沒有找到,就會回到孩子(應用程序類加載器)中繼續嘗試加載
- 應用程序類加載器收到父類返回的任務,開始在第三方庫中查找,如果找到了進入后續的流程中,如果沒有找到,會繼續到孩子(自定義類加載器)中繼續進行加載,但是默認不存在孩子(自定義類加載器),這時候類加載過程就會失敗,拋出ClassNotFoundException異常
雙親委派模型的優點:
- 避免重復加載類,這樣嚴格規定查找的范圍,避免重復加載類
- 安全性,使?雙親委派模型也可以保證了Java的核心API不被篡改,如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現?些問題(如果多個類加載器獨立加載同一個類,會出現類沖突)
上述的規則是JVM中默認類加載器,遵守的規則,這樣的規則,其實也可以被打破
如果我們使用自定義類加載器,指定某個目錄進行查找并加載,自定義類加載器不和系統再帶的類加載器進行關聯(不調用默認類加載器的引用),那么自定義加載器就是獨立的,不會涉及雙親委派算法
6. 垃圾回收機制(GC)
定義一個變量會使用一塊內存空間,如果這個變量長時間用不到又不及時釋放掉,那么內存空間會為占據,導致后面想申請內存申請不到,還有可能導致內存泄漏問題,所以垃圾回收本質上是回收內存空間,更準確的是回收對象
在JVM中存在多個區域,有些區域不需要垃圾回收,對于程序計數器、虛擬機棧、本地方法棧這三部分區域, 其?命周期與相關線程有關,線程銷毀,會自動銷毀,這里垃圾回收的主要區域是方法區和堆
6.1 識別垃圾
判斷哪些對象后續還需要繼續使用,那些不需要繼續使用
6.1.1 單個引用
對象的使用一定伴隨著引用,如果一個對象沒有任何引用指向他,那么就可以視為垃圾。
匿名對象被使用完后應該自動銷毀,也應該被視為垃圾。
6.1.2 多個引用
如果存在多個引用指向同一個對象,必須確保每一個對象的引用都被銷毀了,才能將這個對象視為垃圾。
1)引用計數器
原理:
給每個對象再分配一個額外的空間,空間里面存儲這個對象存在幾個引用,可以設置一個專門的掃描線程,去獲取每個對象的引用計數情況,如果發現對象的引用為0,表示這個對象可以被釋放。
問題:
- 每個對象分配額外的空間,即使每個對象安排的計數器占很小的內存,如果程序中的對象數目很多,總消耗的空間也會很多。
- 如果出現循環引用問題,會導致引用計數器無法工作。
2)可達性分析
可達性分析會消耗更多的時間,但是不會產生循環引用這樣的問題。
如果出現:6.right = null ,則會導致7不可達,7不可達也會導致8不可達。
原理:
- 從一些特別的變量作為起點進行遍歷,沿著這些變量所持有的引用。逐層往下訪問,能被訪問到的對象就不是垃圾,如果訪問不到就是垃圾。
- JVM中存在掃描線程會不斷的對代碼中已有的變量進行遍歷,盡可能地在一定時間內訪問到更多的對象。
特殊的變量:
- 虛擬機棧中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
6.2 釋放垃圾
6.2.1 標記—釋放
在遍歷的過程中檢測到標志為垃圾的對象,直接釋放掉,但是一般不采用這種方法,會出現內存空間碎片化問題。
6.2.2 復制算法
將標志為垃圾的對象不直接釋放掉,而是將不是垃圾的對象復制到內存的另一半里,然后將垃圾的那一半空間整體釋放掉。
特點:
- 可以解決內存碎片化問題。
- 但是可用的內存空間變少了。
- 如果復制的對象比較多,會導致復制的開銷會很大。
6.2.3 標記—整理
類似于順序表。刪除掉標志為垃圾的對象,將正在使用的對象進行搬運,集中放在一端。
特點:
- 可以解決內存碎片化問題。
- 不會浪費太多的內存空間。
- 如果存活的對象很多,搬運的開銷會很大。
6.2.4 分代回收
根據不同種類的對象,采用不同的方式
- JVM中引入了對象年齡的概念并存在專門的線程負責周期性的掃描
- 如果一個對象被掃描的一次并可達,那么年齡就加1(初始年齡都為0)
- JVM會根據年齡的差異將整個堆內存分為兩個部分:新生代和老年代
1)當代碼中new出一個新的對象,這個對象會先存儲在伊甸區,創建出的新對象大部分都活不過第一輪GC,生命周期非常短。
2)? 第一輪GC掃描完成后,伊甸區中少數的對象會通過復制算法拷貝到生活區中,后續gc的掃描線程會持續進行掃描,生存區中的大部分對象也會在掃描中被標記為垃圾,少數存活的會繼續使用復制算法拷貝到另一個生活區中,只要這個對象能在生活區中繼續存活,就會被復制算法來回拷貝到另一半的生活區中,每經歷一次GC掃描,對象的年齡都會加一
3)如果這個對象在生活區中經歷了很多次GC仍然存活,JVM就會認為這個對象的生命周期會很長,然后通過復制算法從生活區拷貝到老年代。
4)老年代中的對象也會被GC掃描。但是掃描的頻率會大大的降低。
5)老年代中的對象,如果被GC標記為垃圾,就會按照整理的方式釋放內存。