1. 程式人生 > >JVM理解其實並不難!

JVM理解其實並不難!

在閱讀本文之前,先向大家強烈推薦一下週志明的《深入理解Java虛擬機器》這本書。

前些天面試了阿里的實習生,問到關於Dalvik虛擬機器能不能執行class檔案,我當時的回答是不能,但是它執行的是class轉換的dex檔案。當面試官繼續問,為什麼不能執行class檔案時,我卻只能回答Dalvik虛擬機器內部的優化原因,卻不能正確回答具體的原因。其實周志明的這本書就有回答:Dakvik並不是一個Java虛擬機器,它沒有遵循Java虛擬機器規範,不能執行Java的class檔案,使用的是暫存器架構而不是JVM中常見的棧架構,但是它與Java又有著千絲萬縷的關係,它執行的dex檔案可以通過class檔案轉化而來

其實在本科期間,就有接觸過《深入理解Java虛擬機器》,但是一直以來都沒去仔細研讀,現在回頭想想實在是覺得可惜!研一期間花了不少時間研讀,現在準備找工作了,發現好多內容看了又忘。索性寫一篇文章,把這本書的知識點做一個總結。當然了,如果你想看比較詳細的內容,可以翻看《深入理解Java虛擬機器》。

JVM記憶體區域

我們在編寫程式時,經常會遇到OOM(out of Memory)以及記憶體洩漏等問題。為了避免出現這些問題,我們首先必須對JVM的記憶體劃分有個具體的認識。JVM將記憶體主要劃分為:方法區、虛擬機器棧、本地方法棧、堆、程式計數器。JVM執行時資料區如下:
JVM執行時資料區

程式計數器

程式計數器是執行緒私有的區域,很好理解嘛~,每個執行緒當然得有個計數器記錄當前執行到那個指令。佔用的記憶體空間小,可以把它看成是當前執行緒所執行的位元組碼的行號指示器。如果執行緒在執行Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令地址;如果執行的是Native方法,這個計數器的值為空(Undefined)。此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域

Java虛擬機器棧

與程式計數器一樣,Java虛擬機器棧也是執行緒私有的。其生命週期與執行緒相同。如何理解虛擬機器棧呢?本質上來講,就是個棧。裡面存放的元素叫棧幀,棧幀好像很複雜的樣子,其實它很簡單!它裡面存放的是一個函式的上下文,具體存放的是執行的函式的一些資料。執行的函式需要的資料無非就是區域性變量表(儲存函式內部的變數)、運算元棧(執行引擎計算時需要),方法出口等等。

執行引擎每呼叫一個函式時,就為這個函式建立一個棧幀,並加入虛擬機器棧。換個角度理解,每個函式從呼叫到執行結束,其實是對應一個棧幀的入棧和出棧。

注意這個區域可能出現的兩種異常:一種是StackOverflowError,當前執行緒請求的棧深度大於虛擬機器所允許的深度時,會丟擲這個異常。製造這種異常很簡單:將一個函式反覆遞迴自己,最終會出現棧溢位錯誤(StackOverflowError)。另一種異常是OutOfMemoryError異常,當虛擬機器棧可以動態擴充套件時(當前大部分虛擬機器都可以),如果無法申請足夠多的記憶體就會丟擲OutOfMemoryError,如何製作虛擬機器棧OOM呢,參考一下程式碼:

public void stackLeakByThread(){
    while(true){
        new Thread(){
            public void run(){
                while(true){
                }
            }
        }.start()
    }
}

這段程式碼有風險,可能會導致作業系統假死,請謹慎使用~~~

本地方法棧

本地方法棧與虛擬機器棧所發揮的作用很相似,他們的區別在於虛擬機器棧為執行Java程式碼方法服務,而本地方法棧是為Native方法服務。與虛擬機器棧一樣,本地方法棧也會丟擲StackOverflowError和OutOfMemoryError異常。

Java堆

Java堆可以說是虛擬機器中最大一塊記憶體了。它是所有執行緒所共享的記憶體區域,幾乎所有的例項物件都是在這塊區域中存放。當然,睡著JIT編譯器的發展,所有物件在堆上分配漸漸變得不那麼“絕對”了。

Java堆是垃圾收集器管理的主要區域。由於現在的收集器基本上採用的都是分代收集演算法,所有Java堆可以細分為:新生代和老年代。在細緻分就是把新生代分為:Eden空間、From Survivor空間、To Survivor空間。當堆無法再擴充套件時,會丟擲OutOfMemoryError異常。

方法區

方法區存放的是類資訊、常量、靜態變數等。方法區是各個執行緒共享區域,很容易理解,我們在寫Java程式碼時,每個執行緒度可以訪問同一個類的靜態變數物件。由於使用反射機制的原因,虛擬機器很難推測那個類資訊不再使用,因此這塊區域的回收很難。另外,對這塊區域主要是針對常量池回收,值得注意的是JDK1.7已經把常量池轉移到堆裡面了。同樣,當方法區無法滿足記憶體分配需求時,會丟擲OutOfMemoryError。
製造方法區記憶體溢位,注意,必須在JDK1.6及之前版本才會導致方法區溢位,原因後面解釋,執行之前,可以把虛擬機器的引數-XXpermSize和-XX:MaxPermSize限制方法區大小。

List<String> list =new ArrayList<String>();
int i =0;
while(true){
    list.add(String.valueOf(i).intern());
} 

執行後會丟擲java.lang.OutOfMemoryError:PermGen space異常。
解釋一下,Stringintern()函式作用是如果當前的字串在常量池中不存在,則放入到常量池中。上面的程式碼不斷將字串新增到常量池,最終肯定會導致記憶體不足,丟擲方法區的OOM。

下面解釋一下,為什麼必須將上面的程式碼在JDK1.6之前執行。我們前面提到,JDK1.7後,把常量池放入到堆空間中,這導致intern()函式的功能不同,具體怎麼個不同法,且看看下面程式碼:

String str1 =new StringBuilder("hua").append("chao").toString();
System.out.println(str1.intern()==str1);

String str2=new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern()==str2);

這段程式碼在JDK1.6和JDK1.7執行的結果不同。JDK1.6結果是:false,false ,JDK1.7結果是true, false。原因是:JDK1.6中,intern()方法會吧首次遇到的字串例項複製到常量池中,返回的也是常量池中的字串的引用,而StringBuilder建立的字串例項是在堆上面,所以必然不是同一個引用,返回false。在JDK1.7中,intern不再複製例項,常量池中只儲存首次出現的例項的引用,因此intern()返回的引用和由StringBuilder建立的字串例項是同一個。為什麼對str2比較返回的是false呢?這是因為,JVM中內部在載入類的時候,就已經有"java"這個字串,不符合“首次出現”的原則,因此返回false

垃圾回收(GC)

JVM的垃圾回收機制中,判斷一個物件是否死亡,並不是根據是否還有物件對其有引用,而是通過可達性分析。物件之間的引用可以抽象成樹形結構,通過樹根(GC Roots)作為起點,從這些樹根往下搜尋,搜尋走過的鏈稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,則證明這個物件是不可用的,該物件會被判定為可回收的物件。

那麼那些物件可作為GC Roots呢?主要有以下幾種:

1.虛擬機器棧(棧幀中的本地變量表)中引用的物件。
2.方法區中類靜態屬性引用的物件。
3.方法區中常量引用的物件
4.本地方法棧中JNI(即一般說的Native方法)引用的物件。

另外,Java還提供了軟引用和弱引用,這兩個引用是可以隨時被虛擬機器回收的物件,我們將一些比較佔記憶體但是又可能後面用的物件,比如Bitmap物件,可以宣告為軟引用貨弱引用。但是注意一點,每次使用這個物件時候,需要顯示判斷一下是否為null,以免出錯。

三種常見的垃圾收集演算法

1.標記-清除演算法

首先,通過可達性分析將可回收的物件進行標記,標記後再統一回收所有被標記的物件,標記過程其實就是可達性分析的過程。這種方法有2個不足點:效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量的不連續的記憶體碎片。

2.複製演算法

為了解決效率問題,複製演算法是將記憶體分為大小相同的兩塊,每次只使用其中一塊。當這塊記憶體用完了,就將還存活的物件複製到另一塊記憶體上面。然後再把已經使用過的記憶體一次清理掉。這使得每次只對半個區域進行垃圾回收,記憶體分配時也不用考慮記憶體碎片情況。

但是,這代價實在是讓人無法接受,需要犧牲一般的記憶體空間。研究發現,大部分物件都是“朝生夕死”,所以不需要安裝1:1比例劃分記憶體空間,而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和一塊Survivor空間,預設比例為Eden:Survivor=8:1.新生代區域就是這麼劃分,每次例項在Eden和一塊Survivor中分配,回收時,將存活的物件複製到剩下的另一塊Survivor。這樣只有10%的記憶體會被浪費,但是帶來的效率卻很高。當剩下的Survivor記憶體不足時,可以去老年代記憶體進行分配擔保。如何理解分配擔保呢,其實就是,記憶體不足時,去老年代記憶體空間分配,然後等新生代記憶體緩過來了之後,把記憶體歸還給老年代,保持新生代中的Eden:Survivor=8:1.另外,兩個Survivor分別有自己的名稱:From Survivor、To Survivor。二者身份經常調換,即有時這塊記憶體與Eden一起參與分配,有時是另一塊。因為他們之間經常相互複製。

3.標記-整理演算法

標記整理演算法很簡單,就是先標記需要回收的物件,然後把所有存活的物件移動到記憶體的一端。這樣的好處是避免了記憶體碎片。

類載入機制

類從被載入到虛擬機器記憶體開始,到卸載出記憶體為止,整個生命週期包括:載入、驗證、準備、解析、初始化、使用和解除安裝七個階段。

其中載入、驗證、準備、初始化、和解除安裝這5個階段的順序是確定的。而解析階段不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支援Java的執行時繫結。

關於初始化:JVM規範明確規定,有且只有5中情況必須執行對類的初始化(載入、驗證、準備自然再此之前要發生):
1.遇到new、getstatic、putstatic、invokestatic,如果類沒有初始化,則必須初始化,這幾條指令分別是指:new新物件、讀取靜態變數、設定靜態變數,呼叫靜態函式。
2.使用java.lang.reflect包的方法對類進行反射呼叫時,如果類沒初始化,則需要初始化
3.當初始化一個類時,如果發現父類沒有初始化,則需要先觸發父類初始化。
4.當虛擬機器啟動時,使用者需要制定一個執行的主類(包含main函式的類),虛擬機器會先初始化這個類。
5.但是用JDK1.7啟的動態語言支援時,如果一個MethodHandle例項最後解析的結果是REF_getStaticREF_putStaticRef_invokeStatic的方法控制代碼時,並且這個方法控制代碼所對應的類沒有進行初始化,則要先觸發其初始化。

另外要注意的是:通過子類來引用父類的靜態欄位,不會導致子類初始化

public class SuperClass{
    public static int value=123;
    static{
        System.out.printLn("SuperClass init!");
    }
}

public class SubClass extends SuperClass{
    static{
        System.out.println("SubClass init!");
    }


}

public class Test{

    public static void main(String[] args){
        System.out.println(SubClass.value);
    }
}

最後只會列印:SuperClass init!
對應靜態變數,只有直接定義這個欄位的類才會被初始化,因此通過子類類引用父類中定義的靜態變數只會觸發父類初始化而不會觸發子類初始化。

通過陣列定義來引用類,不會觸發此類的初始化

public class Test{

    public static void main(String[] args){
        SuperClass[] sca=new SuperClass[10];
    }
}

常量會在編譯階段存入呼叫者的常量池,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類初始化,示例程式碼如下:

public class ConstClass{
    public static final String HELLO_WORLD="hello world";
    static {
        System.out.println("ConstClass init!");
    }

}

public class Test{
    public static void main(String[] args){

        System.out.print(ConstClass.HELLO_WORLD);
    }


}

上面程式碼不會出現ConstClass init!

載入

載入過程主要做以下3件事
1.通過一個類的全限定名稱來獲取此類的二進位制流
2.強這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
3.在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料訪問入口。

驗證

這個階段主要是為了確保Class檔案位元組流中包含資訊符合當前虛擬機器的要求,並且不會出現危害虛擬機器自身的安全。

準備

準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都在方法區中分配。首先,這個時候分配記憶體僅僅包括類變數(被static修飾的變數),而不包括例項變數。例項變數會在物件例項化時隨著物件一起分配在java堆中。其次這裡所說的初始值“通常情況下”是資料型別的零值,假設一個類變數定義為

public static int value=123;

那變數value在準備階段後的初始值是0,而不是123,因為還沒有執行任何Java方法,而把value賦值為123是在程式編譯後,存放在類建構函式<clinit>()方法中。

解析

解析階段是把虛擬機器中常量池的符號引用替換為直接引用的過程。

初始化

類初始化時類載入的最後一步,前面類載入過程中,除了載入階段使用者可以通過自定義類載入器參與以外,其餘動作都是虛擬機器主導和控制。到了初始化階段,才是真正執行類中定義Java程式程式碼。

準備階段中,變數已經賦過一次系統要求的初始值,而在初始化階段,根據程式設計師通過程式制定的主觀計劃初始化類變數。初始化過程其實是執行類構造器<clinit>()方法的過程。

<clinit>()方法是由編譯器自動收集類中所有類變數的賦值動作和靜態語句塊中的語句合併產生的。收集的順序是按照語句在原始檔中出現的順序。靜態語句塊中只能訪問定義在靜態語句塊之前的變數,定義在它之後的變數可以賦值,但不能訪問。如下所示:

public class Test{
    static{
        i=0;//給變數賦值,可以通過編譯
        System.out.print(i);//這句編譯器會提示:“非法向前引用”
    }
    static int i=1;

}

<clinit>()方法與類建構函式(或者說例項構造器<init>())不同,他不需要顯式地呼叫父類構造器,虛擬機器會保證子類的<clinit>()方法執行之前,父類的<clinit>()已經執行完畢。

類載入器

關於自定義類載入器,和雙親委派模型,這裡不再提,寫了幾個小時了,該洗洗睡了~

相關推薦

JVM理解其實不難

在閱讀本文之前,先向大家強烈推薦一下週志明的《深入理解Java虛擬機器》這本書。 前些天面試了阿里的實習生,問到關於Dalvik虛擬機器能不能執行class檔案,我當時的回答是不能,但是它執行的是class轉換的dex檔案。當面試官繼續問,為什麼不能執行c

SEM營銷推廣策略和優化,做好其實不難

當今社會,移動網際網路和各種超級app興起,導致營銷形式一直在變化,營銷手段多種多樣,但是不得不承認的是SEM仍然是很多中小企業最信賴的推廣方式。但是競價推廣的成本卻隨著其發展變得越來越高,而轉化成交也在逐漸減少。 為什麼推廣成本變高? 其實造成推廣成本過高的原因有很多,要根據賬戶的

shll基礎,原來腳本不難

下標 嵌套 h+ i++ case語句 === 目錄 ber i+1 ------一個小計算器----- #!/bin/bash#this read -p "請輸入第一個整數:" ONEread -p "請輸入第二個整數:" TWOr

shell基礎,原來腳本不難

嵌套 stdin his 性別 length dir ase 區分 字母 ------一個小計算器----- #!/bin/bash#this read -p "請輸入第一個整數:" ONEread -p "請輸入第二個整數:" TW

shll基礎,原來指令碼不難

------一個小計算器----- #!/bin/bash#this read -p "請輸入第一個整數:" ONEread -p "請輸入第二個整數:" TWOread -p "請輸入運算子:" THecho "$ONE$TH$TWO=$(($ONE $TH $TWO))" ---------一個IF

Java程式設計師進階架構師其實不難,關鍵在於選擇。

很多人做java開發2,3年後,都會感覺自己遇到瓶頸。什麼都會又什麼都不會,如何改變困境,為什麼很多人寫了7,8年還是一個碼農,工作中太多被動是因為不懂底層原理。公司的工作節奏又比較快,難有機會學習架構原理,也沒人教,所以這個時候,學習架構原理,擴充套件思維,對自己以後職業生涯尤為重要。 同樣公

laravel安裝:laravel其實不難之二

安裝 準備階段 laravel的安裝,其實稍微還是挺簡單的。有個大前提是,你要熟悉瞭解composer的作用。這裡,我就不對composer做很大的解釋了。 因為我係統是ubuntu,用的是docker 環境。所以就不過多來說明環境了。 使用ph

Dask快速搭建分散式叢集(大資料0基礎可以理解使用

轉載:  https://blog.csdn.net/a19990412/article/details/79510219常開心,解決了很久都沒有解決的問題使用的語言: Python3.5 分散式機器: windows7注意到,其實,通過這工具搭建分散式不需要管使用的電腦是什

看完後,搞懂ARP的工作原理,其實不難(好文)

原址本期,我就來解答一下關於ARP的工作原理吧! 思路1、思考ARP到底是幹嘛的? 為什麼要有它?why?  2、掌握ARP的工作原理。技術人,要掌握一種技術,絕對離不開以上兩點基本思路,帶著這兩個思路去學習新技術,你絕對是不可能學不會的。再笨,你也會學得很深刻的。甚至,你還

甲級PAT 1016 Phone Bills(這是到目前為止甲級寫的最長最複雜的程式碼,其實不難,邏輯理順了就好)

1016 Phone Bills(25 分) A long-distance telephone company charges its customers by the following rules: Making a long-distance call costs

基於JVM原理JMM模型和CPU緩存模型深入理解Java發編程

可靠的 解決 start 關鍵字 juc .com 失效 接下來 直接 許多以Java多線程開發為主題的技術書籍,都會把對Java虛擬機和Java內存模型的講解,作為講授Java並發編程開發的主要內容,有的還深入到計算機系統的內存、CPU、緩存等予以說明。實際上,在實際的J

雲計算運維工程師怎麽學?網管變工程師不難

雲計算如今的Linux非常火,很多人都想參加Linux培訓學習Linux開發技能高薪就業,但是對於Linux運維工程師和系統管理員有什麽區別?有多少人知道或者了解呢?都知道系統管理員(俗稱網管)和Linux運維工程師差距甚遠,但是究竟區別在哪兒呢?很多朋友就有些不清楚了。 關於運維工程師百度上的官方解釋是:

Java oop(一些自己的理解沒有展開很細)

繼承 new .get 獲得 數量 語言 讀寫權限 訪問 setname 一下內容是自己總結用的,只是按照自己的理解去寫。參考的是菜鳥教程。Java 是一個面向對象的語言。OOP就是面向對象編程。封裝:在某些類裏面,某些屬性不想向外暴露,但是我們又想提供一個方法去訪問或修改

《Linux學習不難》歸檔和壓縮(2):tar包的使用和管理

linux tar 壓縮 22.2 《Linux學習並不難》歸檔和壓縮(2):tar包的使用和管理使用tar命令可以將許多文件一起保存到一個單獨的磁帶或磁盤歸檔,並能從歸檔中單獨還原所需文件。命令語法:tar [選項] [文件|目錄]命令中各選項的含義如表所示。選項 選項含義 -c 創建新的

《Linux學習不難》用戶管理(1):Linux用戶賬戶分類

Linux 用戶 9.1 《Linux學習並不難》用戶管理(1):Linux用戶賬戶分類用戶賬戶在Linux系統中是分角色的,由於角色不同,每個用戶的權限和所能執行的工作任務也不同。在實際的管理中,用戶的角色是通過UID(用戶ID號)來標識的,每個用戶的UID都是不同的。 在Linux系統中有三大類

《Linux學習不難》用戶管理(2):/etc/passwd文件詳細介紹

Linux 用戶 passwd 9.2 《Linux學習並不難》用戶管理(2):/etc/passwd文件詳細介紹/etc/passwd文件是Linux系統識別用戶的一個重要文件,Linux系統中所有的用戶都記錄在該文件中。假設用戶以賬戶zhangsan登錄系統時,系統首先會檢查/etc/pas

《Linux學習不難》使用RPM軟件包(3):安裝RPM軟件包

Linux 軟件包 rpm 14.3 《Linux學習並不難》使用RPM軟件包(3):安裝RPM軟件包RPM軟件包的安裝流程如圖9.1所示,如果軟件包滿足依賴條件則允許安裝,如果不滿足依賴關系則需要先安裝其它軟件包。使用rpm命令可以在Linux系統中安裝、刪除、刷新、升級、查詢RPM軟件包。命

《Linux學習不難》用戶管理(3):/etc/shadow文件詳細介紹

Linux 用戶 shadow 9.3 《Linux學習並不難》用戶管理(3):/etc/shadow文件詳細介紹/etc/shadow文件是/etc/passwd的影子文件,這兩個文件應該是對應互補的。/etc/shadow文件的內容包括用戶被加密的密碼以及其它/etc/passwd文件不能包

《Linux學習不難》文件系統管理(3):在Linux系統中創建文件系統

Linux xfs 文件系統 18.3 《Linux學習並不難》文件系統管理(3):在Linux系統中創建文件系統使用mkfs命令可以在分區上創建各種文件系統。mkfs命令本身並不執行建立文件系統的工作,而是去調用相關的程序來執行。這裏的文件系統是要指定的,比如xfs、ext4、ext3、vfa

《Linux學習不難》文件系統管理(1):xfs文件系統介紹

Linux xfs 文件系統 18.1 《Linux學習並不難》文件系統管理(1):xfs文件系統介紹xfs是一種高性能的64位日誌文件系統,由Silicon Graphics在1993年為IRIX操作系統開發。2000年5月,以GNU通用公共許可證發布這套文件系統的源代碼,之後被移植到Linu