JVM
JVM是Java Virtual Machine(Java虛擬機)的縮寫吏够,JVM是一種用于計算設(shè)備的規(guī)范勾给,它是一個虛構(gòu)出來的計算機滩报,是通過在實際的計算機上仿真模擬各種計算機功能來實現(xiàn)的。Java虛擬機包括一套字節(jié)碼指令集播急、一組寄存器脓钾、一個棧、一個垃圾回收堆和一個存儲方法域桩警。 JVM屏蔽了與具體操作系統(tǒng)平臺相關(guān)的信息可训,使Java程序只需生成在Java虛擬機上運行的目標代碼(字節(jié)碼),就可以在多種平臺上不加修改地運行。JVM在執(zhí)行字節(jié)碼時捶枢,實際上最終還是把字節(jié)碼解釋成具體平臺上的機器指令執(zhí)行握截。
1. 概述
垃圾回收(Garbage Collection谨胞,GC),顧名思義就是釋放垃圾占用的空間蒜鸡,防止內(nèi)存泄露胯努。有效的使用可以使用的內(nèi)存,對內(nèi)存堆中已經(jīng)死亡的或者長時間沒有使用的對象進行清除和回收逢防。
2. 垃圾判斷算法
2.1 引用計數(shù)法
給每個對象添加一個計數(shù)器叶沛,當有地方引用該對象時計數(shù)器加1,當引用失效時計數(shù)器減1忘朝。用對象計數(shù)器是否為0來判斷對象是否可被回收灰署。缺點:無法解決循環(huán)引用的問題。
先創(chuàng)建一個字符串,String m = new String("jack");导狡,這時候 "jack" 有一個引用约巷,就是m。然后將m設(shè)置為null旱捧,這時候 "jack" 的引用次數(shù)就等于 0 了独郎,在引用計數(shù)算法中,意味著這塊內(nèi)容就需要被回收了枚赡。
[圖片上傳失敗...(image-977668-1615294672628)]
引用計數(shù)算法是將垃圾回收分攤到整個應用程序的運行當中了氓癌,而不是在進行垃圾收集時,要掛起整個應用的運行贫橙,直到對堆中所有對象的處理都結(jié)束贪婉。因此,采用引用計數(shù)的垃圾收集不屬于嚴格意義上的Stop-The-World的垃圾收集機制卢肃。
看似很美好疲迂,但我們知道JVM的垃圾回收就是Stop-The-World的才顿,那是什么原因?qū)е挛覀冏罱K放棄了引用計數(shù)算法呢?看下面的例子尤蒿。
public class ReferenceCountingGC {
public Object instance;
public ReferenceCountingGC(String name) {
}
public static void testGC(){
ReferenceCountingGC a = new ReferenceCountingGC("objA");
ReferenceCountingGC b = new ReferenceCountingGC("objB");
a.instance = b;
b.instance = a;
a = null;
b = null;
}
}
復制代碼
我們可以看到郑气,最后這2個對象已經(jīng)不可能再被訪問了,但由于他們相互引用著對方腰池,導致它們的引用計數(shù)永遠都不會為0尾组,通過引用計數(shù)算法,也就永遠無法通知GC收集器回收它們示弓。
2.2 可達性分析算法
通過GC ROOT的對象作為搜索起始點讳侨,通過引用向下搜索,所走過的路徑稱為引用鏈奏属。通過對象是否有到達引用鏈的路徑來判斷對象是否可被回收(可作為GC ROOT的對象:虛擬機棧中引用的對象跨跨,方法區(qū)中類靜態(tài)屬性引用的對象,方法區(qū)中常量引用的對象囱皿,本地方法棧中JNI引用的對象)
[圖片上傳失敗...(image-f7f891-1615294672628)]
通過可達性算法歹叮,成功解決了引用計數(shù)所無法解決的循環(huán)依賴問題,只要你無法與GC Root建立直接或間接的連接铆帽,系統(tǒng)就會判定你為可回收對象。那這樣就引申出了另一個問題德谅,哪些屬于GC Root爹橱。
Java內(nèi)存區(qū)域中可以作為GC ROOT的對象:
虛擬機棧中引用的對象
public class StackLocalParameter {
public StackLocalParameter(String name) {}
public static void testGC() {
StackLocalParameter s = new StackLocalParameter("localParameter");
s = null;
}
}
復制代碼
此時的s,即為GC Root窄做,當s置空時愧驱,localParameter對象也斷掉了與GC Root的引用鏈,將被回收椭盏。
方法區(qū)中類靜態(tài)屬性引用的對象
public class MethodAreaStaicProperties {
public static MethodAreaStaicProperties m;
public MethodAreaStaicProperties(String name) {}
public static void testGC(){
MethodAreaStaicProperties s = new MethodAreaStaicProperties("properties");
s.m = new MethodAreaStaicProperties("parameter");
s = null;
}
}
復制代碼
此時的s组砚,即為GC Root,s置為null掏颊,經(jīng)過GC后糟红,s所指向的properties對象由于無法與GC Root建立關(guān)系被回收。而m作為類的靜態(tài)屬性乌叶,也屬于GC Root盆偿,parameter 對象依然與GC root建立著連接,所以此時parameter對象并不會被回收准浴。
方法區(qū)中常量引用的對象
public class MethodAreaStaicProperties {
public static final MethodAreaStaicProperties m = MethodAreaStaicProperties("final");
public MethodAreaStaicProperties(String name) {}
public static void testGC() {
MethodAreaStaicProperties s = new MethodAreaStaicProperties("staticProperties");
s = null;
}
}
復制代碼
m即為方法區(qū)中的常量引用事扭,也為GC Root,s置為null后乐横,final對象也不會因沒有與GC Root建立聯(lián)系而被回收求橄。
本地方法棧中引用的對象
[圖片上傳失敗...(image-5d9560-1615294672627)]
任何native接口都會使用某種本地方法棧今野,實現(xiàn)的本地方法接口是使用C連接模型的話,那么它的本地方法棧就是C棧罐农。當線程調(diào)用Java方法時条霜,虛擬機會創(chuàng)建一個新的棧幀并壓入Java棧。然而當它調(diào)用的是本地方法時啃匿,虛擬機會保持Java棧不變蛔外,不再在線程的Java棧中壓入新的幀,虛擬機只是簡單地動態(tài)連接并直接調(diào)用指定的本地方法溯乒。
3. 垃圾回收算法
在確定了哪些垃圾可以被回收后裆悄,垃圾收集器要做的事情就是開始進行垃圾回收矛纹,但是這里面涉及到一個問題是:如何高效地進行垃圾回收。這里我們討論幾種常見的垃圾收集算法的核心思想光稼。
3.1 標記-清除算法
標記清除算法(Mark-Sweep)是最基礎(chǔ)的一種垃圾回收算法艾君,它分為2部分采够,先把內(nèi)存區(qū)域中的這些對象進行標記,哪些屬于可回收標記出來冰垄,然后把這些垃圾拎出來清理掉蹬癌。就像上圖一樣,清理掉的垃圾就變成未使用的內(nèi)存區(qū)域虹茶,等待被再次使用逝薪。但它存在一個很大的問題,那就是內(nèi)存碎片蝴罪。
上圖中等方塊的假設(shè)是2M董济,小一些的是1M,大一些的是4M要门。等我們回收完虏肾,內(nèi)存就會切成了很多段。我們知道開辟內(nèi)存空間時暂衡,需要的是連續(xù)的內(nèi)存區(qū)域询微,這時候我們需要一個2M的內(nèi)存區(qū)域,其中有2個1M是沒法用的狂巢。這樣就導致撑毛,其實我們本身還有這么多的內(nèi)存的,但卻用不了。
3.2 復制算法
復制算法(Copying)是在標記清除算法基礎(chǔ)上演化而來,解決標記清除算法的內(nèi)存碎片問題胯杭。它將可用內(nèi)存按容量劃分為大小相等的兩塊驯杜,每次只使用其中的一塊。當這一塊的內(nèi)存用完了做个,就將還存活著的對象復制到另外一塊上面鸽心,然后再把已使用過的內(nèi)存空間一次清理掉。保證了內(nèi)存的連續(xù)可用居暖,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復雜情況顽频。復制算法暴露了另一個問題,例如硬盤本來有500G太闺,但卻只能用200G糯景,代價實在太高。
3.3 標記-整理算法
標記-整理算法標記過程仍然與標記-清除算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理钞澳,而是讓所有存活的對象都向一端移動怠惶,再清理掉端邊界以外的內(nèi)存區(qū)域。
標記整理算法解決了內(nèi)存碎片的問題轧粟,也規(guī)避了復制算法只能利用一半內(nèi)存區(qū)域的弊端甚疟。標記整理算法對內(nèi)存變動更頻繁,需要整理所有存活對象的引用地址逃延,在效率上比復制算法要差很多。一般是把Java堆分為新生代和老年代轧拄,這樣就可以根據(jù)各個年代的特點采用最適當?shù)氖占惴ā?/p>
3.4 分代收集算法
分代收集算法分代收集算法嚴格來說并不是一種思想或理論揽祥,而是融合上述3種基礎(chǔ)的算法思想,而產(chǎn)生的針對不同情況所采用不同算法的一套組合拳檩电,根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊拄丰。
在新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去俐末,只有少量存活料按,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集卓箫。
在老年代中载矿,因為對象存活率高、沒有額外空間對它進行分配擔保烹卒,就必須使用標記-清理算法或者標記-整理算法來進行回收岖圈。
4. 內(nèi)存區(qū)域與回收策略
對象的內(nèi)存分配喂饥,往大方向講雏节,就是在堆上分配(但也可能經(jīng)過JIT編譯后被拆散為標量類型并間接地棧上分配),對象主要分配在新生代的Eden區(qū)上牡整,如果啟動了本地線程分配緩沖,將按線程優(yōu)先在TLAB上分配溺拱。少數(shù)情況下也可能會直接分配在老年代中(大對象直接分到老年代)逃贝,分配的規(guī)則并不是百分百固定的,其細節(jié)取決于當前使用的是哪一種垃圾收集器組合迫摔,還有虛擬機中與內(nèi)存相關(guān)的參數(shù)的設(shè)置沐扳。
4.1 對象優(yōu)先在Eden分配
大多數(shù)情況下,對象會在新生代Eden區(qū)中分配攒菠。當Eden區(qū)沒有足夠空間進行分配時迫皱,虛擬機會發(fā)起一次 Minor GC。Minor GC相比Major GC更頻繁辖众,回收速度也更快卓起。通過Minor GC之后,Eden區(qū)中絕大部分對象會被回收凹炸,而那些存活對象戏阅,將會送到Survivor的From區(qū)(若From區(qū)空間不夠,則直接進入Old區(qū)) 啤它。
4.2 Survivor區(qū)
Survivor區(qū)相當于是Eden區(qū)和Old區(qū)的一個緩沖奕筐,類似于我們交通燈中的黃燈。Survivor又分為2個區(qū)变骡,一個是From區(qū)离赫,一個是To區(qū)。每次執(zhí)行Minor GC塌碌,會將Eden區(qū)中存活的對象放到Survivor的From區(qū)渊胸,而在From區(qū)中,仍存活的對象會根據(jù)他們的年齡值來決定去向台妆。(From Survivor和To Survivor的邏輯關(guān)系會發(fā)生顛倒: From變To 翎猛, To變From,目的是保證有連續(xù)的空間存放對方接剩,避免碎片化的發(fā)生)
4.2.1 Survivor區(qū)存在的意義
如果沒有Survivor區(qū)切厘,Eden區(qū)每進行一次Minor GC,存活的對象就會被送到老年代懊缺,老年代很快就會被填滿疫稿。而有很多對象雖然一次Minor GC沒有消滅,但其實也并不會蹦跶多久,或許第二次而克,第三次就需要被清除靶壮。這時候移入老年區(qū),很明顯不是一個明智的決定员萍。所以腾降,Survivor的存在意義就是減少被送到老年代的對象,進而減少Major GC的發(fā)生碎绎。Survivor的預篩選保證螃壤,只有經(jīng)歷16次Minor GC還能在新生代中存活的對象,才會被送到老年代筋帖。
4.3 大對象直接進入老年代
所謂大對象是指奸晴,需要大量連續(xù)內(nèi)存空間的Java對象,最典型的大對象就是那種很長的字符串以及數(shù)組日麸。大對象對虛擬機的內(nèi)存分配來說就是一個壞消息寄啼,經(jīng)常出現(xiàn)大對象容易導致內(nèi)存還有不少空間時就提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間來 “安置” 它們。
虛擬機提供了一個XX:PretenureSizeThreshold參數(shù)代箭,令大于這個設(shè)置值的對象直接在老年代分配墩划,這樣做的目的是避免在Eden區(qū)及兩個Survivor區(qū)之間發(fā)生大量的內(nèi)存復制(新生代采用的是復制算法)。
4.4 長期存活的對象將進入老年代
虛擬機給每個對象定義了一個對象年齡(Age)計數(shù)器嗡综,如果對象在Eden出生并經(jīng)過第一次Minor GC后仍然存活乙帮,并且能被Survivor容納的話,將被移動到Survivor空間中(正常情況下對象會不斷地在Survivor的From與To區(qū)之間移動)极景,并且對象年齡設(shè)為1察净。對象在Survivor區(qū)中每經(jīng)歷一次Minor GC,年齡就增加1歲盼樟,當它的年齡增加到一定程度(默認15歲)氢卡,就將會晉升到老年代中。對象晉升老年代的年齡閾值晨缴,可以通過參數(shù) XX:MaxPretenuringThreshold 設(shè)置异吻。
4.5 動態(tài)對象年齡判定
為了能更好地適應不同程度的內(nèi)存狀況,虛擬機并不是永遠地要求對象的年齡必須達到 MaxPretenuringThreshold才能晉升老年代喜庞,如果Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于改年齡的對象就可以直接進入老年代棋返,無需等到MaxPretenuringThreshold中要求的年齡延都。
這其實有點類似于負載均衡,輪詢是負載均衡的一種睛竣,保證每臺機器都分得同樣的請求晰房。看似很均衡,但每臺機的硬件不同殊者,健康狀況不同与境,我們還可以基于每臺機接受的請求數(shù),或每臺機的響應時間等猖吴,來調(diào)整我們的負載均衡算法摔刁。
以上就是有關(guān)JVM的學習筆記,希望可以對大家有所幫助海蔽,喜歡的小伙伴可以幫忙轉(zhuǎn)發(fā)+關(guān)注共屈,感謝大家!LZ也會每天不定時更新干貨党窜!