元空間概念
其實說到這還是要簡單說下java8妓灌、雖然是版本迭代跃洛,但是JAVA8相對于之前來說是個大版本的迭代率触,改了很多東西。首先汇竭,在Java8中葱蝗,永久代已經(jīng)被移除穴张,被一個稱為元空間的取間所取代毒坛。元空間的本質(zhì)和永久代類似憔古。
元空間與永久代最大的區(qū)別在于:永久代使用的JVM的堆內(nèi)存。但是Java8以后的元空間并不在虛擬機中而是使用本機物理內(nèi)存供常。
因此合愈,默認情況下叮贩,元空間的大小僅僅受本地內(nèi)存限制。類的元數(shù)據(jù)放入native memory佛析,字符串池和類的靜態(tài)變量放入java堆中益老,這樣可以加載多少類的元數(shù)據(jù)就不再由MaxPermSize控制,而是由系統(tǒng)的實際可用空間來控制寸莫。
其實JVM的報錯還是有挺多的捺萌,下面我們一個個介紹:
java.lang.StackOverflowError
這個是棧異常。而棧是存方法區(qū)的膘茎。顯示這個異常很容易:死遞歸就可以了:
我們思考一下這個到底是異常還是錯誤:雖然口語上我們一般都說報錯了桃纯,但是其實本質(zhì)上java中的報錯分兩種:Exception異常和Error錯誤。
而幾乎后綴帶Error的都是錯誤披坏。后綴的Exception的都是異常态坦。
java.lang.OutOfMemoryError:java.heap.spack
這個錯誤其實也比較容易理解。如其名內(nèi)存溢出:堆空間棒拂。
而如何實現(xiàn)堆的溢出呢伞梯?這個其實我們上面也測試過。測試弱引用和軟引用gc 的時候創(chuàng)建了大對象帚屉。
java.lang.OutOfMemoryError:GC overhead limit exceeded
這個錯誤的中文翻譯:超出GC開銷限制谜诫。
就是說某一個時刻,GC回收時間過長會拋出這個異常攻旦。過長的指:超過百分之九十八的時間用來做GC喻旷,并且回收了不到百分之二的堆內(nèi)存。連續(xù)GC多次都只回收了不到百分之二的極端情況才會拋出這個異常牢屋。假如不拋出這個異常會產(chǎn)生的情況:很快內(nèi)存滿了且预,繼續(xù)GC,GC又收不到東西烙无,然后又很快滿锋谐,再GC。皱炉。怀估。如此惡行循環(huán)下去狮鸭。所以才會有這個錯誤合搅。下面是代碼的測試:
public static void main(String[] args) throws Exception {
List<String> list = new ArrayList<String>();
int i = 0;
try {
while (true) {
list.add(String.valueOf(i++));
}
} catch (Exception e) {
System.out.println(i);
e.printStackTrace();
// TODO: handle exception
}
}
java.lang.OutOfMemoryError:Direct buffer memory
這個錯誤指直接內(nèi)存掛了多搀。元空間并不在虛擬機中,而是使用本地內(nèi)存灾部。理論上大小僅受硬件大小限制康铭。而這個錯誤的原因是指硬件內(nèi)存受限了。簡而言之總結(jié)為:jvm好好的赌髓,本地的內(nèi)存用光了从藤,導(dǎo)致程序崩潰。
我們常用的i/o ByteBuffer有兩個方法:
ByteBuffer.allocate()是分配JVM堆內(nèi)存锁蠕。屬于GC管轄范圍夷野。由于需要拷貝所以速度相對較慢。
ByteBuffer.allocateDirect()是分配os的本地內(nèi)存荣倾,不屬于GC管轄范圍悯搔。由于不需要內(nèi)存拷貝所以速度相對較快。
而這個報錯就是指物理內(nèi)存的崩盤舌仍,也就是一直用allocateDirect不斷創(chuàng)建對象妒貌。如下代碼:
上圖第一個打印語句是查詢當前可用物理內(nèi)存多大。我這邊打印是5.5M.所以我創(chuàng)建一個6M的對象就直接報錯了铸豁。(這里物理內(nèi)存的大小最好調(diào)小一點灌曙。不然不容易出效果)
java.lang.OutOfMemoryError:unable to create new native thread
這個錯誤的字面意思:不能再創(chuàng)建更多新的本地線程了。
首先這個錯一般都是高并發(fā)的時候報出來的节芥,導(dǎo)致原因:
- 一個應(yīng)用進程創(chuàng)建了太多的線程在刺。超過系統(tǒng)承載極限。
- 服務(wù)器并不允許你的應(yīng)用程序創(chuàng)建這么多線程藏古。linux默認允許單個進程創(chuàng)建的線程數(shù)是1024.
解決辦法:
- 降低應(yīng)用的線程數(shù)量增炭。
- 修改linux的默認配置。
java.lang.OutOfMemoryError:Metaspace
這個也比較好理解:元空間溢出拧晕。元空間是方法區(qū)隙姿。它與永久代最大的區(qū)別就是它在本地內(nèi)存而不是虛擬機內(nèi)存中。下面是demo:
public class OOMTest {
public static void test() {
}
public static void main(String[] args){
int i = 0;
try {
while(true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTest.class);
enhancer.setUseCache(false);
enhancer.setCallback(new org.springframework.cglib.proxy.MethodInterceptor() {
@Override
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
return arg3.invokeSuper(0, args);
}
});
enhancer.create();
i++;
}
} catch (Exception e) {
System.out.println(i);
e.printStackTrace();
}
}
}
利用反射不斷創(chuàng)建類厂捞。然后就爆了输玷。需要注意的是這個默認的元空間大小比較大,想要跑出效果一定要實現(xiàn)把元空間和最大元空間設(shè)置小點靡馁,如下參數(shù):
-XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=5m
以上就是JVM常見報的錯誤欲鹏!下面開始簡單的說一下垃圾回收算法和垃圾回收器。
垃圾回收
首先GC算法(引用計數(shù)/復(fù)制/標記清除/標記整理)是內(nèi)存回收的方法論臭墨,而垃圾收集器是算法的落地實現(xiàn)赔嚎。
目前為止還沒有完美的收集器出現(xiàn),更加沒有萬能的收集器,只能根據(jù)具體應(yīng)用選擇最合適的收集器尤误。
目前主要有四種垃圾收集器(java10以前的侠畔,java11以及以后出來了個新的ZGC。不過因為比較新损晤,所以先不談了):
-
串行垃圾回收器(Serial)
它為單線程環(huán)境設(shè)計且只使用一個線程進行垃圾回收软棺。會暫停所有的用戶線程,所以不適合服務(wù)器環(huán)境尤勋。 -
并行垃圾回收器(Parallel)
多個垃圾收集線程并行工作喘落。此時用戶線程也是暫停的,適用于科學(xué)計算/大數(shù)據(jù)處理等弱交互場景最冰。
串行和并行都需要暫停用戶線程瘦棋,也叫STW(stop the world)。 -
并發(fā)垃圾回收器(CMS)
用戶線程和垃圾收集線程同時執(zhí)行(不一定是并行暖哨,可能是交替執(zhí)行)不需要停頓用戶線程兽狭。互聯(lián)網(wǎng)公司多用它鹿蜀,適用對響應(yīng)時間有要求的場景箕慧。 -
G1垃圾回收器
歷經(jīng)了十年的準備,在java8中開始使用茴恰。G1垃圾收集器將堆內(nèi)存分割成不同的區(qū)域然后并發(fā)的對其進行垃圾回收颠焦。
注意,上面說的是四種垃圾收集器機制往枣,但是落實到實際是有七大垃圾收集器的伐庭。
怎么查看/修改程序的垃圾收集器呢?
其實這個上篇筆記記到了分冈,有一個jvm的命令可以查看圾另,如下:
java -XX:+PrintCommandLineFlags -version
這個命令是查看一些比較重要參數(shù)的,結(jié)果如下圖:
如圖所示雕沉,默認的是并行垃圾回收器集乔。當然了我們也可以去修改這個默認。修改方法就是單純的修改jvm參數(shù)坡椒,也沒啥好說的扰路。而這個我們可以設(shè)置的參數(shù)值有六種(注意上面說有七種垃圾收集器,這里只有六種的原因:有個serialOldGC倔叼,但是因為現(xiàn)在被廢棄了汗唱,都沒人用了。):
- UserSerialGC
- UseParallelGC
- UseConcMarkSweepGC
- UseParNewGC
- UseParallelOldGC
- UseG1GC
下面我們實際測試一下:
首先在啟動的時候添加參數(shù)丈攒,修改垃圾收集器:
然后我們在控制臺查看這個線程:
加號代表的是啟動哩罪,減號代表未啟用授霸。所以說這個參數(shù)是起效果了的。
七大垃圾收集器
重復(fù)一遍:這塊的概念比較容易混淆际插,最上面說的四大垃圾收集器我們可以理解為按照垃圾收集器的原理劃分的绝葡。
而六種垃圾收集器可設(shè)置參數(shù)是因為其中串行老年代這種垃圾收集器已經(jīng)被廢棄了,所以可設(shè)置的參數(shù)就只有六種了腹鹉。
而這里將的七大垃圾收集器是指java中出現(xiàn)過的垃圾收集器,包括已經(jīng)廢棄了的那個敷硅,繼續(xù)往下說功咒。垃圾收集器分兩種,這個也是java的內(nèi)存結(jié)構(gòu)決定的绞蹦、因為分為年輕代和老年代力奋。年輕代朝生夕死,迭代比較快幽七,老年代一般都是比較穩(wěn)定的或者大對象什么的景殷。所以針對這兩個區(qū)域我們要采取的垃圾收集方法也并不一樣。所以同理針對不同區(qū)垃圾收集器也是不同的澡屡。下面是一張圖來粗略的看下垃圾收集器的情況:
其實如其名:我們之前的參數(shù)就可以看出來:old一般都是用在老年區(qū)的猿挚。cms也是用在老年區(qū)的。new就是用在年輕代的驶鹉。而G1比較特殊绩蜻,它是既可以用在老年代,也可以用在年輕代的室埋。
JVM的Server和Client模式
首先在生產(chǎn)環(huán)境下办绝,一般都使用Server模式,Client模式基本不會用姚淆。二者的區(qū)別:
- 32位window系統(tǒng)孕蝉,無論硬件如何都默認使用Client的JVM模式
- 32位其它操作系統(tǒng),2G內(nèi)存同時有2個CPU以上用Server模式腌逢,低于該配置還是Client模式降淮。
- 64位只能是Server模式。(做開發(fā)一定用64位的2取V韪亍!)
下面繼續(xù)說垃圾回收的選擇窍蓝,其實因為垃圾收集器除了G1以外都是一對一對的腋颠,所以有時候我們選擇了新生代,老年代會自動選擇其匹配的吓笙。也就是新生代的垃圾收集器選擇很重要淑玫。
新生代可選的垃圾收集器(不考慮G1):
- 串行GC(Serial)
它是最古老,最穩(wěn)定,效率最高的收集器絮蒿。壞處就是收集過程中需要暫停其他工作線程尊搬。對于限定單核CPU的環(huán)境下(現(xiàn)在怎么可能有單核的服務(wù)器!所以這個收集器也老了)土涝,沒有線程交互的開銷可以獲得最高的收集效率佛寿,因此Serial垃圾收集器是Client模式(低配才會是Client模式)下默認的新生代垃圾收集器。
開啟這個參數(shù)UseSerialGC以后但壮,會默認采用Serial/SerialOld兩種垃圾收集器. - 并行GC(ParNew)
一句話:使用多線程進行垃圾回收冀泻,在垃圾回收的時候也會STW直到它收集結(jié)束。
ParNew其實是Serial收集器新生代的并行多線程版本蜡饵。最常見的就是配合老年代的CMS 工作弹渔,其余的行為了Serial收集器完全一樣。它是很多java虛擬機在Server模式新生代的默認收集器溯祸。
這個開啟這個垃圾回收器的參數(shù)-XX:+UseParNewGC,啟用ParNew收集器肢专,只影響新生代,不影響老年代焦辅。(注意ParNew+SerialOld聯(lián)合使用會JVM報warn博杖,不推薦)。
另外這個ParNew是可以設(shè)置垃圾收集器并行的最大線程數(shù)的筷登。 - 并行回收GC(Parallel Scavenge)
注意欧募,現(xiàn)在常用的默認的垃圾收集器不是上面的并行GC,而是更加進步了仆抵,變成了默認是UseParallelGC跟继。這個代表的是ParallelScavenge。
它是類似ParNew的一個新生代垃圾收集器镣丑,使用復(fù)制算法舔糖,也是一個并行的多線程的垃圾收集器。俗稱吞吐量優(yōu)先收集器莺匠。并行收集器就是串行收集器在新生代和老年代的并行化金吗。
它的重點關(guān)注是:
可控制的吞吐量。吞吐量計算公式= 代碼運行時間/(代碼運行時間+垃圾回收時間)趣竣。高吞吐量意味著高效利用CPU時間摇庙。
自適應(yīng)調(diào)節(jié)策略(相比于ParNew的重要區(qū)別)。虛擬機根據(jù)當前系統(tǒng)的運算情況收集性能監(jiān)控信息遥缕。動態(tài)的調(diào)整這些參數(shù)以提供最合適的停頓時間(-XX:MaxGCPauseMillis)或最大吞吐量卫袒。
這里有個很重點的事項:Parallel和ParallelOld可以互相激活!也就是設(shè)置其中一個另一個也會相應(yīng)生效单匣。
同樣這個也是可以設(shè)置GC收集器的最大線程數(shù)夕凝。參數(shù)如下:
-XX:ParallelGCThreads = N.表示啟動N個GC線程宝穗。
這里有個建議:
- CPU>8, N = 5/8
- CPU<8, N = cpu個數(shù)
老年代可選的垃圾收集器:
- 串行GC
jdk1.6之前默認老年代使用SerialOld。這個沒啥說的码秉,和Serial是一樣的逮矛,只不過是用在老年代而已。而且是CMS收集器的后備收集器转砖。 - 并行GC
這個垃圾收集器和版本關(guān)系很大须鼎。JDK1.6之前新生代用ParallelScavenge老年代用SerialOld
ParallelOld是JDK1.6出現(xiàn)的
JDK1.8及以后默認是ParallelScavenge+ParallelOld - 并發(fā)標記清除GC(CMS)
這個也是比較經(jīng)典的有個垃圾回收算法的落地實現(xiàn)了。標記清除上文提過了府蔗。先標記晋控,再統(tǒng)一清除。好處是速度快礁竞,壞處是會產(chǎn)生內(nèi)存碎片。
這種方式非常適合應(yīng)用在互聯(lián)網(wǎng)或者B/S系統(tǒng)的服務(wù)器上杉辙。這類應(yīng)用尤其重視服務(wù)器響應(yīng)速度模捂,希望停頓時間最短。
CMS非常適合堆內(nèi)存大蜘矢,CPU核數(shù)多的服務(wù)器上狂男,也是G1出現(xiàn)之前大型應(yīng)用的首選收集器。
如果采用這個老年代收集器品腹,會自動將ParNew收集器打開岖食。
開啟該參數(shù)后,會使用ParNew+CMS+SerialOld(作為CMS出錯的后備收集器)組合舞吭。
CMS的標記清除分為四步:
- 初始標記:標記GC Roots能直接關(guān)聯(lián)的對象泡垃,速度很快,但需要暫停所有工作線程羡鸥。
- 并發(fā)標記:進行GC Roots的跟蹤過程蔑穴,和用戶線程一起工作,主要標記過程惧浴,標記全部對象存和。
- 重新標記:修正并發(fā)標記期間,因為程序繼續(xù)運行而導(dǎo)致標記變動的那一部分的標記記錄衷旅。需要暫停所有工作線程捐腿。
- 并發(fā)清除:清除GC Roots不可達對象。和用戶線程一起工作柿顶。不需要暫停工作線程茄袖,基于標記結(jié)果清理對象。
和一次STW相比嘁锯,因為并發(fā)和清除是最耗時的绞佩,都可以和用戶線程一起用寺鸥。所以感覺上停頓時間較短。這也是CMS最大的優(yōu)點:并發(fā)收集停頓低品山。
而CMS的缺點也比較有意思:
由于并發(fā)執(zhí)行胆建,CMS在收集時與應(yīng)用線程同時工作,會增加對堆內(nèi)存的占用肘交。也就是說:CMS必須要在老年代堆內(nèi)存用盡之前完成垃圾回收笆载,否則CMS會回收失敗⊙纳耄回收失敗時會觸發(fā)擔(dān)保機制:也就是之間說的SerialOld凉驻。串行老年代收集器會以STW的方式進行一次GC從而造成較大的停頓時間。
同時因為CMS采用的是標記清除复罐,所以會產(chǎn)生大量的內(nèi)存碎片涝登。
如何選擇合適的垃圾收集器呢?
我們知道了各種垃圾收集器的優(yōu)缺點和運行機制效诅,但是工作中如何選擇合適的垃圾收集組合呢胀滚?
- 單CPU或小內(nèi)存,單機程序 選擇-XX:+UseSerialGC
- 多CPU乱投,需要大量吞吐計算咽笼,如后臺計算型應(yīng)用,選擇 -XX:+UseParallelGC(-XX:+UseParallelOldGC也可以戚炫,這兩者互相啟動的)
-
多CPU剑刑,追求低停頓時間,需要快速響應(yīng)如互聯(lián)網(wǎng)應(yīng)用双肤,選擇
-XX:+UseConcMarkSweepGC和-XX:+ParNewGC
各個收集器使用的算法
G1垃圾收集器
G1不同于上面說的那六種收集器施掏。而是全新的一種模式。以前的收集器有以下特點:
- 年輕代和老年代是各自獨立且連續(xù)的內(nèi)存塊茅糜。
- 年輕代收集使用eden+from+to進行復(fù)制算法
- 老年代手機必須掃描整個老年代區(qū)域其监。
- 都是以盡可能少而快速的執(zhí)行GC為設(shè)計原則。
而G1是一款面向服務(wù)端應(yīng)用的收集器限匣。應(yīng)用在處理多處理器和大容量內(nèi)存環(huán)境中抖苦,在實現(xiàn)高吞吐量的同時,盡可能的滿足收集器暫停時間的要求米死。它的特性:
- 和CMS一樣可以與應(yīng)用線程并發(fā)執(zhí)行锌历。
- 整理空閑空間更快。
- 需要更多的時間來預(yù)測GC停頓時間峦筒。
- 不希望犧牲大量的吞吐性能究西。
- 不需要更大的Java heap。
G1收集器的設(shè)計目標就是取代DMS收集器物喷。它與CMS相比因為是標整卤材,不會產(chǎn)生很多內(nèi)存碎片遮斥,而且STW更可控,G1的停頓時間是有預(yù)測機制的扇丛,所以用戶可以指定期望停頓時間术吗。
G1是2012年,jdk1.7u4版本出現(xiàn)的帆精。而jdk9中较屿,G1變成了默認的垃圾收集器代替了CMS。G1的特點:
- G1充分利用多CPU,多核環(huán)境硬件優(yōu)勢卓练,盡量縮短STW隘蝎。
- G1整體上采用標記整理算法,局部是通過復(fù)制算法襟企,不會產(chǎn)生內(nèi)存碎片嘱么。
- 宏觀上看G1之中不再區(qū)分年輕代和老年代,把內(nèi)存劃分成多個獨立的子區(qū)域(Region)顽悼。可以近似理解為一個魔方面或者棋盤曼振。
- G1收集器里面將整個內(nèi)存都混合在一起了。但其本身依然在小范圍內(nèi)要進行年輕代和老年代的區(qū)分表蝙。保留了新生代和老年代拴测。但它們不再是物理隔離的乓旗,而是一部分子區(qū)域的集合且不要求是連續(xù)的府蛇。也就是說依然會采用不同的GC方式來處理不同區(qū)域。
- G1雖然也是分代收集器屿愚,但整個內(nèi)存分區(qū)不存在物理上的年輕代和老年代的區(qū)別汇跨。也不需要完全獨立的survivor堆做復(fù)制準備。G1只有邏輯上的分代概念妆距∏钏欤或者說每個分區(qū)都可能隨著G1的運動在不同代之間切換。
G1的底層原理
區(qū)域化內(nèi)存劃片Region娱据。整體編為了一些不連續(xù)的內(nèi)存區(qū)域蚪黑,避免了全內(nèi)存區(qū)的GC操作。
核心思想是將整個堆內(nèi)存區(qū)域分乘大小相同的子區(qū)域中剩,在JVM啟動時會自動設(shè)置這些子區(qū)域的大小忌穿。
G1并不要求對象的存儲一定是物理上連續(xù)的。只要邏輯上連續(xù)就可以结啼。每個分區(qū)也不會固定為某個代服務(wù)掠剑。可以按需切換為年輕代或者老念代郊愧。
Region的大小在1M-32M之間朴译。最多能設(shè)置2048個區(qū)井佑。所以能支持的內(nèi)存大小最大為322048 = 64G.*
一句話總結(jié)G1:區(qū)域化管理,最大的好處是化整為零眠寿,避免全內(nèi)存掃描躬翁,只需要按照區(qū)域來進行掃描即可。
G1回收步驟
針對eden區(qū)進行收集澜公,eden區(qū)耗盡會觸發(fā)姆另,主要是小區(qū)域收集+形成連續(xù)的內(nèi)存塊。避免內(nèi)存碎片坟乾。
- eden區(qū)的數(shù)據(jù)移動到Survivor迹辐。如果Survivor區(qū)空間不夠,eden區(qū)數(shù)據(jù)會晉升到old區(qū)甚侣。
- Survivor區(qū)的數(shù)據(jù)移動到新的Survivor區(qū)明吩。部分數(shù)據(jù)晉升到old區(qū)。
- 最后eden區(qū)收拾干凈了殷费,GC結(jié)束印荔,用戶的應(yīng)用程序繼續(xù)執(zhí)行。
G1參數(shù)配置
G1有很多特有的參數(shù)详羡,常用的幾個如下:
- -XX:G1HeapRefionSize=n.設(shè)置G1區(qū)域大小仍律,范圍是1-32m。
- -XX:MaxGCPauseMillis=n.最大GC停頓時間实柠。JVM盡可能但不保證小于這個時間水泉。
- -XX:InitiatingHeapOccupancyPercent=n.堆占用多少的時候觸發(fā)GC,默認是45
- -XX:ConcGCThreads=n.并發(fā)GC使用的線程數(shù)
- -XX:G1ReservePercent=n.設(shè)置作為空閑空間的預(yù)留內(nèi)存百分比窒盐。默認是百分之十草则。
本篇筆記就記到這里,如果稍微幫到你了記得點個喜歡點個關(guān)注蟹漓。也祝大家工作順順利利炕横,身體健康!愿所有的努力都不會被辜負葡粒!