目錄
1. JVM 內存區域劃分
2. JVM 中類加載的過程
1) 類加載的基本流程
2) 雙親委派模型
3. JVM 中垃圾回收機制
1) 找到垃圾
a) 引用計數
b) 可達性分析
2) 釋放垃圾
1. JVM 內存區域劃分
一個運行起來的 Java 進程,其實就是一個 JVM 虛擬機。
而進程是資源分配的基本單位,所以 JVM 就首先會申請一大塊內存,然后把這個內存劃分成不同的區域,每個區域都有不同的作用。
JVM 內存區域劃分成以下四個部分:
1. 方法區 (1.7 及之前) / 元數據區 (1.8 開始)
方法區存儲的內容,就是類對象。( .class 文件加載到內存之后,就成了類對象)
2. 堆
這里存儲的內容,就是代碼中 new 的對象。
堆是占據空間最大的區域。
3. 虛擬機棧(就是平常我們所說的棧)
這里存儲的內容,就是代碼執行過程中,方法之間的調用關系。
4. 程序計數器
是個比較小的空間,主要用來存放一個 "地址",這個地址,就表示了下一條要執行的指令,在內存中的哪個地方(方法區里)。
每個方法,里面的指令,都是以二進制的形式, 保存到類對象中的。
?剛開始調用方法的時候,程序計數器記錄的就是方法的入口的地址。
隨著一條一條的執行指令,每執行一條指令,程序計數器的值都會自動更新,去指向下一條指令。
程序計數器和虛擬機棧是每個線程都有一份,而堆和方法區在 JVM 進程中是只有一份的。
在 Java 里,每個線程都有自己私有的棧空間。
2. JVM 中類加載的過程
1) 類加載的基本流程
java 代碼會被編譯成 .class 文件(包含了一些字節碼),java 程序想要運行起來,就需要讓 JVM 讀取到這些 .class 文件,并把里面的內容,構造成類對象,保存到內存的方法區中。?
官方文檔把類加載的過程主要分成了 5 個步驟。
1. 加載:找到 .class 文件,打開文件,讀取文件內容。
往往代碼中,會給定某個類的 "全限定類名"(比如 java.lang.String,java.util.ArrayList) ,JVM 就會根據這個類名,在一些指定的目錄范圍內查找。
2. 驗證: .class 文件是一個二進制的格式。(某個字節,都是有某些特定含義的),就需要驗證你當前讀到的這個格式是否符合要求。
3. 準備:給類對象分配內存空間(最終的目標,是要構造出類對象)
這里只是分配空間,還沒有初始化,此時這個空間上的內存的數值,就是全 0 的,此時如果嘗試打印類的 static 成員,就是全 0 的。
4. 解析:針對類對象中包含的字符串常量進行處理,進行一些初始化操作。
java 代碼中用到的字符串常量,在編譯之后,也會進入到 .class 文件中。
5. 初始化:針對類對象進行初始化。
把類對象中需要的各個屬性都設置好。
還需要初始化號 static 成員
還需要執行靜態代碼塊
以及可能還需要加載一下父類。
總結類加載的基本流程:
1. 加載:找到 .class 文件,打開 .class 文件,讀取 .class 文件
2. 驗證:驗證當前 .class 文件格式是否正確
3. 準備:給類對象分配內存空間
4. 解析:將符號引用替換成直接引用
5. 初始化:初始化類對象
2) 雙親委派模型
屬于類加載中第一個步驟 "加載" 中的一個環節,是負責根據全限定類名,來找到 .class 文件的。
類加載器,是 JVM 中的一個模塊(專門負責類加載的操作)。
JVM 中,內置了三個類加載器:
1. BootStrap ClassLoader? ? ? ? ? ?爺
2. Extension ClassLoader? ? ? ? ? ?父
3. Application ClassLoader? ? ? ? ?子
這個父子關系,不是繼承構成的,而是這幾個 ClassLoader 里有一個 parent 這樣的屬性,指向了一個 父 "類加載器"。
程序員也可以手動創建出新的類加載器。
所以說,雙親委派模型,就是一個查找優先級的問題,先找標準庫,再找擴展庫,最后找第三方庫。
3. JVM 中垃圾回收機制
在 Java 中,new 一個對象,就是 "動態內存申請",在 C 語言中,使用 malloc 這種 "動態內存申請" 的函數,使用完之后,就需要手動調用 free 釋放內存,如果不釋放,就會出現內存泄露這樣的問題,而在 Java 中就不用手動釋放內存,因為 JVM 自動判定,是否某個對象已經不再使用了,并幫我們進行釋放不再使用的對象的內存了。像這種不再使用的對象,就稱之為 "垃圾",這種機制,也就叫做 GC 垃圾回收機制。
GC 也有缺陷:
1. 系統開銷,需要有一個/一些特定的線程,不停的掃描你內存中的所有的對象,看是否能夠回收,此時是需要額外的內存和 CPU 資源的。
2. 效率問題,這樣的掃描線程,不一定能夠及時的釋放內存 (掃描總是有一定周期的),一旦同一時刻,出現大量的對象都需要被回收,GC 產生的負擔就會很大,甚至引起整個程序都卡頓 (STW 問題? ? stop? ?the? ?world)
但是 GC 屬于大勢所趨,Python,PHP,Go.... 都是具有 GC 機制的。
GC 是垃圾回收,GC 回收的目標,其實是 內存中的 對象。
對于 Java 來說,就是 new 出來的這些對象。
棧里的局部變量,是跟隨著棧幀的生命周期走的。(方法執行結束,棧幀銷毀,內存自然釋放)
靜態變量,生命周期就是整個程序,這就意味著 靜態變量 是無需釋放的。
因此真正需要 gc 釋放的對象就是 堆 上的對象。
gc 可以理解成兩個大的步驟:
1. 找到垃圾
2. 釋放垃圾
1) 找到垃圾
在 GC 的圈子中,有兩種主流的方案:1. 引用計數? ? ?2. 可達性分析 (Java?采用的是這種)
a) 引用計數
new 出來的對象,單獨安排一塊空間,來保存一個計數器。
b) 可達性分析
可達性分析,本質上是一個時間換空間這樣的手段。
有一個/一組線程,周期性的掃描代碼中的所有對象。
從一些特定的對象出發,盡可能的進行訪問的遍歷,把所有能夠訪問到的對象,都標記成 "可達",反之,經過掃描之后,未被標記成 "可達" 的對象,就是垃圾了。
就跟二叉樹的遍歷差不多,只不過不是二叉樹,而是 N 叉樹。
2) 釋放垃圾
有三種基本的思路:
1. 標記清除
是一種比較簡單粗暴的方式。
2. 復制算法
第二種思路,就是解決,剛剛標記清除出現的內存碎片的辦法。
通過復制的方式,把有效的對象,歸類到一起,再統一釋放剩下的空間。
3. 標記整理
既能夠解決內存碎片的問題,又能夠處理復制算法中利用率。
類似于順序表刪除元素的搬運操作。
實際上,JVM 采取的釋放思路,是上述基礎思路的結合體。
分代回收:
分代回收,對象能活過的 GC 掃描輪次越多,就是越老。