當我們第一次學習Java時這些原理上的東西就會被提到奶甘,但是很少有真正去學習篷店。今天開始從頭過一遍Java,打算從JVM開始臭家。
1. JVM是什么
JVM是Java Virtual Mechine的縮寫。它是一種基于計算設備的規(guī)范钉赁,是一臺虛擬機蹄殃,即虛構的計算機。
JVM屏蔽了具體操作系統平臺的信息(顯然你踩,就像是我們在電腦上開了個虛擬機一樣)诅岩,當然,JVM執(zhí)行字節(jié)碼時實際上還是要解釋成具體操作平臺的機器指令的带膜。
通過JVM吩谦,Java實現了平臺無關性,Java語言在不同平臺運行時不需要重新編譯膝藕,只需要在該平臺上部署JVM就可以了式廷。因而能實現一次編譯多處運行。(就像是你的虛擬機也可以在任何安了VMWare的系統上運行)
2. JRE和JDK
JRE:Java Runtime Environment芭挽,也就是JVM的運行平臺滑废,聯系平時用的虛擬機,大概可以理解成JRE=虛擬機平臺+虛擬機本體(JVM)袜爪。類似于你電腦上的VMWare+適用于VMWare的Ubuntu虛擬機蠕趁。這樣我們也就明白了JVM到底是個什么。
JDK:Java Develop Kit辛馆,Java的開發(fā)工具包俺陋,JDK本體也是Java程序,因此運行依賴于JRE,由于需要保持JDK的獨立性與完整性,JDK的安裝目錄下通常也附有JRE倔韭。目前Oracle提供的Windows下的JDK安裝工具會同時安裝一個正常的JRE和隸屬于JDK目錄下的JRE术浪。
3. JVM結構
JVM主要包括:程序計數器(Program Counter),Java堆(Heap)寿酌,Java虛擬機棧(Stack)胰苏,本地方法棧(Native Stack),方法區(qū)(Method Area)
詳細的結構如下:
現在我來分別介紹一下每一部分的功能醇疼。
3.1. 程序計數器(PC, Program Counter)
是一個寄存器硕并,可以看作是代碼行號指示器,類似于實際計算機里的PC秧荆,用于指示倔毙,跳轉下一條需要執(zhí)行的命令。Java的基礎操作以及異常處理等都十分依賴PC乙濒。
JVM多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現的陕赃。在一個確定的時刻,一個處理器(或者說多核處理器的一個內核)只會執(zhí)行一條線程中的命令颁股。因此么库,為了正常的切換線程,每個線程都會有一個獨立的PC甘有,各線程的PC不會互相影響诉儒。這個私有的PC所占的這塊內存即是線程的“私有內存”。
如果線程在執(zhí)行的是Java方法亏掀,那么PC記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址忱反。如果正在執(zhí)行的不是Java方法即Native方法,那么PC的值為undefined滤愕。
PC的內存區(qū)域是唯一的沒有規(guī)定任何OutOfMemoryError的Java虛擬機規(guī)范中的區(qū)域温算。
3.2. Java虛擬機棧(Stack,Java Virtual Mechine Stacks)
同PC一樣(從工作流程圖里我們可以看到该互,實際上米者,PC也是存在于JVM Stack上的),也是線程私有的宇智,生命周期與線程相同蔓搞。虛擬機棧描述Java方法執(zhí)行的內存模型,每個方法被執(zhí)行時都會創(chuàng)建一個棧幀(Stack Frame)随橘,棧幀會利用局部變量數組存儲局部變量(Local Variables)喂分,操作棧(Operand Stack),方法出口(Return Value)机蔗,動態(tài)連接(Current Class Constant Pool Reference)等信息蒲祈。
局部變量數組存儲了編譯可知的八個基本類型(int, boolean, char, short, byte, long, float, double)甘萧,對象引用(根據不同的虛擬機實現可能是引用地址的指針或者一個handle),returnAddress類型梆掸。64位的long和double會占用兩個Slot扬卷,其余類型會占用一個Slot。在編譯期間酸钦,局部變量所需的空間就會完成分配怪得,動態(tài)運行期間不會改變所需的空間。
操作棧在執(zhí)行字節(jié)碼指令時會被用到卑硫,這種方式類似于原生的CPU寄存器徒恋,大部分JVM把時間花費在操作棧的花費上,操作棧和局部變量數組會頻繁的交換數據欢伏。
動態(tài)連接控制著運行時常量池和棧幀的連接入挣。所有方法和類的引用都會被當作符號的引用存在常量池中。符號引用是實際上并不指向物理內存地址的邏輯引用硝拧。JVM 可以選擇符號引用解析的時機径筏,一種是當類文件加載并校驗通過后,這種解析方式被稱為饑餓方式河爹。另外一種是符號引用在第一次使用的時候被解析匠璧,這種解析方式稱為惰性方式桐款。無論如何 咸这,JVM 必須要在第一次使用符號引用時完成解析并拋出可能發(fā)生的解析錯誤。綁定是將對象域魔眨、方法媳维、類的符號引用替換為直接引用的過程。綁定只會發(fā)生一次遏暴。一旦綁定侄刽,符號引用會被完全替換。如果一個類的符號引用還沒有被解析朋凉,那么就會載入這個類州丹。每個直接引用都被存儲為相對于存儲結構(與運行時變量或方法的位置相關聯的)偏移量。
對Java虛擬機棧這個區(qū)域杂彭,Java虛擬機規(guī)范規(guī)定了兩種異常:
- 線程請求的棧深度大于虛擬機所允許的深度墓毒,拋出StackOverFlow異常。
- 對于支持動態(tài)擴展的虛擬機亲怠,當擴展無法申請到足夠的內存時也會拋出StackOverFlow異常所计。
3.3. 本地方法棧(Native Stack)
本地方法棧如其名字,和Java Virtual Machine Stack其實極為類似团秽,只是執(zhí)行的是Native方法主胧,為Native方法服務叭首。在JVM規(guī)范中,沒有對它的實現做具體規(guī)定踪栋。
3.4. Java 堆(Heap, Garbage Collection Heap)
Java堆是被所有線程共享的一塊區(qū)域焙格,在虛擬機啟動時創(chuàng)建。此內存區(qū)域的唯一目的就是存放對象實例夷都,幾乎所有的對象實例都在這里分配內存(隨著技術的發(fā)展间螟,已不絕對)。
Java堆是垃圾收集器管理的主要區(qū)域损肛,因而也被稱為GC堆厢破。收集器采用分代回收法,GC堆可以分為新生代(Yong Generation)和老生代(Old Generation)治拿。新生代包括Eden Space和Survivor Space摩泪。但無論哪個區(qū)域,如何劃分劫谅,存儲的都是Java對象實例见坑,進一步的劃分是為了更好的回收內存或快速的分配內存。
根據Java虛擬機規(guī)范捏检,堆所在的物理內存區(qū)間可以是不連續(xù)的荞驴,只要邏輯連續(xù)就可以。實現時既可以是固定大小贯城,也可以是可擴展的熊楼。如果堆無法擴展時,就會拋出OutOfMemoryError能犯。
3.5. 方法區(qū)(Method Area)
方法區(qū)和Java堆類似鲫骗,也屬于各線程共享的內存區(qū)域。用于存儲已被虛擬機加載的類信息踩晶,常量执泰,靜態(tài)變量,即時編譯器編譯后的代碼數據等渡蜻。它屬于非堆區(qū)(Non Heap)术吝,和Java堆區(qū)分開。對于存在永久代(Permanent)概念的虛擬機(HotSpot)而言茸苇,方法區(qū)存在于永久代排苍。Java虛擬機規(guī)范對方法區(qū)的規(guī)定很寬松,甚至可以不實現GC税弃。不過并非進入方法區(qū)的數據就會永久存在了纪岁,這塊區(qū)域的內存回收主要為常量池的回收和類型的卸載。這個區(qū)域的回收處理不善也會導致嚴重的內存泄漏则果。
當方法區(qū)無法滿足內存分配需求時也會拋出OutOfMemoryError幔翰。
3.6. 代碼緩存(Code Cache)
用于編譯和存儲那些被 JIT 編譯器編譯成原生代碼的方法漩氨。
3.7. 類信息(Class Data)
類信息存儲在方法區(qū),其主要構成為運行時常量池(Run-Time Constant Pool)和方法(Method Code)遗增。
一個編譯后的類文件包括以下結構:
結構 | 解釋 |
---|---|
magic, minor_version, major_version | 類文件的版本信息和用于編譯這個類的 JDK 版本叫惊。 |
constant_pool | 類似于符號表,盡管它包含更多數據做修。下面有更多的詳細描述霍狰。 |
access_flags | 提供這個類的描述符列表。 |
this_class | 提供這個類全名的常量池(constant_pool)索引饰及,比如org/jamesdbloom/foo/Bar蔗坯。 |
super_class | 提供這個類的父類符號引用的常量池索引。 |
interfaces | 指向常量池的索引數組燎含,提供那些被實現的接口的符號引用宾濒。 |
fields | 提供每個字段完整描述的常量池索引數組。 |
methods | 指向constant_pool的索引數組屏箍,用于表示每個方法簽名的完整描述绘梦。如果這個方法不是抽象方法也不是 native 方法,那么就會顯示這個函數的字節(jié)碼赴魁。 |
attributes | 不同值的數組卸奉,表示這個類的附加信息,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 注解颖御。 |
3.8. 運行時常量池(Run-Time Constant Pool)
運行時常量池是方法區(qū)的一部分戏阅。Class文件中有類的版本捐名,字段堵泽,方法史飞,接口等描述信息和用于存放編譯期生成的各種字面量和符號引用阶祭。這部分內容將在類加載后存放到方法區(qū)的運行時常量池中雕擂。Java虛擬機規(guī)范對Class的細節(jié)有著嚴苛的要求而對運行時常量池的實現不做要求缘挽。一般來說除了翻譯的Class,翻譯出來的直接引用也會存在運行時常量池中北发。
運行時常量池具備動態(tài)性辑鲤,即運行時也可將新的常量放入池中盔腔。比如String類的intern()方法。
常量池無法申請到足夠的內存分配時也會拋出OutOfMemoryError月褥。
3.9. 直接內存(Direct Memory)
直接內存并不在Java虛擬機規(guī)范中弛随,不是Java的一部分,但是也被頻繁使用并可能導致OutOfMemoryError宁赤。Native函數庫可以直接分配堆外內存舀透,通過存儲在Java堆里的DirectDataBuffer對象作為這塊內存的引用進行操作。這樣做在一些場景中可以顯著提高性能决左。
直接內存是堆外內存愕够,自然不受Java堆大小的限制走贪,但是可能受實體機內存大小的限制。如果內存各部分總和大于實體機的內存時惑芭,也會報出OutOfMemoryError坠狡。
4. Java垃圾回收
將內存中不再被使用的對象進行回收,GC中用于回收的方法稱為收集器遂跟,由于GC需要消耗一些資源和時間逃沿,Java在對對象的生命周期特征進行分析后,按照新生代幻锁、舊生代的方式來對對象進行收集凯亮,以盡可能的縮短GC對應用造成的暫停。
不同的對象引用類型哄尔, GC會采用不同的方法進行回收触幼,JVM對象的引用分為了四種類型:
- 強引用:默認情況下,對象采用的均為強引用(這個對象的實例沒有其他對象引用究飞,GC時才會被回收)置谦。
- 軟引用:軟引用是Java中提供的一種比較適合于緩存場景的應用(只有在內存不夠用的情況下才會被GC)。
- 弱引用:在GC時一定會被GC回收亿傅。
- 虛引用:由于虛引用只是用來得知對象是否被GC媒峡。
5. JVM線程與原生線程的關系
JVM允許一個程序使用多個并發(fā)線程,Hotspot JVM中Java的線程與原生操作系統的線程是直接映射關系葵擎。即當線程本地存儲谅阿、緩沖區(qū)分配、同步對象酬滤、棧签餐、程序計數器等準備好以后,就會創(chuàng)建一個操作系統原生線程盯串。Java 線程結束氯檐,原生線程隨之被回收。操作系統負責調度所有線程体捏,并把它們分配到任何可用的 CPU 上冠摄。當原生線程初始化完畢,就會調用 Java 線程的 run() 方法几缭。run() 返回時河泳,被處理未捕獲異常,原生線程將確認由于它的結束是否要終止 JVM 進程(比如這個線程是最后一個非守護線程)年栓。當線程結束時拆挥,會釋放原生線程和 Java 線程的所有資源。