­

JVM簡單入門

初識JVM

JVM的位置:jre中包含jvm。

雙親委派機制

雙親委派機制:是指當一個類載入器收到一個類載入請求時,該類載入器首先會把請求委派給父類載入器。每個類載入器都是如此,只有在父類載入器在自己的搜索範圍內找不到指定類時,子類載入器才會嘗試自己去載入。

在IDE中編寫的Java源程式碼會被編譯器編譯成.class文件,然後再由ClassLoader(類載入器)將這些class文件載入到JVM中執行。

JVM中提供了三層的ClassLoader:

  • Bootstrap classLoader: 主要負責載入核心的類庫(java.lang.*等),構造ExtClassLoader和APPClassLoader。
  • ExtClassLoader:主要負責載入jre/lib/ext目錄下的一些擴展的jar。
  • AppClassLoader:主要負責載入應用程式的主函數類。

沙箱安全機制

Java安全模型的核心就是Java沙箱(sandbox),什麼是沙箱?沙箱是一個限制程式運行的環境,沙箱機制就是將Java程式碼限定在虛擬機(JVM)特定的運行範圍中,並且嚴格限制程式碼對本地系統資源的訪問,通過這樣的措施來保證對程式碼的有效隔離,防止對本地系統造成破壞,沙箱主要限制系統資源訪問,那系統資源包括什麼?CPU,記憶體,文件系統,網路。不同級別的沙箱對這些資源的訪問許可權也可以不一樣。

所有的java程式運行都可以指定沙箱,可以訂製安全策略。

在Java中將執行程式分為本地程式碼和遠程程式碼兩種,本地程式碼默認視為可信任的,可以訪問一切本地資源;而遠程程式碼則被視為不受信任的,對於未授信的遠程程式碼在早期的Java實現中,安全依賴於沙箱機制。

如此嚴格的安全機制給程式的拓展帶來了障礙,當遠程程式碼需要訪問本地資源的時候就無法實現。因此在Java1.1中,針對安全機製做了改進,增加了安全策略,允許用戶指定程式碼對本地資源的訪問。在Java1.2版本中,再次改進了安全機制,不論本地程式碼或是遠程程式碼,都會按照用戶的安全策略設定,由類載入器載入到虛擬機中許可權不同的運行空間,來實現差異化的程式碼執行許可權控制。

當前最新的安全機制實現,則引入了域(Domain)的概念,虛擬機會把所有的程式碼載入到不同的系統域和應用域,系統域專門負責與關鍵資源進行交互,而各個應用域部分則通過系統域的部分代理來對各種需要的資源進行訪問。虛擬機中不同的受保護域,對應不一樣的許可權,存在於不同域的中的類文件,就具有了當前域的全部許可權。

沙箱的基本組件

  • 位元組碼校驗器(bytecode verifier):確保Java類文件遵循Java語言規範,這樣可以幫助Java程式實現記憶體保護,但並不是所有的類文件都會經過位元組碼校驗,比如核心庫。

  • 類裝載器(ClassLoader):類裝載器在三個方面對Java沙箱起作用。

    • 防止惡意程式碼去干涉善意程式碼。
    • 守護被信任的類庫邊界。
    • 將程式碼歸入保護域,確保程式碼可以進行哪些操作。

    虛擬機為不同的類載入器載入的類提供了不同的命名空間,命名空間由一系列唯一的名稱組成,每一個被裝載的類將有一個名字,這個命名空間是由Java虛擬機為每一個類裝載器維護,它們互相之間甚至不可見。

    類裝載器採用的機制是雙親委派機制。

    • 由最內層JVM自帶的類載入器開始載入,外層惡意同名類得不到載入從而無法使用。
    • 由於嚴格通過包來區分了訪問域,外層惡意的類通過內置程式碼也無法獲得許可權訪問的內層類,破壞程式碼就自然無法實現。
  • 存取控制器(access controller):存取控制器可以控制核心API對作業系統的存取許可權,而這個控制策略的設定,也可以由用戶指定。

  • 安全管理器(security manager):是核心API和作業系統之間的主要介面,實現許可權控制,比存取控制器優先順序高。

  • 安全軟體包(security package):java.cecurity下的類和拓展包下的類,允許用戶為自己的類增加新的安全特性,包括:

    • 安全提供者
    • 消息摘要
    • 數字簽名
    • 加密
    • 鑒別

Native

native:凡是帶native關鍵字的,說明Java的作用範圍達不到了。會去調用底層c語言的庫,進入本地方法棧,調用本地方法本地介面(JNI)。

JNI作用:拓展Java的使用,融合不同的程式語言為Java所用。

native method stack作用:登記native方法,在(Execution Engine)執行引擎執行的時候載入native libraies(本地庫)。

PC計數器

程式計數器:program counter register。

每個執行緒都有一個程式計數器,是執行緒私有的,就是一個指針,指向方法區中的方法位元組碼(用來存儲指向一條指令的地址),在執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不記。

方法區

img

方法區是被所有的執行緒共享,所有欄位和方法位元組碼,以及一些特殊方法,如構造函數,介面程式碼也在此定義,就是所有定義的方法的資訊都保存在該區域,此區域屬於共享空間

靜態變數,常量,類資訊(構造方法,介面),運行時的常量池存在方法區中,但是實例變數存在堆記憶體中,與方法區無關。

(jdk1.8已經將方法區去掉了,將方法區移動到直接記憶體)

JDK1.8為什麼要移除方法區

1)永久代來存儲類資訊、常量、靜態變數等數據不是個好主意, 很容易遇到記憶體溢出的問題.JDK8的實現中將類的元數據放入 native memory, 將字元串池和類的靜態變數放入java堆中. 可以使用MaxMetaspaceSize對元數據區大小進行調整;

2)對永久代進行調優是很困難的,同時將元空間與堆的垃圾回收進行了隔離,避免永久代引發的Full GC和OOM等問題;

棧:數據結構(先進後出),棧記憶體,主管程式的運行,生命周期和執行緒同步,執行緒結束,棧記憶體釋放。

對於棧來說,不存在垃圾回收問題。

棧:主要存儲八大基本數據類型和對象引用。

棧運行原理:棧幀用於存儲局部變數表,動態鏈接,方法出口等資訊,方法的執行就對應著棧幀在虛擬機棧中入棧和出棧的過程。

堆(Heap):一個JVM只有一個堆記憶體,堆記憶體的大小是可以調節的。

堆:此記憶體區域唯一的目的就是存放對象實例。所有的對象實例都在這裡分配記憶體

堆記憶體中分為三個區域

  • 新生區:
    • 伊甸園區:Java新對象的出生地(如果新創建的對象佔用記憶體很大,則直接分配到老年代),當Eden區記憶體不夠的時候就會觸發MinorGC,對新生代區進行一次垃圾回收。
    • 倖存區0區:保留了一次MinorGC過程中的倖存者。
    • 倖存區1區:上一次GC的倖存者,作為這一次GC的被掃描者。
  • 老年區:存放穩定的對象(年齡到達設定的值 ,一般為15)。
  • 永久區:常駐記憶體的,存放Java運行時的一些環境或類資訊(這個區不存在垃圾回收,關閉JVM就釋放這個區的記憶體)。
    • Java17之前:永久代,常量池在方法區;
    • jdk1.7:去永久代,常量池在堆中;
    • jdk1.8:無永久代,常量池在元空間(元空間邏輯上存在,物理上不存在);
package com.jvm;

public class Test02 {
    public static void main(String[] args) {
        //返回虛擬機試圖使用的最大記憶體
        long max = Runtime.getRuntime().maxMemory();//位元組
        //jvm的初始化總記憶體
        long total = Runtime.getRuntime().totalMemory();
        System.out.println("max="+max+"位元組\t"+(max/(double)1024/1024)+"M");
        System.out.println("total="+total+"位元組\t"+(total/(double)1024/1024+"M"));
    }
}

工具分析OOM

在一個項目中,出現了OOM,使用的記憶體分析工具(MAT,Jprofile)。

作用:分析Dump記憶體文件,快速定位記憶體泄漏;獲得堆中的數據,獲得大的對象等。

package com.jvm;

import java.util.ArrayList;
//dump文件
public class Test03 {
    byte[] array = new byte[1*1024*1024];

    public static void main(String[] args) {
        ArrayList<Object> list = new ArrayList<>();
        int count = 0;
        try{
            while (true){
                list.add(new Test03());
                count+=1;
            }
        }catch (Exception e){
            System.out.println("count:"+count);
            e.printStackTrace();
        }

    }
}

GC演算法

GC回收大部分都是在新生區。

什麼時候觸發GC

​ (1)程式調用System.gc時可以觸發

​ (2)系統自身來決定GC觸發的時機(根據Eden區和From Space區的記憶體大小來決定。當記憶體大小不足時,則會啟動GC執行緒並停止應用執行緒)

GC又分為 minor GC 和 Full GC (也稱為 Major GC )

Minor GC觸發條件:當Eden區滿時,觸發Minor GC。

Full GC觸發條件:

a.調用System.gc時,系統建議執行Full GC,但是不必然執行

b.老年代空間不足

c.方法區空間不足

d.通過Minor GC後進入老年代的平均大小大於老年代的可用記憶體

e.由Eden區、From Space區向To Space區複製時,對象大小大於To Space可用記憶體,則把該對象轉存到老年代,且老年代的可用記憶體小於該對象大小

GC常用的演算法:引用計數法,標記清除演算法,標記壓縮演算法,複製演算法分代收集演算法

目前主流的JVM(HotSpot)採用的是分代收集演算法。

引用計數法:當一個對象的引用為0時會清除該對象(使用較少)。

複製演算法

複製演算法:該演算法將新生區記憶體平均分成兩部分,每次只使用其中的一部分,當這部分記憶體快滿的時候,將其中的倖存者複製到另一個記憶體上,將之前的記憶體清空。

  • 優點:不存在記憶體碎片,只需移動棧頂指針,按順序分配記憶體即可。
  • 缺點:每次只使用一半的記憶體。
  • 每交換一次年齡加1,到達默認年齡15後,倖存者進入老年區(-XX: -XX:MaxTenuringThreshold=30設置進入老年代的年齡)。

標記清除演算法

標記清除法:用在老年代中,為對象存儲一個標記位,標記存活的對象,對死亡的對象執行清除操作。

  • 優點:不需要額外的空間,不需要移動對象。
  • 缺點:兩次掃描效率比較低(全棧遍歷),沒有移動對象會產生記憶體碎片。

標記壓縮演算法

標記壓縮演算法是標記清除演算法的一個改進版,再次掃描,將存活的對象向一端移動,

  • 優點:沒有記憶體碎片。
  • 缺點:存活的對象較多時,會進行多次的移動操作,效率低。

分代收集演算法

分代收集演算法是目前大部分JVM的垃圾收集器採用的演算法。它的核心思想是根據對象存活的生命周期將記憶體劃分為若干個不同的區域。一般情況下將堆區劃分為老年代(Tenured Generation)和新生代(Young Generation),在堆區之外還有一個代就是永久代(Permanet Generation)。老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點採取最適合的收集演算法。

GC演算法總結

記憶體效率:複製演算法 > 標記清除演算法 > 標記壓縮演算法(時間複雜度)

記憶體整齊度:複製演算法 = 標記壓縮演算法 > 標記清楚演算法(記憶體碎片)

記憶體利用率:標記壓縮演算法 = 標記清除演算法 > 複製演算法

所以JVM採用分代收集演算法!!

JMM

Java memory model:Java記憶體模型。

Tags: