記憶體區域
Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區
域。這些區域都有各自的用途,以及建立和銷燬的時間,有的區域隨著虛擬機器程式的啟動而存在,有些區域則依賴使用者執行緒的啟動和結束而建立和銷燬。Java虛擬機器所管理的記憶體將會包括以下幾個執行時資料區域,如下圖
Program Count Register
程式計數器:是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器。每個執行緒都有自己的獨立的程式計數器。
如果執行緒正在執行的是Java方法,那麼這個計數器的值就是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Native方法,這個計數器值為空(undefined)。此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。
Stack
執行緒私有的,它的生命週期與執行緒相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法執行的同時都會建立一個棧幀用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。
區域性變數表存放了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用和returnAddress型別(指向了一條位元組碼指令的地址)。
其中64位長度的long和double型別的資料會佔用2個區域性變數空間(slot),其餘的資料型別佔1個。區域性變數表所需的記憶體空間在編譯期間分配完成,當進入一個方法時,這個方法需要在幀中分配多大的區域性變數空間是完全確定的,在方法執行期間不會改變區域性變數表的大小。如果執行緒請求棧的深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常;無法申請到記憶體丟擲OutOfMemoryError異常。
Native Stack
本地方法棧與虛擬機器棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機器棧為虛擬機器執行java方法,而本地棧則為虛擬機器使用到的Native方法服務。
Heap
Java堆是執行緒共享的,在虛擬機器啟動時建立。此區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。
Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱作“GC堆”。由於現在收集器基本都採用分代收集演算法,所以Java堆中還可以細分為:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。
在實現時,既可以實現成固定大小的,也可以是可擴充套件的,不過當前主流的虛擬機器都是按照可擴充套件來實現的(通過-Xmx和-Xms控制)。
Method Area
執行緒共享,用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。
這區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝!
本地方法區存在一塊特殊的記憶體區域,叫常量池(Constant Pool)。
常量池(Constant Pool)指的是在編譯期被確定,並被儲存在已編譯的.class檔案中的一些資料。它包括了關於類、方法、介面等中的常量,也包括字串常量。Java把記憶體分為堆記憶體跟棧記憶體,前者主要用來存放物件,後者用於存放基本型別變數以及物件的引用。
GC機制
GC回收的時候需要判斷三個條件:
- 哪些記憶體需要回收?
- 什麼時候回收?
- 如何回收?
Java記憶體的程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著入棧和出棧操作。每一個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的,因此這幾個區域的記憶體分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因為方法結束或者執行緒結束,記憶體自然就跟隨著回收了。
而Java堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,只有在程式處於執行期間時才能知道會建立哪些物件,這部分記憶體的分配和回收是動態的,垃圾收集器所關注的是這部分的記憶體。
物件引用
Java 中的垃圾回收一般是在 Java 堆中進行,因為堆中幾乎存放了 Java 中所有的物件例項。談到 Java 堆中的
垃圾回收,自然要談到引用。在 JDK1.2 之前,Java 中的引用定義很很純粹:如果 reference 型別的資料中存
儲的數值代表的是另外一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。但在 JDK1.2 之後,Java 對引用的
概念進行了擴充,將其分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,引用強度依次減弱。
- 強引用:如“Object obj = new Object()”,這類引用是 Java 程式中最普遍的。只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的物件。
- 軟引用:它用來描述一些可能還有用,但並非必須的物件。在系統記憶體不夠用時,這類引用關聯的物件將被
垃圾收集器回收。JDK1.2 之後提供了 SoftReference 類來實現軟引用。 - 弱引用:它也是用來描述非需物件的,但它的強度比軟引用更弱些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。在JDK1.2 之後,提供了 WeakReference 類來實現弱引用。
- 虛引用:最弱的一種引用關係,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項
GC判定演算法
引用計數演算法
給物件中新增一個引用計數器,每當有
一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的物件就是不可能再被使用的,引用計數演算法(Reference Counting)的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的演算法,但是,至少主流的Java虛擬機器裡面沒有選用引用計數演算法來管理記憶體,其中最主要的原因是它很難解決物件之間相互迴圈引用的問題。
可達性分析演算法
Java中使用可達性分析(Reachability Analysis)來判定物件是否存活的。
通過一系列的稱為“GCRoots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GCRoots沒有任何引用鏈相連時,則證明此物件是不可用的。
GC收集演算法
標記-清除演算法
演算法分為標記和清除兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件,它的標記過程就是使用可達性演算法進行標記的。
主要缺點有兩個:
- 效率問題,標記和清除兩個過程的效率都不高
- 空間問題,標記清除之後會產生大量不連續的記憶體碎片
複製演算法
複製演算法:將可用記憶體按照容量分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另一塊上面,然後把已使用過的記憶體空間一次清理掉。
記憶體分配時不用考慮記憶體碎片問題,只要一動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。代價是將記憶體縮小為原來的一半。
標記-整理演算法
標記整理演算法(Mark-Compact),標記過程仍然和“標記-清除”一樣,但後續不走不是直接對可回收物件進行清理,而是讓所有存活物件向一端移動,然後直接清理掉端邊界以外的記憶體。
分代演算法
根據物件存活週期的不同將記憶體分為幾塊。一般把Java堆分為新生代和老年代,根據各個年代的特點採用最合適的收集演算法。在新生代中,每次垃圾收集時有大批物件死去,只有少量存活,可以選用複製演算法。而老年代物件存活率高,使用標記清理或者標記整理演算法。
類載入機制
概述
虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的 Java型別,這就是虛擬機器的類載入機制。
類載入過程
類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期包括載入、驗證、準備、解析、初始化、使用、解除安裝。
其中類載入的過程包括了載入、驗證、準備、解析、初始化五個階段。在這五個階段中,載入、驗證、準備和初始化這四個階段發生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始,這是為了支援Java語言的執行時繫結(也成為動態繫結或晚期繫結)。另外注意這裡的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中呼叫或啟用另一個階段。
這裡簡要說明下 Java 中的繫結:繫結指的是把一個方法的呼叫與方法所在的類(方法主體)關聯起來,對 Java 來說,繫結分為靜態繫結和動態繫結:
• 靜態繫結:即前期繫結。在程式執行前方法已經被繫結,此時由編譯器或其它連線程式實現。針對 Java,簡單的可以理解為程式編譯期的繫結。Java當中的方法只有final,static,private 和構造方法是前期繫結的。
• 動態繫結:即晚期繫結,也叫執行時繫結。在執行時根據具體物件的型別進行繫結。在 Java 中,幾乎所有的方法都是後期繫結的。下面詳細講述類載入過程中每個階段所做的工作。
- 載入階段: 是“類載入”(Class Loading)過程的第一個階段,在此階段,虛擬機器需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進位制位元組流。
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
- 在Java堆中生成一個代表這個類的java.lang.Class物件,作為方法區這些資料的訪問入口。
驗證階段:是連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。
準備階段:是為類的靜態變數分配記憶體並將其初始化為預設值,這些記憶體都將在方法區中進行分配。準備階段不分配類中的例項變數的記憶體,例項變數將會在物件例項化時隨著物件一起分配在Java堆中。
解析階段:是虛擬機器將常量池內的符號引用替換為直接引用的過程。
類初始化:是類載入過程的最後一步,前面的類載入過程,除了在載入階段使用者應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,才真正開始執行類中定義的Java程式程式碼。
類載入器
- 啟動類載入器,負責將存放在JAVA_HOME\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中,並且是虛擬機器識別的(僅按照檔名識別,如rt.jar,名字不符合的類庫即時放在lib目錄中也不會被載入)類庫載入到虛擬機器記憶體中。啟動類載入器無法被java程式直接引用。
- 擴充套件類載入器:負責載入JAVA_HOME\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發者可以直接使用該類載入器。
- 應用程式類載入器:負責載入使用者路徑上所指定的類庫,開發者可以直接使用這個類載入器,也是預設的類載入器。
三種載入器的關係:啟動類載入器->擴充套件類載入器->應用程式類載入器->自定義類載入器。
雙親委派模型
從上面的類載入器可以看出,類載入器的載入順序是自下而上的。
我們把類載入器的這種載入順序雙親委派模型。其要求除啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。這裡類載入器之間的父子關係一般不以繼承關係實現,而是用組合的方式來複用父類的程式碼。
雙親委派模型的工作過程:如果一個類載入器接收到了類載入的請求,它首先把這個請求委託給他的父類載入器去完成,每個層次的類載入器都是如此,因此所有的載入請求都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它在搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。
好處:java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存放在rt.jar中,無論哪個類載入器要載入這個類,最終都會委派給啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。相反,如果使用者自己寫了一個名為java.lang.Object的類,並放在程式的Classpath中,那系統中將會出現多個不同的Object類,java型別體系中最基礎的行為也無法保證,應用程式也會變得一片混亂。
實現:在java.lang.ClassLoader的loadClass()方法中,先檢查是否已經被載入過,若沒有載入則呼叫父類載入器的loadClass()方法,若父載入器為空則預設使用啟動類載入器作為父載入器。如果父載入失敗,則丟擲ClassNotFoundException異常後,再呼叫自己的findClass()方法進行載入。
參考資料:
《深入理解Java虛擬機器》