前言
相信很多人和我一樣長期使用java編程,卻很少關注過JVM底層實現,這很大程度上是因為JVM設計的很精巧,因此平時項目也很少遇到涉及JVM的問題。但是一方面出於對java底層技術的好奇,另一方面某些高並發,要對特定場景優化或者是排錯的問題也迫切需要對JVM實現的了解,於是樓主這兩天仔細拜讀了《inside JVM》這本關於JVM的經典著作,對JVM的一些實現細節有了較為清楚的認識,將一些學習的體會和收獲記錄下來與各位有相同困擾的朋友分享。
本文將從JVM的幾大核心技術切入:JVM內存管理、class文件格式、類裝載、垃圾收集、多線程並發。需要注意的是因為Java是一個平台無關的技術,JVM在不同平台上必須有不同的實現,因此當年的Sun發布了一個JVM specification(Java 虛擬機規范)。任何團體或個人實現的JVM都必須遵照該規范才能正確的運行java程序。因此本文討論的很多技術可能在不同的虛擬機上實現會有所不同,本文只是討論一些通用的技術以及虛擬機規范定義的一些要求。
JVM內存管理
在Java虛擬機規范中,將JVM虛擬機的內存分成了如下圖中運行時數據區幾大區域
這幾個區域分別是方法區,堆,Java棧,PC寄存器,本地方法棧。接下來我們就來詳細認識下這些內存區域的作用。
首先要說的是堆,堆中存放的是所有在java程序運行過程中創建的對象,因為在java里,數組是以對象的形式存在,因此數組也是存放在堆中的。堆占據了JVM的大部分內存。因此也是Java的GC,垃圾收集器主要工作的目標區域。
接下來要說的是方法區,方法區里存儲了所有類裝載進來后和這個類相關的所有運行時需要的信息(如類的靜態變量,常量,類的全局名稱,方法信息等)。我們在后面介紹class文件的章節里會詳細介紹class文件加載進來之后是如何將這些信息對應寫入方法區的數據結構中的。
和前面介紹的兩個區域是所有線程共享的不同,Java棧和本地方法棧以及PC寄存器都是線程獨占的,也就是說每個線程都有一個java棧和PC寄存器或者本地方法棧(如果用到了本地方法的話)。
說到這里需要介紹一下本地方法,我們知道java是跨平台的,但是我們比如在需要讀文件的時候,不用去關心將來是在哪個平台運行,只要調用FileInputStream把文件讀入就可以了,不用調用底層操作系統的API函數,這是因為不同平台Java的API把所有這些與平台相關的操作都封裝了起來提供了一個統一的Java編程接口。而Java的API正是通過調用一些本地方法(這些方法很多時候是一些編譯后的可執行的C程序)來實現了這些功能。同時雖然Java實現了大部分平台都有的一些功能(如IO,多線程等),但是有些平台的一些功能是該平台特有的,提供Java虛擬機的廠商為了提供這些功能往往就以動態鏈接庫的形式提供一些本地方法的調用來完善JVM在該平台的功能。至於如何去調用以及如何與本地方法通信(獲取返回值等)就是具體JVM實現需要去做的事情。
說了這么多本地方法的內容,現在回到Java棧的部分,每個線程都有一個自己獨立的Java棧,每次線程執行到一個新的方法時就在棧里面壓入一個棧幀。幀里包含了方法里的局部變量,操作數棧以及幀數據區。這三種區域中局部變量很好理解,就是在方法作用范圍內的變量,包括基本變量和對象的引用。理解操作數棧要先對JVM執行java程序的過程有所了解,JVM在裝載進class文件后可能采用解釋執行、即時編譯執行、混合執行這三種方式來執行class文件中的JVM指令集。JVM指令集是一個4字節的指令集,就像匯編語言做相加操作需要先將兩個數存入寄存器一樣,JVM指令做數據相關的操作也要先將數據壓入java棧里面的操作數棧才能進行。比如方法里將i變量和j變量相加賦值給z,JVM先將i壓入操作數棧,再將j壓入操作數棧,最后將結果寫回局部變量表或者是對象的字段。至於幀數據區,是為了在方法執行過程中訪問方法區的數據以及返回方法結果而用的。某個方法執行結束完之后如果是正常返回則會將返回結果壓入上一個方法的操作數棧中,如果是異常退出且沒有catch該異常則會運行到上一個方法繼續拋出該異常。
本地方法和Java方法一樣,只是Java棧是執行Java方法的線程申請的內存,而本地方法是執行本地方法而申請的內存。下面這張圖顯示了兩者的關系。
最后程序計數器是為每個線程記錄當前執行的字節碼位置而設立的,線程切換時需要記錄下當前執行到哪一步了以便該線程重新獲取CPU執行時能繼續正確執行。
順便說一句,在java里面對象是通過引用來操作的,棧里面存儲的引用,而堆里存儲的對象。不同的JVM實現在引用的具體實現上可能有所不同,兩種比較流行的方式分別是通過對象句柄引用和通過直接指針引用。JVM的GC也是通過引用來確定哪些對象可以回收。下圖分別表示了兩種引用的實現:
對比這兩種引用實現,句柄池的方法在GC需要移動對象(消除內存碎片以存放大對象)時,只需要將句柄池中每個對象的指針地址修改即可。但是引用訪問對象需要經過兩個地址查找,降低了效率。直接指向對象的方式在需要移動對象時要將每個引用的地址都做修改,這相對直接修改句柄池來說要昂貴的多,但是因為一次尋址提高了效率。
細心的讀者可能注意到不管采用什么方式,每個引用都有一個指向方法區里該類數據的指針。這是因為在java里面不像C++可以直接對內存對象做類型轉換,Java類型轉換前一定要做類型檢查以保證這次轉換是安全的以避免可能因此帶來的程序崩潰。因此每個引用都有一個指向類型數據的指針。
本文花了很大篇幅介紹java棧的內容,是因為作者認為在這幾個區域中,Java棧是最難理解的部分,希望讀者能耐心讀完,有什么問題也歡迎留言交流,最后為了加深對堆和棧存儲哪些數據的理解,作者寫了兩個分別產生OutOfMemoryError和StackOverflowError的函數以幫助理解,oom函數在數組對象s中不停的添加數據,最后堆內存無法滿足新的添加需求JVM就退出同時報出了OutOfMemoryError, stack()方法中有一個s的雙精度局部變量,同時不停的遞歸調用自己,Java棧中就不停的壓入新的方法棧,最后JVM退出並報出了StackOverflowError
Java代碼:
package Experiment; import java.util.ArrayList; public class TestJVM { public static void main(String[] args) { stackof(); //oom();
} private static void oom() { ArrayList<Integer> s=new ArrayList<>(); while(true) { s.add(1); } } private static void stackof() { double s; stackof(); } }
運行結果:
原文地址:http://www.cnblogs.com/developerY/p/3330811.html 轉載請注明出處