JVM是Java的運行時虛擬機,所有的Java程序都是在JVM沙箱中運行,每個Java程序就是一個獨立的JVM進程。
談到Java程序是如何運行的,首先需要理解的肯定是JVM是如何運行的,什么是JVM;要理解我們編寫的Java程序,運行起來以后到底是什么樣子,本質上就是弄清楚JVM是什么樣子。
Java程序的代碼是什么樣的
Java誕生之初最大的賣點就是編寫的代碼跨平臺可移植性,實現這種可移植性,是因為Java通過平臺特定的虛擬機,運行中間的字節碼,而不是直接編譯成本地二進制代碼實現,中間字節碼也就是java文件編譯后生成的.class文件,Jar包的話,實際上只是一系列.class文件的集合。
編寫Java程序,首先需要一個入口點,在運行的時候通過指定MainClass來指定入口點,代碼層面主類必須實現一個靜態的main函數,運行時虛擬機會從MainClass.main開始執行指令,其他的邏輯只是import和函數調用了。
SDK自帶的javac命令,負責將我們編程的Java代碼,也就是.java文件,編譯成平臺無關的字節碼;字節碼可以在任何操作系統平臺上,通過平臺對應的JVM執行;JVM執行的時候,運行字節碼,根據自己的平臺特性,將字節碼轉換成平臺相關的二進制碼運行。
javac編譯器運行的過程大致分為:詞法分析(Token流)、語法分析(語法樹)、語義分析(注解語法樹),還有代碼生成器,根據注解語法樹,生成字節碼,
語義分析階段,編譯器會做一些操作,將人類友好的代碼,做一些處理,轉換成更符合機器執行機制的代碼,例如全局變量,魔法變量,依賴注入,注解這些魔法機制。大致分為以下步驟:
給類添加默認構造函數
處理注解
檢查語義的合法性并進行邏輯判斷
數據流分析
對語法樹進行語義分析(變量自動轉換并去掉語法糖)
JVM是什么
JVM = 類加載器 classloader + 執行引擎 execution engine + 運行時數據區域 runtime data area
JVM就是運行編譯好字節碼的虛擬機,不同的操作系統和平臺上,虛擬機將平臺無關的字節碼,編譯成特定平臺的指令去執行。我覺得,JVM首先是一個獨立運行在操作系統上的進程。執行java命令運行程序的時候,會啟動一個進程,每個獨立的程序就運行在一個獨立的JVM進程里。JVM負責執行字節碼,從而實現程序要完成的所有功能。
JVM主要由三部分組成:類加載器、運行時數據區和執行引擎。類加載器加載編譯好的.class文件,將所有類結構和方法變量放入運行時數據區,初始化之后,將程序的執行交給執行引擎;JIT編譯器,負責將字節碼編譯成平臺特定的二進制碼,調用本地接口庫。垃圾回收器作為執行引擎的一部分,負責維護運行時數據區中可變的應用程序內存空間。
類加載器(ClassLoader)
類加載器將類加載到內存,并管理類的生命周期,知道將類從內存中卸載結束生命周期。
系統提供了三種類加載器,分別用于不同類的加載:
啟動類加載器(Bootstrap ClassLoader),該加載器會將《JAVA_HOME》lib目錄下能被虛擬機識別的類加載到內存中,也就是系統類
擴展類加載器(Extension ClassLoader),該加載器會將《JAVA_HOME》libext目錄下的類庫加載到內存
應用程序類加載器(Application ClassLoader),該加載器負責加載用戶路徑上所指定的類庫。
運行時數據區(Runtime Data Area)
運行時數據區,是JVM運行時,在內存中分配的空間。
運行時數據區,被分為五個不同的結構:
Java虛擬機棧(Java Stacks): 也叫棧內存,主管Java程序的運行,是在線程創建時創建,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放,對于棧來說不存在垃圾回收問題,只要線程一結束該棧就Over,生命周期和線程一致,是線程私有的。
本地方法棧(Native Method Memory): 登記的native方法,執行引擎執行時加載
程序寄存器(PC Registers): 當前線程所執行字節碼的指針,存儲每個線程下一步要執行的字節碼JVM指令。
Java堆(Heap Memory): 應用的對象和數據都是存在這個區域,這塊區域也是線程共享的,也是gc 主要的回收區,一個 JVM 實例只存在一個堆類存,堆內存的大小是可以調節的。類加載器讀取了類文件后,需要把類、方法、常變量放到堆內存中,以方便執行器執行。
方法區(Method Area): 所有定義的方法的信息都保存在該區域,此區域屬于共享區間。靜態變量+常量+類信息+運行時常量池存在方法區中,實例變量存在堆內存中。
其中的程序寄存器、Java虛擬機棧是按照線程分配的,每個線程都有自己私有的獨立空間。
運行的方法和運行期數據,以棧幀的形式存儲在運行時JVM虛擬機棧中,棧幀中保存了本地變量,包括輸入輸出參數和本地變量;保存類文件和方法等幀數據,還記錄了出棧入棧操作。每一個方法被調用直至執行完成的過程就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
堆在JVM是所有線程共享的,因此在其上進行對象內存的分配均需要進行加鎖。
執行引擎(Execution Engine)
執行引擎由三個模塊組成,分別是執行引擎,JIT Compiler和Garbage Collector,執行引擎的核心是Jit Compiler,執行字節碼或者本地方法;垃圾回收器,則是一系列線程,負責管理分代堆內存。
三個模塊分別是運行時計算和運行時內存的管理,負責執行運行時指令的是執行引擎,通過程序寄存器和虛擬機棧中的棧幀出入棧實現方法和指令的執行。GC則負責堆內存的管理,因為GC的時候需要停止指令的執行,消耗資源,所以采用分代方式管理對象收集。JIT則是把字節碼編譯成本地二進制代碼,并調用本地庫執行。
GC垃圾回收機制
Java的內存管理,主要是針對的堆內存,因為堆內存是運行時程序和數據分配的空間;不同于內存的其他區域,加載完程序之后,基本上可以確定需要占用的空間大??;heap memory 空間會在運行時動態的分配,無法預測,可大可小,而且快速變化,管理不慎就容易產生內存溢出,所以由JVM提供了強大的分代內存管理機制。
JVM 使用分代內存管理,來分配運行時的堆內存結構,針對不同的世代,采用不同的垃圾回收算法。
常用垃圾回收算法
引用計數器法(Reference Counting)
標記清除法(Mark-Sweep)
復制算法(Coping)
標記壓縮法(Mark-Compact)
分代算法(Generational Collecting)
分區算法(Region)
堆內存的組成
heap 的組成有三區域/世代:分別是新生代(Young Generation)、老生代(Old Generation/tenured)和永久區(Perm)。
新生代堆內存又分成Eden區和兩個生存區,其中Eden區和生存區的占比為8:1:1,在清理新生代內存的時候,使用的是復制清除算法,優點是清除以后不會產生碎片;簡單的復制算法,將內存分成大小相同的兩個區域,每次周期只分配其中的一半,這樣空間利用率比較低,只使用了一半的內存。
考慮到新生代內存區的對象都是周期很短的,所以JVM實現了一種優化的復制算法,設置一個較大的Eden區來分配對象內存,Eden區空間不夠了觸發垃圾回收,將上一個生存區和Eden區中還存活的對象,復制到空閑的生存區。然后清空上述兩個區域,這樣就不會產生內存碎片。
將清理一定次數(15次)還生存的對象,定期晉升到老生代內存區,如果生存區空間不夠了,則馬上就會觸發晉升機制。將部分對象直接晉升到老生代。
如果晉升之后,發現老生代內存不夠,就會觸發完整的全局GC,清理老生代和新生代內存,老生代內存清理需要使用標記清除和標記整理兩種算法。
GC工作原理
分配內存的時候,首先分配到新生代的Eden區,如果Eden區滿了,就會發起一次Minor GC,將Eden和From Survivor生存的對象,拷貝到To Survivor Space,如果清理過程中,to Space的空間占用達到一定閾值,或者有對象經歷Minor GC的次數達標,就會將對象移動到老生代內存。如果移動過程中發現,老生代內存的空間已經不夠了。這時就需要發起Full GC,先進行一次Minor GC,然后通過CMS進行標記清除算法,清理老生代內存,老生代內存經歷標記清除之后,因為會產生內存碎片,還需要采用標記整理算法,將所有內存塊往前移動,形成連續的內存空間。
老生代標記清除的優點是不需要額外空間。不同于老生代清除算法,會產生碎片,而且標記算法的成本開銷也很大;在新生代清除中,因為考慮到大多數新生代對象生存期都是很短暫的,可以使用一種空間換時間的思路,拿出一部分內存空間不分配,而是作為中轉,將每次檢查時還生存的對象拷貝到Survivor Space,然后直接清除所有原區域的對象,因為大量對象都是生存周期極短的,所以Survivor Space的空間可以遠小于正常分配的空間。
不同于引用計數方法,Java使用一種 GC Roots 的對象作為起點開始檢查對象,當一個對象到 GC Roots 沒有任何引用鏈相連時, 即該對象不可達, 也就說明此對象是不可用的。就會在GC的時候收回。
GC清理類型的時候,為了防止程序地址出現異常,需要stop the world,清理線程會停止所有運行線程,直到清理完,這個時候是影響性能的。
垃圾回收器的本質
垃圾回收器在JVM層面,是由一系列不同的組件組成的,每種組件是一個獨立線程,分別執行自己的邏輯。
新生代垃圾收集器:
Serial(串行)收集器是最基本、發展歷史最悠久的收集器,它是采用復制算法的新生代收集器,。它是一個單線程收集器,只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集時,必須暫停其他所有的工作線程,直至Serial收集器收集結束為止(“Stop The World”)。
ParNew收集器就是Serial收集器的多線程版本,它也是一個新生代收集器。除了使用多線程進行垃圾收集外,其余行為包括Serial收集器可用的所有控制參數、收集算法(復制算法)、Stop The World、對象分配規則、回收策略等與Serial收集器完全相同,兩者共用了相當多的代碼。
Parallel Scavenge收集器也是一個并行的多線程新生代收集器,它也使用復制算法。Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標是達到一個可控制的吞吐量(Throughput)。
老生代垃圾收集器:
Serial Old 是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”(Mark-Compact)算法
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,優點是:并發收集、低停頓,因此CMS收集器也被稱為并發低停頓收集器(Concurrent Low Pause Collector)
面向服務端的G1收集器。
G1收集器是一款面向服務端應用的垃圾收集器。
在使用G1收集器時,Java堆的內存布局和其他收集器有很大的差別,它將這個Java堆分為多個大小相等的獨立區域,雖然還保留新生代和老年代的概念,但是新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。
GC回收的觸發條件
Minor GC觸發條件:當Eden區滿時,觸發Minor GC
Full GC觸發條件:
gc()方法的調用
老年代代空間不足
方法區空間不足
CMS GC時出現promotion failed和concurrent mode failure
統計得到的Minor GC晉升到舊生代的平均大小大于老年代的剩余空間
堆中分配很大的對象
通過Minor GC后進入老年代的平均大小大于老年代的可用內存
由Eden區、From Space區向To Space區復制時,對象大小大于To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小于該對象大小
GC Roots
在Java語言中,可以作為GC Roots的對象包括下面幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象;
方法區中類靜態屬性引用的對象;
方法區中常量引用的對象;
本地方法棧中JNI(即一般說的Native方法)引用的對象;
總結就是,方法運行時,方法中引用的對象;類的靜態變量引用的對象;類中常量引用的對象;Native方法中引用的對象。
責任編輯:Ct
評論
查看更多