前言
了解JVM是對Java程序員的基本要求尝抖,但是有多少同學(xué)和我有一樣醉心解bug堆布局,忘記了內(nèi)功修煉,對JVM的理解是零碎的煎楣。系統(tǒng)地學(xué)習(xí)一次JVM也許能讓我們在這條路走得更好更遠(yuǎn)岂昭。
了解Java虛擬機(jī)家族
我們可以把Java程序設(shè)計語言以现、Java虛擬機(jī)、Java類庫這三部分統(tǒng)稱為JDK(Java Development
Kit)约啊,JDK是用于支持Java程序開發(fā)的最小環(huán)境邑遏。
JVM 是 JDK 的一部分,《Java 虛擬機(jī)規(guī)范》(The Java Virtual Machine Specification) 是平行于《Java 語言規(guī)范》(The Java Language Specification)的一套獨立的規(guī)范恰矩,不同的公司對其有不同的實現(xiàn) (類似于一個接口被不同的類實現(xiàn))记盒。
虛擬機(jī)始祖:Sun Classic/Exact VM
- 世界上第一款商用Java虛擬機(jī)
- 在JDK 1.2之前是JDK中唯一的虛擬機(jī)
- JDK 1.4的時候,Classic VM才完全退出商用虛擬機(jī)的歷史舞臺被HotSpot取代
武林盟主:HotSpot VM
- 是Sun/OracleJDK和OpenJDK中的默認(rèn)Java虛擬機(jī)外傅,也是目前使用范圍最廣的Java虛擬機(jī)
- 如它名稱中的HotSpot指的就是它的熱點代碼探測技術(shù)
- 為全世界使用最廣泛的Java虛擬機(jī)
小家碧玉:Mobile/Embedded VM
- 面對移動和嵌入式市場纪吮,Java ME中的Java虛擬機(jī)
天下第二:BEA JRockit/IBM J9 VM
- JRockit虛擬機(jī)曾經(jīng)號稱是“世界上速度最快的Java虛擬機(jī)”,是一款一款專門為服務(wù)器硬件和服務(wù)端應(yīng)用場景高度優(yōu)化的虛擬機(jī)。JRockit隨著BEA被Oracle收購萎胰,現(xiàn)已不再繼續(xù)發(fā)展
- IBM J9虛擬機(jī)的市場定位與HotSpot比較接近碾盟,它是一款在設(shè)計上全面考慮服務(wù)端、桌面應(yīng)用技竟,再到嵌入式的多用途虛擬機(jī)
軟硬合璧:BEA Liquid VM/Azul VM
- 與特定硬件平臺綁定冰肴、軟硬件配合工作的專有虛擬機(jī)
挑戰(zhàn)者:Apache Harmony/Google Android Dalvik VM
- Apache Harmony是一個Apache軟件基金會旗下以Apache License協(xié)議開源的實際兼容于JDK 5和JDK 6的Java程序運(yùn)行平臺,它含有自己的虛擬機(jī)和Java類庫API榔组,用戶可以在上面運(yùn)行Eclipse熙尉、Tomcat、Maven等常用的Java程序瓷患。
- Dalvik虛擬機(jī)并不是一個Java虛擬機(jī)骡尽,它沒有遵循《Java虛擬機(jī)規(guī)范》,不能直接執(zhí)行Java的Class文件擅编,使用寄存器架構(gòu)而不是Java虛擬機(jī)中常見的棧架構(gòu)攀细。但是它與Java卻又有著千絲萬縷的聯(lián)系,它執(zhí)行的DEX(Dalvik Executable)文件可以通過Class文件轉(zhuǎn)化而來爱态,使用Java語法編寫應(yīng)用程序谭贪,可以直接使用絕大部分的Java API等。
Java內(nèi)存區(qū)域劃分與OutOfMemory
根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定锦担,Java虛擬機(jī)所管理的內(nèi)存將會包括以下幾個運(yùn)行時數(shù)據(jù)區(qū)域:
程序計數(shù)器
- 是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器
- 線程私有: 每條線程都需要有一個獨立的程序計數(shù)器俭识,各條線程之間計數(shù)器互不影響,獨立存儲
- 如果線程正在執(zhí)行的是一個Java方法洞渔,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址套媚;如果正在執(zhí)行的是本地(Native)方法缚态,這個計數(shù)器值則應(yīng)為空(Undefined)
- 沒有OutOfMemory
Java虛擬機(jī)棧
- 線程私有
- 虛擬機(jī)棧描述的是Java方法執(zhí)行的線程內(nèi)存模型:每個方法被執(zhí)行的時候,Java虛擬機(jī)都會同步創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表堤瘤、操作數(shù)棧玫芦、動態(tài)連接、方法出口等信息本辐。每一個方法被調(diào)用直至執(zhí)行完畢的過程桥帆,就對應(yīng)著一個棧幀在虛擬機(jī)棧中從入棧到出棧的過程
- 局部變量表存放了編譯期可知的各種Java虛擬機(jī)基本數(shù)據(jù)類型(boolean、byte慎皱、char老虫、short、int茫多、float祈匙、long、double)天揖、對象引用(reference類型菊卷,它并不等同于對象本身,可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔槺ζ剩部赡苁侵赶蛞粋€代表對象的句柄或者其他與此對象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)
- 在《Java虛擬機(jī)規(guī)范》中,對這個內(nèi)存區(qū)域規(guī)定了兩類異常狀況:如果線程請求的棧深度大于虛擬機(jī)所允許的深度歉甚,將拋出StackOverflowError異常万细;如果Java虛擬機(jī)棧容量可以動態(tài)擴(kuò)展,當(dāng)棧擴(kuò)展時無法申請到足夠的內(nèi)存會拋出OutOfMemoryError異常
本地方法棧
- 與虛擬機(jī)棧所發(fā)揮的作用相似纸泄,為虛擬機(jī)使用到的本地方法服務(wù)
Java堆
- 是虛擬機(jī)所管理的內(nèi)存中最大的一塊
- 所有的對象實例以及數(shù)組都應(yīng)當(dāng)在堆上分配
- Java堆是被所有線程共享的一塊內(nèi)存區(qū)域
- 所有線程共享的Java堆中可以劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer赖钞,TLAB)Java堆內(nèi)存是線程共享的!面試官:你確定嗎聘裁?
- Java堆既可以被實現(xiàn)成固定大小的雪营,也可以是可擴(kuò)展的,不過當(dāng)前主流的Java虛擬機(jī)都是按照可擴(kuò)展來實現(xiàn)的(通過參數(shù)-Xmx和-Xms設(shè)定)衡便。如果在Java堆中沒有內(nèi)存完成實例分配献起,并且堆也無法再擴(kuò)展時,Java虛擬機(jī)將會拋出OutOfMemoryError異常镣陕。
方法區(qū)
- 與Java堆一樣谴餐,是各個線程共享的內(nèi)存區(qū)域
- 它用于存儲已被虛擬機(jī)加載的類型信息、常量呆抑、靜態(tài)變量岂嗓、即時編譯器編譯后的代碼緩存等數(shù)據(jù)
- 在JDK 8完全廢棄了永久代的概念,改用元空間來代替鹊碍。
- 運(yùn)行時常量池是方法區(qū)的一部分厌殉。Class文件中除了有類的版本食绿、字段、方法公罕、接口等描述信息外器紧,還有一項信息是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符號引用熏兄,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時常量池中
- 運(yùn)行期間也可以將新的常量放入池:比如String類的intern()方法
- 如果方法區(qū)無法滿足新的內(nèi)存分配需求時品洛,將拋出OutOfMemoryError異常。
直接內(nèi)存
- 并不是虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的一部分摩桶,也不是《Java虛擬機(jī)規(guī)范》中定義的內(nèi)存區(qū)域
- 在JDK 1.4中新加入了NIO(New Input/Output)類桥状,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存硝清,然后通過一個存儲在Java堆里面的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作辅斟。
- 會受到本機(jī)總內(nèi)存(包括物理內(nèi)存、SWAP分區(qū)或者分頁文件)大小以及處理器尋址空間的限制芦拿,而導(dǎo)致動態(tài)擴(kuò)展時出現(xiàn)OutOfMemoryError異常
Hotspot虛擬機(jī)對象
對象的創(chuàng)建
- 當(dāng)Java虛擬機(jī)遇到一條字節(jié)碼new指令時士飒,檢查是否類已加載、解析蔗崎、初始化酵幕,如果沒有,則進(jìn)行類加載
- 在類加載檢查通過后缓苛,接下來虛擬機(jī)將為新生對象分配內(nèi)存芳撒。
- 線程安全解決方案:1、CAS+失敗重試保證更新操作的原子性 2未桥、TLAB把內(nèi)存分配的動作按照線程劃分在不同的空間進(jìn)行
- 內(nèi)存分配完成之后笔刹,虛擬機(jī)必須將分配到的內(nèi)存空間(但不包括對象頭)都初始化為零值
- 接下來,Java虛擬機(jī)還要對對象進(jìn)行必要的設(shè)置冬耿,例如這個對象是哪個類的實例舌菜、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼(實際上對象的哈希碼會延后到真正調(diào)用Object::hashCode()方法時才計算)亦镶、對象的GC分代年齡等信息日月。這些信息存放在對象的對象頭(Object Header)之中。根據(jù)虛擬機(jī)當(dāng)前運(yùn)行狀態(tài)的不同缤骨,如是否啟用偏向鎖等山孔,對象頭會有不同的設(shè)置方式。
- new指令之后會接著執(zhí)行<init>()方法荷憋,按照程序員的意愿對對象進(jìn)行初始化台颠,這樣一個真正可用的對象才算完全被構(gòu)造出來
對象的內(nèi)存布局
- 在堆內(nèi)存中的存儲布局可以劃分為三個部分:對象頭(Header)、實例數(shù)據(jù)(Instance Data)和對齊填充(Padding)
- 對象頭包括:對象自身的運(yùn)行時數(shù)據(jù)(Mark Word),所屬類類指針串前,數(shù)組長度(如果是數(shù)組對象)
對象的訪問定位
- Java程序會通過棧上的reference數(shù)據(jù)來操作堆上的具體對象瘫里。
- 對象訪問方式也是由虛擬機(jī)實現(xiàn)而定的,主流的訪問方式主要有使用句柄和直接指針兩種
垃圾收集(GC)
對象的死亡
引用計數(shù)法
- 在對象中添加一個引用計數(shù)器荡碾,每當(dāng)有一個地方引用它時谨读,計數(shù)器值就加一;當(dāng)引用失效時坛吁,計數(shù)器值就減一劳殖;任何時刻計數(shù)器為零的對象就是不可能再被使用的。
- 互相循環(huán)引用的對象無法被回收
可達(dá)性分析法
- 通過一系列稱為“GC Roots”的根對象作為起始節(jié)點集拨脉,從這些節(jié)點開始哆姻,根據(jù)引用關(guān)系向下搜索,搜索過程所走過的路徑稱為“引用鏈”(Reference Chain)玫膀,如果某個對象到GC Roots間沒有任何引用鏈相連矛缨,或者用圖論的話來說就是從GC Roots到這個對象不可達(dá)時,則證明此對象是不可能再被使用的
- 可作為GC Roots的對象:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象
- 在方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 在本地方法棧中JNI(即通常所說的Native方法)引用的對象
- Java虛擬機(jī)內(nèi)部的引用帖旨,如基本數(shù)據(jù)類型對應(yīng)的Class對象箕昭,一些常駐的異常對象(比如NullPointExcepiton、OutOfMemoryError)等解阅,還有系統(tǒng)類加載器
- 所有被同步鎖(synchronized關(guān)鍵字)持有的對象
- 反映Java虛擬機(jī)內(nèi)部情況的JMXBean落竹、JVMTI中注冊的回調(diào)、本地代碼緩存等
四種引用
- 強(qiáng)引用(Strongly Re-ference):無論任何情況下货抄,只要強(qiáng)引用關(guān)系還存在筋量,垃圾收集器就永遠(yuǎn)不會回收掉被引用的對象
- 軟引用(Soft Reference):只被軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常前碉熄,會把這些對象列進(jìn)回收范圍之中進(jìn)行第二次回收,如果這次回收還沒有足夠的內(nèi)存肋拔,才會拋出內(nèi)存溢出異常
- 弱引用(Weak Reference):當(dāng)垃圾收集器開始工作锈津,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象
- 虛引用(Phantom Reference):為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的只是為了能在這個對象被收集器回收時收到一個系統(tǒng)通知( 虛引用必須與ReferenceQueue一起使用凉蜂,當(dāng)GC準(zhǔn)備回收一個對象琼梆,如果發(fā)現(xiàn)它還有虛引用,就會在回收之前窿吩,把這個虛引用加入到與之關(guān)聯(lián)的ReferenceQueue中)
宣告對象死亡
- 要真正宣告一個對象死亡茎杂,至少要經(jīng)歷兩次標(biāo)記過程
- 首先對象不可達(dá),它將會被第一次標(biāo)記纫雁,隨后進(jìn)行一次篩選煌往,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。
- 如果這個對象被判定為確有必要執(zhí)行finalize()方法,那么該對象將會被放置在一個名為F-Queue的隊列之中刽脖,并在稍后由一條由虛擬機(jī)自動建立的羞海、低調(diào)度優(yōu)先級的Finalizer線程去執(zhí)行它們的finalize()方法。
- 如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關(guān)聯(lián)即可
- 如果第二次標(biāo)記時對象沒有逃脫曲管,那基本上它就真的要被回收了
垃圾收集算法
標(biāo)記-清除算法(Mark-Sweep)
- 首先標(biāo)記出所有需要回收的對象却邓,在標(biāo)記完成后,統(tǒng)一回收掉所有被標(biāo)記的對象院水,也可以反過來腊徙,標(biāo)記存活的對象,統(tǒng)一回收所有未被標(biāo)記的對象檬某。
- 缺點:1撬腾、執(zhí)行效率不穩(wěn)定 2、 是內(nèi)存空間的碎片化問題
標(biāo)記-復(fù)制算法(Semispace Copying)
- 將可用內(nèi)存按容量劃分為大小相等的兩塊橙喘,每次只使用其中的一塊时鸵。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面厅瞎,然后再把已使用過的內(nèi)存空間一次清理掉饰潜。解決內(nèi)存碎片化問題。
- 缺點:空間浪費
標(biāo)記-整理算法
- 標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣和簸,然后讓所有存活的對象都向內(nèi)存空間一端移動彭雾,然后直接清理掉邊界以外的內(nèi)存。
- 這種對象移動操作必須全程暫停用戶應(yīng)用程序才能進(jìn)行锁保,被最初的虛擬機(jī)設(shè)計者形象地描述為“Stop The World”
分代收集
在Java堆劃分出不同的區(qū)域之后薯酝,垃圾收集器才可以每次只回收其中某一個或者某些部分的區(qū)域
——因而才有了“Minor GC”“Major GC”“Full GC”這樣的回收類型的劃分;也才能夠針對不同的區(qū)域安排與里面存儲對象存亡特征相匹配的垃圾收集算法爽柒。
- 新生代收集(Minor GC/Young GC):指目標(biāo)只是新生代的垃圾收集吴菠。
- 老年代收集(Major GC/Old GC):指目標(biāo)只是老年代的垃圾收集。
- 混合收集(Mixed GC):指目標(biāo)是收集整個新生代以及部分老年代的垃圾收集浩村。目前只有G1收集器會有這種行為做葵。
- 整堆收集(Full GC):收集整個Java堆和方法區(qū)的垃圾收集
Hotspot虛擬機(jī) 堆內(nèi)存劃分
內(nèi)存分配與回收策略
- 對象優(yōu)先在Eden分配:大多數(shù)情況下,對象在新生代Eden區(qū)中分配心墅。當(dāng)Eden區(qū)沒有足夠空間進(jìn)行分配時酿矢,虛擬機(jī)將發(fā)起一次Minor GC
- 大對象直接進(jìn)入老年代:大對象就是指需要大量連續(xù)內(nèi)存空間的Java對象,最典型的大對象便是那種很長的字符串怎燥,或者元素數(shù)量很龐大的數(shù)組
- 長期存活的對象將進(jìn)入老年代:虛擬機(jī)給每個對象定義了一個對象年齡(Age)計數(shù)器瘫筐,存儲在對象頭中。對象通常在Eden區(qū)里誕生铐姚,如果經(jīng)過第一次Minor GC后仍然存活策肝,并且能被Survivor容納的話,該對象會被移動到Survivor空間中,并且將其對象年齡設(shè)為1歲驳糯。對象在Survivor區(qū)中每熬過一次Minor GC篇梭,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為15)酝枢,就會被晉升到老年代中
- 動態(tài)對象年齡判定:如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半恬偷,年齡大于或等于該年齡的對象就可以直接進(jìn)入老年代
- 空間分配擔(dān)保:當(dāng) Survivor 空間不足以容納一次 Minor GC 之后存活的對象時,就需要依賴其他內(nèi)存區(qū)域 (實際上大多數(shù)情況下就是老年代) 進(jìn)行分配擔(dān)保帘睦。在發(fā)生 Minor GC 之前袍患,虛擬機(jī)必須先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果這個條件成立竣付,那這一次 Minor GC 可以確保是安全的诡延。如果不成立,則虛擬機(jī)會先查看 - XX:HandlePromotionFailure 參數(shù)的設(shè)置值是否允許擔(dān)保失敗 (Handle Promotion Failure)古胆;如果允許肆良,那會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于逸绎,將嘗試進(jìn)行一次 Minor GC惹恃,盡管這次 Minor GC 是有風(fēng)險的;如果小于棺牧,或者-XX: HandlePromotionFailure設(shè)置不允許冒險巫糙,那這時就要改為進(jìn)行一次 Full GC。
參考資料:
《深入理解JAVA虛擬機(jī)》1-3章