聊到JVM(還怕面試官問JVM嗎?)

點擊上方"程序員歷小冰",選擇「置頂或者星標」css

   你的關注意義重大!
java


前言 
不管什麼級別的Java從業者, JVM都是進階時必須邁過的坎。無論是工做仍是面試中,JVM都是必考題。 若是不懂JVM的話,薪酬會很是吃虧(近70%的面試者掛在JVM上了)。

  • 請你談談你對JVM的理解?c++

  • JVM類加載器是怎麼樣的?有幾種?程序員

  • 什麼是OOM,什麼是StackOverFlowError? 怎麼分析?web

  • JVM經常使用調優參數有哪寫?面試

  • GC有幾種算法?分別是怎麼執行的?算法

  • 你知道JProfiler嗎,怎麼分析Dump文件?編程


第一次看到這些真真實實的面試題的時候,我~



這都什麼玩意???????
通過一段時間的研究!!接下來,我將以大白話從頭至尾給你們講講 Java虛擬機 !!
不對的地方還請你們指正~

一、什麼是JVM?在哪? 


JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一種用於計算設備的規範,它是一個虛構出來的計算機,是經過在實際的計算機上仿真模擬各類計算機功能來實現的。

百度的解釋雲裏霧裏,對於咱們Java程序員,說白了就是:
  • JVM本質上是一個程序,它能識別.class 字節碼文件(裏面存放的是咱們對.java編譯後產生的二進制代碼),而且可以解析它的指令,最終調用操做系統上的函數,完成咱們想要的操做!安全


  • 關於Java語言的跨平臺性,就是由於JVM,咱們能夠將其想象爲一個抽象層,只要這個抽象層JVM正確執行了.class文件,就能運行在各類操做系統之上了!這就是一次編譯,屢次運行微信


對於JVM的位置
  • JVM是運行在操做系統之上的,它與硬件沒有直接的交互






二、JVM、JRE、JDK 的關係 


JDK(Java Development Kit):Java開發工具包

JRE(Java Runtime Environment):Java運行環境

JDK = JRE + javac/java/jar 等指令工具

JRE = JVM + Java基本類庫






三、JVM體系結構 

Java虛擬機主要分爲五大模塊:

    • 類裝載器子系統

    • 運行時數據區

    • 執行引擎

    • 本地方法接口

    • 垃圾收集模塊

    

    


  • 方法區是一種特殊的堆

  • 棧裏面不會有垃圾,用完就彈出了,不然阻塞了main方法

  • 垃圾幾乎都在堆裏,因此JVM性能調優%99都針對於堆



四、三種JVM(瞭解) 


Sun公司  HotSpot (咱們都用的這個)


BEA公司  JRockit
IBM公司  J9 VM

五、類加載器  


做用:加載 .Class 字節碼文件

一、回顧new對象的過程

public class Student {
    //私有屬性
    private String name;

    //構造方法
    public Student(String name) {
        this.name = name;
    }
}

類是模板、模板是抽象的;對象是具體的,是對抽象的實例化

//運行時,JVM將Test的信息放入方法區
public class Test{
    //main方法自己放入方法區
  public static void main(String[] args){
        //s一、s二、s3爲不一樣對象
        Student s1 = new Student("zsr");  //引用放在棧裏,具體的實例放在堆裏
        Student s2 = new Student("gcc");
        Student s3 = new Student("BareTH");
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
        System.out.println(s3.hashCode());
        //class一、class二、class3爲同一個對象
        Class<? extends Student> class1 = s1.getClass();
        Class<? extends Student> class2 = s2.getClass();
        Class<? extends Student> class3 = s3.getClass();
        System.out.println(class1.hashCode());
        System.out.println(class2.hashCode());
        System.out.println(class3.hashCode());
    }
}
根據結果,咱們發現:

    • s一、s二、s3的hashcode是不一樣的,由於是三個不一樣的對象,對象是具體的



    • class一、class二、class3的hashcode是相同的,由於這是類模板,模板是抽象的

    

咱們畫圖分析如下 new一個對象的流程
  1. 首先Class Loader讀取字節碼.class文件,加載初始化生成Student模板類


  2. 經過Student模板類new出三個對象



那麼Class Loader具體是怎麼執行咱們的.class字節碼文件呢,這就引出了咱們類加載器~


二、類加載器的類別

咱們編寫這樣一個程序



根據返回結果,咱們來說解如下三種加載器:

級別從高到底

1.啓動類(根)加載器:BootstrapClassLoader
    1. c++編寫,加載java核心庫 java.*,構造拓展類加載器應用程序加載器

    2. 根加載器加載拓展類加載器,而且將拓展類加載器的父加載器設置爲根加載器

    3. 而後再加載應用程序加載器,應將應用程序加載器的父加載器設置爲拓展類加載器

    4. 因爲引導類加載器涉及到虛擬機本地實現細節,咱們沒法直接獲取到啓動類加載器的引用;這就是上面那個程序咱們第三個結果爲null的緣由。

    5. 加載文件存在位置



    
2. 拓展類加載器:PlatformClassLoader
    1. java編寫,加載擴展庫,開發者能夠直接使用標準擴展類加載器。

    2. java9以前爲ExtClassloader,Java9之後更名爲PlatformClassLoader

    3.     加載文件存在位置




3.應用程序加載器:AppClassLoader
            1 . java 編寫,加載程序所在的目錄
    

        2. 是Java默認的類加載器


4.用戶自定義類加載器:CustomClassLoader
            1 . java 編寫,用戶自定義的類加載器,可加載指定路徑的 class 文件


六、雙親委派機制 

一、什麼是雙親委派機制

    • 類加載器收到類加載的請求


    • 將這個請求向上委託給父類加載器去完成,一直向上委託,直到根加載器BootstrapClassLoader


    • 根加載器檢查是否可以加載當前類,能加載就結束,使用當前的加載器;不然就拋出異常,通知子加載器進行加載;自加載器重複該步驟。

    

二、做用

舉個例子:咱們重寫如下java.lang包下的String類



發現報錯了,這就是 雙親委派機制 起的做用,當類加載器委託到 根加載器 的時候, String類 已經被 根加載器 加載過一遍了,因此不會再加載,從必定程度上防止了危險代碼的植入!!


做用總結
    1.  防止重複加載同一個 .class 。經過不斷委託父加載器直到根加載器,若是父加載器加載過了,就不用再加載一遍。保證數據安全。


    2. 保證系統核心.class,如上述的String類不能被篡改。經過委託方式,不會去篡改核心.class,即便篡改也不會去加載,即便加載也不會是同一個.class對象了。不一樣的加載器加載同一個.class也不是同一個class對象。這樣保證了class執行安全。


    

七、沙箱安全機制 

這裏引用了這篇博文引用連接,瞭解即

什麼是沙箱?

Java安全模型的核心就是Java沙箱(sandbox)


    1. 沙箱是一個限制程序運行的環境。沙箱機制就是將 Java 代碼限定在虛擬機(JVM)特定的運行範圍中,而且嚴格限制代碼對本地系統資源訪問,經過這樣的措施來保證對代碼的有效隔離,防止對本地系統形成破壞。


    沙箱主要限制系統資源訪問,系統資源包括CPU、內存、文件系統、網絡。不一樣級別的沙箱對這些資源訪問的限制也能夠不同。


全部的Java程序運行均可以指定沙箱,能夠定製安全策略。

java中的安全模型演進

在Java中將執行程序分紅 本地代碼 遠程代碼 兩種
  • 本地代碼可信任,能夠訪問一切本地資源。

  • 遠程代碼不可信信在早期的Java實現中,安全依賴於沙箱 (Sandbox) 機制。

以下圖所示



如此嚴格的安全機制也給程序的功能擴展帶來障礙,好比當用戶但願遠程代碼訪問本地系統的文件時候,就沒法實現。
所以在後續的  Java1.1  版本中,針對安全機制作了改進,增長了 安全策略 ,容許用戶指定代碼對本地資源的訪問權限。
以下圖所示
Java1.2 版本中,再次改進了安全機制,增長了 代碼簽名
  • 不論本地代碼或是遠程代碼,都會按照用戶的安全策略設定,由類加載器加載到虛擬機中權限不一樣的運行空間,來實現差別化的代碼執行權限控制。

以下圖所示


當前最新的安全機制實現,則引入了 域 (Domain)  的概念。
  • 虛擬機會把全部代碼加載到不一樣的系統域應用域

  • 系統域部分專門負責與關鍵資源進行交互

  • 應用域部分則經過系統域的部分代理來對各類須要的資源進行訪問。

  • 虛擬機中不一樣的受保護域 (Protected Domain),對應不同的權限 (Permission)。存在於不一樣域中的類文件就具備了當前域的所有權限,以下圖所示

組成沙箱的基本組件

1. 字節碼校驗器(bytecode verifier)

確保Java類文件遵循Java語言規範。這樣能夠幫助Java程序實現內存保護。但並非全部的類文件都會通過字節碼校驗,好比核心類(如上述java.lang.String)。

2. 類裝載器(class loader)

其中類裝載器在3個方面對Java沙箱起做用
  • 它防止惡意代碼去幹涉善意的代碼;

  • 它守護了被信任的類庫邊界;

  • 它將代碼納入保護域,肯定了代碼能夠進行哪些操做。

虛擬機爲不一樣的類加載器載入的類提供不一樣的命名空間,命名空間由一系列惟一的名稱組成,每個被裝載的類將有一個名字,這個命名空間是由Java虛擬機爲每個類裝載器維護的,它們互相之間甚至不可見。
類裝載器採用的機制是雙親委派模式。


  1. 從最內層JVM自帶類加載器開始加載,外層惡意同名類得不到加載從而沒法使用;


  2. 因爲嚴格經過包來區分了訪問域,外層惡意的類經過內置代碼也沒法得到權限訪問到內層類,破壞代碼就天然沒法生效。


  • 存取控制器(access controller):存取控制器能夠控制核心API對操做系統的存取權限,而這個控制的策略設定,能夠由用戶指定。

  • 安全管理器(security manager):是核心API和操做系統之間的主要接口。實現權限控制,比存取控制器優先級高。

  • 安全軟件包(security package):java.security下的類和擴展包下的類,容許用戶爲本身的應用增長新的安全特性,包括:


    1. 安全提供者

    2. 消息摘要

    3. 數字簽名

    4. 加密

    5. 鑑別


八、Native本地方法接口 

JNI:Java Native Interface

本地接口的做用是融合不一樣的編程語言爲Java所用,它的初衷是融合C/C++程序


native :凡是帶native關鍵字的,說明java的做用範圍達不到了,會去調用底層c語言的庫!進入本地方法棧,調用 本地方法接口JNI ,拓展Java的使用,融合不一樣的語言爲Java所用
    • Java誕生的時候C、C++橫行,爲了立足,必需要能調用C、C++的程序


    • 因而在內存區域中專門開闢了一塊標記區域:Native Method Stack,登記Native方法


    • 最終在執行引擎執行的的時候經過JNI(本地方法接口)加載本地方法庫的方法


目前該方法使用的愈來愈少了,除非是與硬件有關的應用,好比經過Java程序驅動打印機或者Java系統管理生產設備,在企業級應用中已經比較少見。由於如今的異構領域間通訊很發達,好比可使用 Socket通訊,也可使用 Web service等等,瞭解便可!


九、PC寄存器  

程序計數器 :Program Counter Register
    • 每一個線程都有一個程序計數器,是線程私有的,就是一個指針,指向方法區中的方法字節碼(用來存儲指向像一條指令的地址,也即將要執行的指令代碼),在執行引擎讀取下一條指令,是一個很是小的內存空間,幾乎能夠忽略不計




十、方法區 

方法區 :Method Area
  • 方法區是被全部線程共享,全部字段和方法字節碼,以及一些特殊方法,如構造函數,接口代碼也在此定義,簡單說,全部定義的方法的信息都保存在該區域,此區域屬於共享區間


  • 方法區與Java堆同樣,是各個線程共享的內存區域,用於存儲已被虛擬機加載的類信息常量靜態變量即時編譯器編譯後的代碼等數據。雖然Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作Non-Heap(非堆),目的應該是與Java 堆區分開來。



1. 方法區中有啥?

  1. 靜態變量(static)

  2. 常量(final)

  3. 類信息(構造方法、接口定義)

  4. 運行時的常量池


2. 建立對象內存分析



  • 建立一個對象時,方法區中會生成對應類的抽象模板;還有對應的常量池、靜態變量、類信息、常量


  • 咱們經過類模板去new對象的時候


    • 堆中存放實例對象

    • 棧中存放對象的引用,每一個對象對應一個地址指向堆中相同地址的實例對象


例如這個例子中,生成了對應的Person模板類,name常量「zsr」放在常量池中,三個對象的引用放在棧中,該引用指向放在堆中的三個實例對象。
這就是堆、棧、方法區的交互關係

十一、棧  

又稱 棧內存 ,主管程序的運行,生命週期和線程同步,線程結束,棧內存就釋放了, 不存在垃圾回收
  • 棧:先進後出

  • 隊列:先進先出(FIFO)

一、棧中存放啥?

  1. 8大基本類型

  2. 對象引用

  3. 實例的方法


二、棧運行原理

  • 棧表示Java方法執行的內存模型


  • 每調用一個方法就會爲每一個方法生成一個棧幀(Stack Frame),每一個方法被調用和完成的過程,都對應一個棧幀從虛擬機棧上入棧和出棧的過程。


  • 程序正在執行的方法必定在棧的頂部





三、堆棧溢出StackOverflowError

舉個例子

public class Test {
    public static void main(String[] args) {
        new Test().a();
    }

    public void a() {
        b();
    }

    public void b() {
        a();
    }
}


最開始,main()方法壓入棧中,而後執行a(),a()壓入棧中;再調用b(),b()壓入棧中;以此往復,a與b方法不斷被壓入棧中,最終致使棧溢出




十二、堆  

Heap,一個JVM只有一個堆內存(棧是線程級的),堆內存的大小是能夠調節的



一、堆中有啥?

實例化的對象

二、堆內存詳解


一、Young 年輕代

對象誕生、成長甚至死亡的區

  • Eden Space(伊甸園區):全部的對象都是在此new出來的

  • Survivor Space(倖存區)

    • 倖存0區From Space)(動態的,From和To會互相交換)

    • 倖存1區To Space


Eden區佔大容量,Survivor兩個區佔小容量,默認比例是8:1:1


二、Tenured 老年代

三、Perm 元空間

存儲的是Java運行時的一些環境或類信息,這個區域不存在垃圾回收!關閉虛擬機就會釋放這個區域內存!

這個區域常駐內存,用來存放JDK自身攜帶的Class對象、Interface元數據。

名稱演變
  • jdk1.6以前:永久代

  • jdk1.7:永久代慢慢退化,去永久代

  • jdk1.8以後:永久代更名爲元空間

注意:元空間在邏輯上存在,在物理上不存在
新生代 + 老年代的內存空間 = JVM分配的總內存
如圖所示:


三、什麼是OOM?

內存溢出java.lang.OutOfMemoryError

產生緣由
  1. 分配的太少

  2. 用的太多

  3. 用完沒釋放

四、GC垃圾回收

GC垃圾回收,主要在年輕代和老年代

首先,對象出生再 伊甸園區
  • 假設伊甸園區只能存必定數量的對象,則每當存滿時就會觸發一次輕GC(Minor GC)


  • 輕GC清理後,有的對象可能還存在引用,就活下來了,活下來的對象就進入倖存區;有的對象沒用了,就被GC清理掉了;每次輕GC都會使得伊甸園區爲空



  • 若是倖存區伊甸園都滿了,則會進入老年代,若是老年代滿了,就會觸發一次重GC(FullGC)年輕代+老年代的對象都會清理一次,活下的對象就進入老年代


  • 若是新生代老年代都滿了,則OOM


Minor GC:伊甸園區滿時觸發;從年輕代回收內存
Full GC:老年代滿時觸發;清理整個堆空間,包含年輕代和老年代
Major GC:清理老年代

什麼狀況永久區會崩?
一個啓動類加載了大量的第三方Jar包,Tomcat部署了過多應用,或者大量動態生成的反射類
這些東西不斷的被加載,直到內存滿,就會出現 OOM

1三、堆內存調優 

一、查看並設置JVM堆內存

查看咱們jvm的 堆內存

public class Test {
    public static void main(String[] args) {
        //返回jvm試圖使用的最大內存
        long max = Runtime.getRuntime().maxMemory();
        //返回jvm的初始化內存
        long total = Runtime.getRuntime().totalMemory();
        //默認狀況下:分配的總內存爲電腦內存的1/4,初始化內存爲電腦內存的1/64
        System.out.println("max=" + max / (double) 1024 / 1024 / 1024 + "G");
        System.out.println("total=" + total / (double) 1024 / 1024 / 1024 + "G");
    }
}



默認狀況下
  • JVM最大分配內存爲電腦內存的1/4

  • JVM初始化內存爲電腦內存的1/64

咱們能夠手動調堆內存大小


VM options 中能夠指定 jvm試圖使用的最大內存 jvm初始化內存 大小
  
  
   
   
            
   
   

  
  
   
   
            
   
   
-Xms1024m -Xmx1024m -Xlog:gc*


  • -Xmx用來設置jvm試圖使用的最大內存,默認爲1/4

  • -Xms用來設置jvm初始化內存,默認爲1/64

  • -Xlog:gc*用來打印GC垃圾回收信息




二、怎麼排除OOM錯誤?

1. 嘗試擴大堆內存看結果

利用上述方法指定jvm試圖使用的最大內存jvm初始化內存大小

2. 利用內存快照工具JProfiler

內存快照工具
  • MAT(Eclipse)

  • JProfiler

做用:
  • 分析Dump內存文件,快速定位內存泄漏

  • 得到堆中的文件

  • 得到大的對象

3. 什麼是Dump文件?如何分析?

Dump文件是進程內存鏡像,能夠把程序的執行狀態經過調試器保存到dump文件中

import java.util.ArrayList;

public class Test {
    byte[] array = new byte[1024 * 1024];//1M

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

運行該程序,報錯OOM


接下來咱們設置如下堆內存,並附加生成對應的dump文件的指令


-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

-XX:+HeapDumpOnOutOfMemoryError 表示當JVM發生OOM時,自動生成DUMP文件。

再次點擊運行,下載了對應的Dump文件



咱們右鍵該類,點擊 Show in Explorer


一直點擊上級目錄,直到找到.hprof文件,與src同級目錄下



咱們雙擊打開,能夠看到每塊所佔的大小,便於分析問題



點擊Thread Dump,裏面是全部的線程,點擊對應的線程能夠看到相應的錯誤,反饋到具體的行,便於排錯



每次打開Dump文件查看完後,建議刪除,能夠在idea中看到,打開文件後生成了不少內容,佔內存,建議刪除



附:安裝Jprofiler教程

1.idea中安裝插件

2.下載客戶端 https://www.ej-technologies.com/download/jprofiler/files



3.安裝客戶端
選擇自定義安裝,注意:路徑不能有中文和空格


這裏name和Company任意,License Key你們能夠尋找對應版本的註冊機得到



後續默認,安裝成功便可!!!
4. 安裝完成後,重啓IDEA,能夠看到咱們的內存快照工具

 打開IDEA的設置,找到Tools裏面的JProfiler,沒有設置位置則設置位置

此時則所有安裝完成!


1四、GC垃圾回收 

一、回顧

Garbage Collection:垃圾回收



在12.4中,咱們已經對GC的流程進行了大概的講解,這裏作一些總結
  • JVM在進行GC時,並非對年輕代老年代統一回收;大部分時候,回收都是在年輕代

  • GC分爲兩種

    • 輕GC(清理年輕代)

    • 重GC(清理年輕代+老年代)

二、GC算法

一、引用計數算法(不多使用)

  • 每一個對象在建立的時候,就給這個對象綁定一個計數器。

  • 每當有一個引用指向該對象時,計數器加一;每當有一個指向它的引用被刪除時,計數器減一。

  • 這樣,當沒有引用指向該對象時,該對象死亡,計數器爲0,這時就應該對這個對象進行垃圾回收操做。



二、複製算法

複製算法主要發生在 年輕代 (  倖存0區  和  倖存1區
  • 當Eden區滿的時候,會觸發輕GC,每觸發一次,活的對象就被轉移到倖存區,死的就被GC清理掉了,因此每觸發輕GC時,Eden區就會清空;

  • 對象被轉移到了倖存區,倖存區又分爲From SpaceTo Space,這兩塊區域是動態交換的,誰是空的誰就是To Space,而後From Space就會把所有對象轉移到To Space去;

  • 那若是兩塊區域都不爲空呢?這就用到了複製算法,其中一個區域會將存活的對象轉移到令一個區域去,而後將本身區域的內存空間清空,這樣該區域爲空,又成爲了To Space

  • 因此每次觸發輕GC後,Eden區清空,同時To區也清空了,全部的對象都在From區

這也就是倖存0區倖存1區總有一塊爲空的緣由



好處:沒有內存的碎片(內存集中在一塊)
壞處
  1. 浪費了內存空間(浪費了倖存區一半空間)

  2. 對象存活率較高的場景下(好比老年代那樣的環境),須要複製的東西太多,效率會降低。

最佳使用環境:對象存活度較低的時候,也就是 年輕代

三、標記–清除算法

爲每一個對象存儲一個標記位,記錄對象的生存狀態
  1. 標記階段:這個階段內,爲每一個對象更新標記位,檢查對象是否死亡;

  2. 清除階段:該階段對死亡的對象進行清除,執行 GC 操做。



缺點:兩次掃描嚴重浪費時間,會產生內存碎片
優勢:不須要額外的空間

四、標記–整理算法

標記-整理法  是  標記-清除法  的一個改進版。
又叫作  標記-清楚-壓縮法
  1. 標記階段,該算法也將全部對象標記爲存活和死亡兩種狀態;

  2. 不一樣的是,在第二個階段,該算法並無直接對死亡的對象進行清理,而是將全部存活的對象整理一下,放到另外一處空間,而後把剩下的全部對象所有清除。


能夠進一步優化,在內存碎片不太多的狀況下,就繼續標記清除,到達必定量的時候再壓縮.


總結

內存(時間複雜度)效率:複製算法 > 標記清除算法 > 標記壓縮算法
內存整齊度:複製算法 = 標記壓縮法 > 標記清除法
內存利用率:標記壓縮法 = 標記清除法 > 複製算法

思考:有沒有最優的算法?

沒有最優的算法,只有最合適的算法
GC 也稱爲  分代收集算法
對於 年輕代
  • 對象存活率低

  • 用複製算法

對於 老年代
  • 區域大,對象存活率高

  • 標記清除+標記壓縮混合實現



結束!


    

    做者: Baret H

    原文連接:http://i8n.cn/iWLG4r






-關注我


本文分享自微信公衆號 - 程序員歷小冰(gh_a1d0b50d8f0a)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索