前言
最近開始看這本書弃鸦,記得前段時間拿起這本書的時候绞吁,心情是相當(dāng)沉重的!當(dāng)時的劇本是這樣的——
內(nèi)景唬格。家里 - 下午
我(畫外):唉家破,有點(diǎn)無聊啊9焊凇(偶然撇過書架)這么多書得看到什么時候啊汰聋,要不要拿一本翻翻呢?但是在家里好像有點(diǎn)看不下去啊喊积,是太安逸了嗎烹困?最近那本《圖解 HTTP》也還沒看完,感覺暫時有點(diǎn)不想看了乾吻。(走到書架前)還是挑幾本優(yōu)先級比較高的帶到███下班的時候看吧髓梅。(沉思)嗯,這本帶過去~
當(dāng)我拿起《深入理解 Java 虛擬機(jī)》這本書的那一刻绎签,心里咯噔一下——唉枯饿,PM10 濃度又上升了,地球環(huán)境越來越差了啊诡必,萬惡的地球人奢方!
正文
一、運(yùn)行時數(shù)據(jù)區(qū)域
Java 虛擬機(jī)在執(zhí)行 Java 程序時,會把它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域蟋字。這些區(qū)域都有各自的用途稿蹲,以及創(chuàng)建和銷毀時間。
1愉老、程序計(jì)數(shù)器
- 是一塊較小的內(nèi)存空間场绿,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。在虛擬機(jī)的概念模型里嫉入,字節(jié)碼解釋器就是通過改變這個計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令焰盗。
- 線程私有:為了線程切換后能恢復(fù)到正確的執(zhí)行位置,因此每條線程都需要有一個獨(dú)立的程序計(jì)數(shù)器咒林。
- 唯一一個不會出現(xiàn) OutOfMemoryError 異常的區(qū)域熬拒。
2、Java 虛擬機(jī)棧
- 虛擬機(jī)棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:Java 方法在執(zhí)行時會創(chuàng)建一個棧幀垫竞,用于存儲局部變量表澎粟、操作數(shù)棧、動態(tài)鏈接欢瞪、方法出口等信息活烙。每個方法從調(diào)用到執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中入棧到出棧的過程遣鼓。
- 線程私有啸盏。
- 會出現(xiàn) StackOverflowError 和 OutOfMemoryError 異常。
- StackOverflowError:線程請求的棧深度大于虛擬機(jī)所允許的深度骑祟,將拋出該異常回懦。
- OutOfMemoryError:虛擬機(jī)棧動態(tài)擴(kuò)展時無法申請到足夠的內(nèi)存,將拋出該異常次企。
3怯晕、本地方法棧
- 作用與虛擬機(jī)棧相似,只不過虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法(字節(jié)碼)服務(wù)缸棵,而本地方法棧為虛擬機(jī)執(zhí)行 Native 方法服務(wù)舟茶。
- 線程私有。
- 會出現(xiàn) StackOverflowError 和 OutOfMemoryError 異常堵第。
4稚晚、Java 堆
- Java 虛擬機(jī)所管理的內(nèi)存中最大的一塊,用于存放對象實(shí)例型诚。它是垃圾收集器管理的主要區(qū)域,也被稱為"GC堆”鸳劳。
- 可細(xì)分為新生代和老年代狰贯,新生代又可細(xì)分為 Eden 空間、From Survivor 空間、To Survivor 空間涵紊。
- 線程共享傍妒。
- 會出現(xiàn) OutOfMemoryError 異常。
5摸柄、方法區(qū)
- 用于存儲已被虛擬機(jī)加載的類信息颤练、常量、靜態(tài)變量驱负、即時編譯器編譯后的代碼等數(shù)據(jù)嗦玖。別名 Non-Heap(非堆)。
- 也被稱為“永久代”跃脊,因?yàn)?HotSpot 虛擬機(jī)使用永久代來實(shí)現(xiàn)方法區(qū)宇挫,但本質(zhì)上兩者并不等價。
PS:JDK1.8 已經(jīng)徹底移除了永久代酪术,改用元空間實(shí)現(xiàn)方法區(qū)器瘪。元空間使用的是直接內(nèi)存。 - 線程共享绘雁。
- 會出現(xiàn) OutOfMemoryError 異常橡疼。
6、運(yùn)行時常量池
- 是方法區(qū)的一部分庐舟,用于存放編譯期生成的各種字面量和符號引用欣除。
PS:JKD1.7 已經(jīng)從方法區(qū)移到了 Java 堆中。 - 線程共享继阻。
- 會出現(xiàn) OutOfMemoryError 異常耻涛。
7、直接內(nèi)存
- 不是虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的一部分瘟檩,也不是 Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域抹缕。但是這部分內(nèi)存也被頻繁使用。
- 會出現(xiàn) OutOfMemoryError 異常墨辛。
二卓研、HotSpot 虛擬機(jī)對象探秘
1、對象的創(chuàng)建
類加載檢查 -> 分配內(nèi)存 -> 初始化零值 -> 設(shè)置對象頭 -> 執(zhí)行 init 方法
(1)類加載檢查
虛擬機(jī)遇到 new 指令時睹簇,會先檢查這個指令的參數(shù)能否在常量池中定位到一個類的符號引用奏赘,以及這個符號引用代表的類是否已被加載、解析和初始化過太惠。如果沒有磨淌,就必須先執(zhí)行相應(yīng)的類加載過程。
(2)分配內(nèi)存
對象所需內(nèi)存的大小在類加載完成后便可確定凿渊,為對象分配內(nèi)存空間等同于把一塊確定大小的內(nèi)存從 Java 堆中劃分出來梁只。
分配內(nèi)存的兩種方式:
- 指針碰撞: Java 堆中內(nèi)存規(guī)整時缚柳,將用過的內(nèi)存放在一邊,空閑的內(nèi)存放在另一邊搪锣,中間放一個指針作為分界點(diǎn)的指示器秋忙。分配內(nèi)存時,只需把那個指針向空閑內(nèi)存那邊构舟,移動一段與對象大小相等的距離即可灰追。
- 空閑列表: Java 堆中內(nèi)存不規(guī)整時,虛擬機(jī)通過維護(hù)一個列表狗超,記錄哪些內(nèi)存塊是可用的弹澎。在分配時從列表中找出一塊足夠大的空間劃分給對象實(shí)例,并更新列表上的記錄抡谐。
Java 堆是否規(guī)整(是否有內(nèi)存碎片)裁奇,由所采用垃圾收集器的算法所決定÷竽欤“標(biāo)記-清除”算法會產(chǎn)生內(nèi)存碎片刽肠,而“標(biāo)記-整理”和復(fù)制算法則不會。
如何保證分配內(nèi)存的線程安全:
- CAS 同步機(jī)制:采用 CAS 配上失敗重試的方式保證更新操作的原子性免胃。
- 本地線程分配緩沖(TLAB):每個線程在 Java 堆中預(yù)先分配一小塊內(nèi)存(TLAB)音五,線程要分配內(nèi)存時,先在 TLAB 上分配羔沙,TLAB 用完后再采用 CAS 同步機(jī)制進(jìn)行分配躺涝。
(3)初始化零值
將分配到的內(nèi)存空間初始化為零值(不包括對象頭),保證對象的實(shí)例字段在 Java 代碼中可以不賦初始值就直接使用扼雏。
(4)設(shè)置對象頭
虛擬機(jī)需要對對象進(jìn)行必要的設(shè)置坚嗜,例如這個對象是哪個類的實(shí)例、如何找到類的元數(shù)據(jù)信息诗充、對象的哈希碼苍蔬、對象的 GC 分代年齡等。這些信息存放在對象的對象頭中蝴蜓。
(5)執(zhí)行 init 方法
把對象按照程序員的意愿進(jìn)行初始化碟绑。
2、對象的內(nèi)存布局
HotSpot 虛擬機(jī)中茎匠,對象在內(nèi)存中存儲的布局可分為 3 塊區(qū)域:對象頭格仲、實(shí)例數(shù)據(jù)和對齊填充。
(1)對象頭
對象頭包含兩部分信息:
- Mark Word:用于存儲對象自身的運(yùn)行時數(shù)據(jù)诵冒,如哈希碼凯肋、GC 分代年齡、鎖狀態(tài)標(biāo)志汽馋、線程持有的鎖侮东、偏向線程 ID午笛、偏向時間戳等。
- 類型指針:對象指向它的類元數(shù)據(jù)的指針苗桂,虛擬機(jī)通過這個指針來確定對象是哪個類的實(shí)例。
(2)實(shí)例數(shù)據(jù)
對象真正存儲的有效信息告组,也是在程序代碼中所定義的各種類型的字段內(nèi)容煤伟。
(3)對齊填充
僅僅起著占位符的作用,不是必然存在的木缝,也沒有特別的含義便锨。
由于 HotSpot 虛擬機(jī)的自動內(nèi)存管理系統(tǒng),要求對象起始地址必須是 8 字節(jié)的整倍數(shù)我碟,換句話說放案,對象的大小必須是 8 字節(jié)的整倍數(shù)。而對象頭部分正好是 8 字節(jié)的整倍數(shù)矫俺,因此吱殉,當(dāng)對象實(shí)例數(shù)據(jù)部分沒有對齊時,就需要通過對齊填充來補(bǔ)全厘托。
3友雳、對象的訪問定位
Java 程序需要通過棧上的 reference 數(shù)據(jù)來訪問堆上的具體對象。目前主流的訪問方式有句柄和直接指針兩種铅匹。
(1)句柄
- reference 中存儲的是對象的句柄地址押赊。
- Java 堆中劃分出一塊內(nèi)存作為句柄池,句柄中包含了對象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息包斑。
- 好處:reference 中存儲的是穩(wěn)定的句柄地址流礁,在對象被移動時只會改變句柄中的實(shí)例數(shù)據(jù)指針,reference 本身不需要修改罗丰。
(2)直接指針
- reference 中存儲的直接就是對象的地址神帅。
- Java 堆對象的布局必須考慮如何放置類型數(shù)據(jù)的具體地址信息。
- 好處:節(jié)省了一次指針定位的時間開銷丸卷,速度更快枕稀。
三、OutOfMemoryError 異常
Java 虛擬機(jī)中谜嫉,除了程序計(jì)數(shù)器外萎坷,其他幾個運(yùn)行時區(qū)域都有發(fā)生 OutOfMemoryError(OOM)異常的可能。
1沐兰、Java 堆溢出
異常堆棧信息:java.lang.OutOfMemoryError: Java heap space哆档。
異常原因:內(nèi)存泄露、內(nèi)存溢出住闯。
- 內(nèi)存泄露:存在 GC 無法回收的對象瓜浸。
- 內(nèi)存溢出:堆中存活對象過多澳淑。
異常處理:
- 通過工具查看泄露對象到 GC Roots 的引用鏈,從而定位出泄露代碼的位置插佛。
- 調(diào)大堆參數(shù)(-Xmx杠巡、-Xms),例:
-Xmx256m -Xms128m
雇寇。 - 檢查代碼中是否存在對象生命周期過長的情況氢拥。
2、虛擬機(jī)棧和本地方法棧溢出
異常堆棧信息:java.lang.OutOfMemoryError: unable to create new native thread锨侯。
異常原因:創(chuàng)建線程過多嫩海。
- 操作系統(tǒng)分配給每個進(jìn)程的內(nèi)存是有限制的,因此每個線程分配到的棧容量越大(棧是線程私有的)囚痴,可創(chuàng)建的線程數(shù)量就越少叁怪,創(chuàng)建線程時就越容易把剩下的內(nèi)存耗盡。
異常處理:
- 減少線程數(shù)深滚。
- 更換 64 位虛擬機(jī)奕谭。
- 減少最大堆容量(-Xmx)。
- 減少棧容量(-Xss)成箫,例:
-Xss128k
展箱。
3、方法區(qū)和運(yùn)行時常量池溢出
異常堆棧信息:java.lang.OutOfMemoryError: PermGen space蹬昌。
異常原因:載入內(nèi)存的類混驰、常量過多。
異常處理:調(diào)大方法區(qū)容量(-XX:PermSize皂贩、-XX:MaxPermSize)栖榨,例:-XX:PermSize=64m -XX:MaxPermSize=128m
。
4明刷、本機(jī)直接內(nèi)存溢出
異常堆棧信息:java.lang.OutOfMemoryError: Direct buffer memory婴栽。
異常原因:使用了 NIO 等用到直接內(nèi)存的技術(shù)時就有可能出現(xiàn)。
異常處理:調(diào)大直接內(nèi)存容量(-XX:MaxDirectMemorySize)辈末,例:-XX:MaxDirectMemorySize=512m
愚争。