無論對于Java程序員還是大數(shù)據(jù)研發(fā)人員,JVM是必須掌握的技能之一。既是面試中經(jīng)常問的問題,也是在實際業(yè)務中對程序進行調(diào)優(yōu)、排查類似于內(nèi)存溢出、棧溢出、內(nèi)存泄漏等問題的關(guān)鍵。筆者將按下圖分多篇文章詳細闡述JVM:
本篇文章主要敘述JVM內(nèi)存管理、直接內(nèi)存、垃圾回收和常見的垃圾回收算法:
運行時數(shù)據(jù)區(qū)域
JVM在執(zhí)行一些基于JVM運行的程序,典型的如Java程序、Scala程序時,會把它所管理的內(nèi)存劃分為多個不同的數(shù)據(jù)區(qū)域。這些區(qū)域有各個的作用、創(chuàng)建和銷毀時間,有的區(qū)域生命周期依賴于用戶線程的啟動和結(jié)束,有些區(qū)域則隨著虛擬機的啟動而存在,下圖展示了JVM在運行時的數(shù)據(jù)區(qū)域劃分:
1. 方法區(qū)
方法區(qū)是各個線程共享的內(nèi)存區(qū)域,主要用于存放一些"自始至終都不會變化"的東西,比如final定義的常量、類的信息(class實例)、靜態(tài)變量等、方法信息。因為這些東西一旦被加載,是幾乎不會被GC的,所以方法區(qū)又被稱為永久代(注意一點,二者本質(zhì)并不等價)。
方法區(qū)有一部分叫常量池,用于存儲編譯期生成的一些字面變量、符號引用以及一些運行時產(chǎn)生的常量(如String常量池)。方法區(qū)中的靜態(tài)區(qū)用于存放類變量、靜態(tài)塊等。
方法區(qū)又稱非堆,是有大小限制的,如果方法區(qū)使用內(nèi)存超過了分配的大小,就會報類似OutOfMemory: PermGen Space的錯誤。
2. Java虛擬機棧
Java 虛擬機棧是線程私有的,它的生命周期與線程相同,為虛擬機執(zhí)行Java方法即字節(jié)碼服務,是描述Java方法執(zhí)行時的內(nèi)存模型。
每個方法執(zhí)行時都會創(chuàng)建一個棧幀用于存儲局部變量表(比如編譯期可知的基本數(shù)據(jù)類型、對象引用等)、操作棧、動態(tài)鏈接、方法出口等信息。每一個方法被調(diào)用至執(zhí)行完成的過程,對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
如果線程請求的棧深度大于虛擬機所允許的深度,將會報StackOverFlowError;如果虛擬機棧無法申請到足夠的內(nèi)存時會報OutOfMemoryError。
調(diào)整虛擬機棧大小的方式:-Xss。
3. 本地方法棧
本地方法棧為使用的到Native方法服務,本地方法接口都會使用某種本地方法棧。
當線程調(diào)用Java方法時,虛擬機會創(chuàng)建一個新的棧幀并壓入Java棧。然而,當它調(diào)用的是本地方法時,虛擬機會保持Java棧不變,不會在線程的Java棧中壓入新的棧幀,而是動態(tài)連接并直接調(diào)用指定的本地方法。
4. 堆
堆是JVM管理內(nèi)存中最大的一塊區(qū)域,由Java線程共享,主要用來存儲new出來的對象和數(shù)組,并且這塊區(qū)域隨著虛擬機的啟動而創(chuàng)建。堆可以處于邏輯上連續(xù)但物理上不連續(xù)的內(nèi)存空間中。
堆是垃圾回收器管理的主要區(qū)域,可以細分為新生代和老年代,新生代又劃分為eden區(qū),from survivor區(qū)、to survivor區(qū)。
對象在被創(chuàng)建時,首先在新生代進行分配,eden區(qū)存放新生成的對象,兩個survivor區(qū)用來存放新生代中每次垃圾回收后依然存活下來的對象。但是當創(chuàng)建新創(chuàng)建的對象非常大,該對象會直接進入老年代。
5. 程序計數(shù)器
程序計數(shù)器是線程私有的即每個線程都會有自己的程序計數(shù)器,用來記錄線程執(zhí)行的字節(jié)碼位置,是一個沒有OOM的區(qū)域。
直接內(nèi)存
直接內(nèi)存(direct memory)不屬于JVM運行時數(shù)據(jù)區(qū)的一部分,屬于堆外內(nèi)存,會被頻繁使用,因此在設置各個內(nèi)存范圍時要留出一部分物理內(nèi)存,否則也容易拋出OutOfMemoryError。
垃圾收集
垃圾收集即GC,是JVM進行內(nèi)存回收的處理過程。
開發(fā)人員更多的是關(guān)注業(yè)務需求的實現(xiàn),而內(nèi)存管理是交由JVM完成的,如果不進行或者錯誤的進行垃圾回收會導致程序不穩(wěn)定甚至崩潰。Java提供的GC功能可以自動監(jiān)測對象是否超過作用域等從而達到自動回收內(nèi)存的目的,可以有效防止內(nèi)存泄露,有效的使用可用內(nèi)存。
GC主要分為3種:minor GC、major GC和full GC。
minor GC是發(fā)生在新生代的,minor GC是發(fā)生在老年代的。對于full GC出發(fā)的原因則比較多,比如老年代空間不足,它會出發(fā)stop world,處理不好往往會影響整個程序的穩(wěn)定性嚴重會導致系統(tǒng)不可用,需要特別注意。
常見的垃圾回收算法
1. 標記清除算法
首先標記出所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象。存在如下兩個缺點:1.效率低需要先對要回收的對象進行標記,然后再統(tǒng)一清除,然而標記和清除兩個過程效率都很低下。2.內(nèi)存碎片問題標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作,影響性能。
2. 復制算法
先將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當使用的這一塊的內(nèi)存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
優(yōu)點:這樣使得每次都是對整個半?yún)^(qū)進行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復雜情況,只要移動堆頂指針,按順序分配內(nèi)存即可,實現(xiàn)簡單,運行高效。
缺點:不適合對象存活率較高的場景,因為這種場景要進行較多的復制操作影響效率;實際可用內(nèi)存變?yōu)榉峙鋬?nèi)存的一半,因為每次只使用其中的一半內(nèi)存。
3. 標記整理算法
先標記(標記過程與標記清除算法一樣),讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存。這樣可以解決內(nèi)存碎片問題。
4. 分代收集算法
就是針對Java堆內(nèi)存中新生代、老年代等采用不同的垃圾回收算法。如在新生代中,往往只有少量對象存活(最后會進入老年代),則適合用復制算法。而老年代中對象存活率較高,沒有額外的空間對它進行分配擔保,就使用標記清除算法。
當然實際應用中,使用什么算法,要看使用的垃圾回收器。
本文來自博客園,原文鏈接:https://www.cnblogs.com/bigdatalearnshare/p/13864712.html
申請創(chuàng)業(yè)報道,分享創(chuàng)業(yè)好點子。點擊此處,共同探討創(chuàng)業(yè)新機遇!