1. 大概內(nèi)容
- 內(nèi)存區(qū)域
- 內(nèi)存泄漏和內(nèi)存溢出
- 類(lèi)型擦除
- 對(duì)象創(chuàng)建,分配和訪問(wèn)
- GC的判定
- GC實(shí)現(xiàn)方法
- 類(lèi)加載過(guò)程
- 雙親委派模型
- 分派
- GC收集器
2. 內(nèi)存結(jié)構(gòu)介紹
我們?cè)贘ava開(kāi)發(fā)時(shí)經(jīng)常會(huì)遇到OutOfMemory的錯(cuò)誤,那我們有時(shí)候會(huì)不清楚問(wèn)題在哪里,需要花很大力氣調(diào)試;我們?cè)陂_(kāi)發(fā)時(shí)需要設(shè)置JVM參數(shù),那么,我們就只有在了解了JVM的內(nèi)存結(jié)構(gòu)之后,才能更好的幫助我們進(jìn)行Java開(kāi)發(fā)。
首先,JVM的內(nèi)存結(jié)構(gòu)主要分為三個(gè)最主要的部分:堆,方法區(qū)和棧,其中堆負(fù)責(zé)存放對(duì)象實(shí)例,是虛擬機(jī)內(nèi)存中最大的一部分;方法區(qū)存儲(chǔ)類(lèi)信息、常量、靜態(tài)變量等數(shù)據(jù);棧分為java虛擬機(jī)棧和本地方法棧主要用于方法的執(zhí)行。下面詳細(xì)介紹一下JVM內(nèi)存各部分的作用:
-
堆
- 對(duì)于大多數(shù)應(yīng)用來(lái)說(shuō),Java堆(Java Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。
- 用途:
- 存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存。
- 垃圾收集:
- Java堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱(chēng)做“GC堆”。
- 如果從內(nèi)存回收的角度看,由于現(xiàn)在收集器基本都是采用的分代收集算法,所以Java堆中還可以細(xì)分為:新生代和老年代;再細(xì)致一點(diǎn)的有Eden空間、From Survivor空間、To Survivor空間等。
- 所有的對(duì)象在實(shí)例化后的整個(gè)運(yùn)行周期內(nèi),都被存放在堆內(nèi)存中。堆內(nèi)存又被劃分成不同的部分:伊甸區(qū)(Eden),幸存者區(qū)域(Survivor Sapce),老年代(Old Generation Space)
- 物理上不連續(xù),邏輯連續(xù);可固定大小,也可擴(kuò)展(通過(guò)-Xmx和-Xms控制)。
- 異常情況:
- 如果在堆中沒(méi)有內(nèi)存完成實(shí)例分配,并且堆也無(wú)法再擴(kuò)展時(shí),將會(huì)拋出OutOfMemoryError異常。
-
方法區(qū)
- 與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域
- 用途:
- 存儲(chǔ)已被虛擬機(jī)加載的類(lèi)信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)
- 雖然規(guī)范中看作堆的邏輯部分,但被稱(chēng)為Non-heap
- 性質(zhì):前兩點(diǎn)與堆相同
- 物理不連續(xù),邏輯連續(xù)
- 可擴(kuò)展
- 可以選擇不實(shí)現(xiàn)垃圾收集
- 內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類(lèi)型的卸載
-
Java虛擬機(jī)棧
- 線程私有的,生命周期與線程相同,與前兩者有明顯區(qū)別
- 用途:
- 記錄方法執(zhí)行的信息,每個(gè)方法在執(zhí)行時(shí)會(huì)創(chuàng)建一個(gè)棧幀
- 存儲(chǔ)方法中的局部變量表、操作棧、動(dòng)態(tài)鏈接、方法出口等信息
- 每一個(gè)方法被調(diào)用直至執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過(guò)程。
- 異常情況:
- 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;
- 如果虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展(當(dāng)前大部分的Java虛擬機(jī)都可動(dòng)態(tài)擴(kuò)展,只不過(guò)Java虛擬機(jī)規(guī)范中也允許固定長(zhǎng)度的虛擬機(jī)棧),當(dāng)擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存時(shí)會(huì)拋出OutOfMemoryError異常。
-
本地方法棧
- 與虛擬機(jī)棧作用基本相同,只是面向本地方法進(jìn)行棧幀的管理
- 與虛擬機(jī)棧相同,會(huì)產(chǎn)生StackOverflowError和OutOfMemoryError異常
-
程序計(jì)數(shù)器
- 一塊較小的內(nèi)存,線程私有
- 用途:
- 當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器
- 字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴(lài)這個(gè)計(jì)數(shù)器來(lái)完成。
- 為了線程切換,程序可以執(zhí)行正確的指令,需要程序計(jì)數(shù)器來(lái)記錄
- 此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError情況的區(qū)域。
實(shí)例說(shuō)明:
import java.text.SimpleDataFormat;
import java.util.Date;
import org.apache.log4j.Logger;
public class HelloWorld{
private static Logger LOGGER = Logger.getLogger(HelloWorld.class.getName());
public void sayHello(String Hello){
SimpleDataFormat formatter = new SimpleDataFormat("dd.MM.YYYY");
String today = formatter.format(new Date());
LOGGER.info(today + " : "+messeage);
}
}
? 在上面的實(shí)例中,數(shù)據(jù)在內(nèi)存中的存放如下:
| 內(nèi)存位置 | 存放數(shù)據(jù) |
|---|---|
| 堆 | Object: HelloWorld<br />Object: SimpleDataFormat<br />Object: String<br />Object:Logger |
| 方法區(qū) | Class: HelloWorld<br />Class: Logger<br />Class: SimpleDataFormat |
| JVM棧 | Parameter Ref: String "message"<br />Variable Ref: formatter<br />local primitive: "lineNo" |
2018-05-26更新
3.垃圾回收器
由于堆中的對(duì)象和方法區(qū)中所占用的內(nèi)存,既不能像程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧都是隨線程而生隨線程而滅,棧幀隨著方法的進(jìn)入和退出做入棧和出棧操作,實(shí)現(xiàn)了自動(dòng)的內(nèi)存清理。因此,需要進(jìn)行垃圾回收。
-
對(duì)象存活判斷
引用計(jì)數(shù):每個(gè)對(duì)象有一個(gè)引用計(jì)數(shù)屬性,新增一個(gè)引用時(shí)計(jì)數(shù)加1,引用釋放時(shí)計(jì)數(shù)減1,計(jì)數(shù)為0時(shí)可以回收。此方法簡(jiǎn)單,無(wú)法解決對(duì)象相互循環(huán)引用的問(wèn)題。
-
可達(dá)性判斷:從GC Roots開(kāi)始向下搜索,搜索所走過(guò)的路徑稱(chēng)為引用鏈。當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的。不可達(dá)對(duì)象。
其中GC Root包括:
- 虛擬機(jī)棧中引用的對(duì)象
- 方法區(qū)中類(lèi)靜態(tài)屬性實(shí)體引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- 本地方法棧中JNI引用的對(duì)象
-
垃圾收集算法
- 標(biāo)記-清除算法
- 首先標(biāo)記需要回收的對(duì)象,然后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象
- 后續(xù)的收集算法都是基于這種思路并對(duì)其缺點(diǎn)進(jìn)行改進(jìn)而得到的
- 缺點(diǎn):1. 效率問(wèn)題,標(biāo)記和清除過(guò)程的效率都不高;2. 空間問(wèn)題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,會(huì)導(dǎo)致不能找到足夠大的內(nèi)存分配較大對(duì)象
- 復(fù)制算法
- 它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。
- 優(yōu)點(diǎn):內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況
- 缺點(diǎn):將內(nèi)存縮小為原來(lái)的一半,持續(xù)復(fù)制長(zhǎng)生存期的對(duì)象則導(dǎo)致效率降低;可能執(zhí)行較多的復(fù)制操作,影響效率
- 標(biāo)記-壓縮算法
- 首先標(biāo)記需要回收的對(duì)象,然后讓所有存活的對(duì)象都向一端移動(dòng),直接清理掉端邊界以外的內(nèi)存
- 提高清理效率
- 分代收集算法
- GC分代的基本假設(shè):絕大部分對(duì)象的生命周期都非常短暫,存活時(shí)間短。
- “分代收集”(Generational Collection)算法,把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴?。在新生代中,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象存活率高、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記-清理”或“標(biāo)記-整理”算法來(lái)進(jìn)行回收。
- 標(biāo)記-清除算法
-
幾種垃圾收集器
- Serial收集器
- 串行收集器:新生代,老年代都穿行;新生代復(fù)制算法、老年代標(biāo)記-壓縮;穩(wěn)定,效率高;可能產(chǎn)生較長(zhǎng)停頓
- ParNew:新生代并行,老年代串行;新生代復(fù)制算法、老年代標(biāo)記-壓縮
- Parallel收集器
- 類(lèi)似ParNew收集器
- 更關(guān)注系統(tǒng)的吞吐量
- 動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或最大的吞吐量
- 也可以通過(guò)參數(shù)控制GC的時(shí)間
- 新生代復(fù)制算法、老年代標(biāo)記-壓縮
- Parallel Old收集器
- 老年代并行
- CMS收集器
一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器
基于“標(biāo)記-清除”算法
初始標(biāo)記 -> 并發(fā)標(biāo)記 -> 重新標(biāo)記 -> 并發(fā)清除
其中初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要“Stop The World”。初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快,并發(fā)標(biāo)記階段就是進(jìn)行GC Roots Tracing的過(guò)程,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間一般會(huì)比初始標(biāo)記階段稍長(zhǎng)一些,但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短。
優(yōu)點(diǎn): 并發(fā)收集、低停頓
缺點(diǎn): 產(chǎn)生大量空間碎片、并發(fā)階段會(huì)降低吞吐量
- G1收集器
- 空間整合,G1收集器采用標(biāo)記整理算法,不會(huì)產(chǎn)生內(nèi)存空間碎片。分配大對(duì)象時(shí)不會(huì)因?yàn)闊o(wú)法找到連續(xù)空間而提前觸發(fā)下一次GC
- 可預(yù)測(cè)停頓,這是G1的另一大優(yōu)勢(shì),降低停頓時(shí)間是G1和CMS的共同關(guān)注點(diǎn),但G1除了追求低停頓外,還能建立可預(yù)測(cè)的停頓時(shí)間模型,能讓使用者明確指定在一個(gè)長(zhǎng)度為N毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過(guò)N毫秒
- Serial收集器
2018-6-16更新
JVM類(lèi)加載機(jī)制
-
什么是類(lèi)的加載?
- 類(lèi)的加載指的是將類(lèi)的.class文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存中,將其放在運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個(gè) java.lang.Class對(duì)象,用來(lái)封裝類(lèi)在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。類(lèi)的加載的最終產(chǎn)品是位于堆區(qū)中的 Class對(duì)象, Class對(duì)象封裝了類(lèi)在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且向Java程序員提供了訪問(wèn)方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口。
- 類(lèi)加載器并不需要等到某個(gè)類(lèi)被“首次主動(dòng)使用”時(shí)再加載它,JVM規(guī)范允許類(lèi)加載器在預(yù)料某個(gè)類(lèi)將要被使用時(shí)就預(yù)先加載它,如果在預(yù)先加載的過(guò)程中遇到了.class文件缺失或存在錯(cuò)誤,類(lèi)加載器必須在程序首次主動(dòng)使用該類(lèi)時(shí)才報(bào)告錯(cuò)誤(LinkageError錯(cuò)誤)如果這個(gè)類(lèi)一直沒(méi)有被程序主動(dòng)使用,那么類(lèi)加載器就不會(huì)報(bào)告錯(cuò)誤
-
類(lèi)的生命周期
加載 - 驗(yàn)證 - 準(zhǔn)備 - 解析 - 初始化 - 使用 - 卸載(其中加載 驗(yàn)證 準(zhǔn)備 初始化的順序是一定的,但是解析則不一定,這是為了支持java語(yǔ)言的運(yùn)行時(shí)綁定)
-
加載
- 查找并加載類(lèi)的二進(jìn)制數(shù)據(jù)加載時(shí)類(lèi)加載過(guò)程的第一個(gè)階段,在加載階段,虛擬機(jī)需要完成以下三件事情:(1)通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取其定義的二進(jìn)制字節(jié)流;(2)將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。(3)在Java堆中生成一個(gè)代表這個(gè)類(lèi)的 java.lang.Class對(duì)象,作為對(duì)方法區(qū)中這些數(shù)據(jù)的訪問(wèn)入口。
-
連接
- 驗(yàn)證:連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。驗(yàn)證階段大致會(huì)完成4個(gè)階段的檢驗(yàn)動(dòng)作:(1)文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范;例如:是否以 0xCAFEBABE開(kāi)頭、主次版本號(hào)是否在當(dāng)前虛擬機(jī)的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類(lèi)型;(2)元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析(注意:對(duì)比javac編譯階段的語(yǔ)義分析),以保證其描述的信息符合Java語(yǔ)言規(guī)范的要求;例如:這個(gè)類(lèi)是否有父類(lèi),除了 java.lang.Object之外。(3)字節(jié)碼驗(yàn)證:通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的;(4)符號(hào)引用驗(yàn)證:確保解析動(dòng)作能正確執(zhí)行。
- 準(zhǔn)備:為類(lèi)的 靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值
- 解析:把類(lèi)中的符號(hào)引用轉(zhuǎn)換為直接引用,解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程,解析動(dòng)作主要針對(duì)類(lèi)或接口、字段、類(lèi)方法、接口方法、方法類(lèi)型、方法句柄和調(diào)用點(diǎn)限定符7類(lèi)符號(hào)引用進(jìn)行。符號(hào)引用就是一組符號(hào)來(lái)描述目標(biāo),可以是任何字面量。
- 初始化:初始化,為類(lèi)的靜態(tài)變量賦予正確的初始值,JVM負(fù)責(zé)對(duì)類(lèi)進(jìn)行初始化,主要對(duì)類(lèi)變量進(jìn)行初始化。在Java中對(duì)類(lèi)變量進(jìn)行初始值設(shè)定有兩種方式:①聲明類(lèi)變量是指定初始值 ②使用靜態(tài)代碼塊為類(lèi)變量指定初始值
- 執(zhí)行順序:構(gòu)造函數(shù)代碼 -> 由類(lèi)入口開(kāi)始的變量初始化或靜態(tài)代碼塊按順序執(zhí)行
- 含有父類(lèi)變量:先執(zhí)行父類(lèi)的初始化,在執(zhí)行子類(lèi)初始化
-
結(jié)束生命周期
- 執(zhí)行了 System.exit()方法;2. 程序正常執(zhí)行結(jié)束;3. 程序在執(zhí)行過(guò)程中遇到了異?;蝈e(cuò)誤而異常終止;4. 由于操作系統(tǒng)出現(xiàn)錯(cuò)誤而導(dǎo)致Java虛擬機(jī)進(jìn)程終止