前言
JVM系列介紹到這里,其實(shí)理論知識(shí)和基本工具的使用基本上都介紹過了瘸洛,當(dāng)然玻募,JVM的理論知識(shí)也不僅僅只是這些,如果想要更深入的里面還是會(huì)有很多細(xì)節(jié)值得深入了解滑肉,但是就目前來說包各,掌握了前面幾篇文章介紹的內(nèi)容,我們已經(jīng)可以對(duì)JVM進(jìn)行基本的調(diào)優(yōu)工作了靶庙,所以本篇文章會(huì)以一些常見問題并結(jié)合實(shí)際例子來進(jìn)行分析问畅。
常見問題及調(diào)優(yōu)實(shí)戰(zhàn)
1、內(nèi)存泄漏與內(nèi)存溢出的區(qū)別
內(nèi)存泄漏(Memory Leak):指的是對(duì)象無法得到及時(shí)的回收六荒,導(dǎo)致其持續(xù)占用內(nèi)存空間护姆,造成了內(nèi)存空間的浪費(fèi)。 內(nèi)存泄露一般是強(qiáng)引用才會(huì)出現(xiàn)問題掏击,其他像軟引用卵皂,弱引用和虛引用影響不大。
內(nèi)存溢出(Out Of Memory):內(nèi)存泄漏到一定的程度就會(huì)導(dǎo)致內(nèi)存溢出砚亭,但是內(nèi)存溢出也有可能是大對(duì)象導(dǎo)致的灯变。
這兩個(gè)區(qū)別結(jié)合下面的問題2可以更好的理解。
2钠惩、如何防止內(nèi)存泄露
我們先來看下面一個(gè)簡(jiǎn)單的例子:
package com.zwx.jvm;
public class JVMTuningDemo {
public static void main(String[] args) {
{
byte[] bytes = new byte[1024 * 1024 * 64];
}
System.gc();
}
}
調(diào)用之后打開gc日志柒凉,如果不知道怎么獲取gc日志的,可以點(diǎn)擊這里篓跛。
可以看到GC之后膝捞,對(duì)象并沒有回收掉,從代碼上來說,因?yàn)橛衶}蔬咬,所以理論上已經(jīng)離開作用域了鲤遥,bytes會(huì)被回收(如果不加{}是肯定不會(huì)被回收的,因?yàn)闆]有離開作用域)林艘,但是這里為什么還是沒有被回收盖奈?
回答這個(gè)問題之前我們先對(duì)上面的代碼改進(jìn)一下
package com.zwx.jvm;
public class JVMTuningDemo {
public static void main(String[] args) {
{
byte[] bytes = new byte[1024 * 1024 * 64];
bytes = null;
}
System.gc();
}
}
這時(shí)候再來看,會(huì)發(fā)現(xiàn)已經(jīng)被回收了
這是因?yàn)橹半m然已經(jīng)離開作用域了狐援,但是卻并沒有收回引用钢坦,也就是說棧幀中的局部變量表數(shù)組中所對(duì)應(yīng)的slot(局部變量表中數(shù)組的每一個(gè)位置都被稱之為slot)還是有值的,并沒有被切斷引用啥酱,而將其置為Null就等于切斷了引用爹凹,所以可以被回收。
如果看過我的并發(fā)編程系列文章中對(duì)AQS同步隊(duì)列以及阻塞隊(duì)列的源碼分析镶殷,那么也應(yīng)該可以看到禾酱,這些源碼中也是大量使用了這種方式來幫助虛擬機(jī)進(jìn)行g(shù)c:
在有些場(chǎng)景這種設(shè)置為null的方式確實(shí)是一種解決方式,但是其實(shí)最優(yōu)雅的方式還是以恰當(dāng)?shù)淖兞孔饔糜騺砜刂苹厥兆兞俊?br> 我們?cè)賹?duì)上面的例子進(jìn)行改寫:
package com.zwx.jvm;
public class JVMTuningDemo {
public static void main(String[] args) {
{
byte[] bytes = new byte[1024 * 1024 * 64];
}
int i = 0;
System.gc();
}
}
運(yùn)行之后打開gc日志:
我們會(huì)發(fā)現(xiàn)绘趋,bytes對(duì)象確實(shí)也被回收了颤陶,這又是為什么呢?
這是因?yàn)闂械木植孔兞勘韮?nèi)的每一個(gè)slot都是可以復(fù)用的陷遮,當(dāng)bytes變量離開了其作用域之后滓走,Java虛擬機(jī)知道這個(gè)slot已經(jīng)無效了,但是雖然無效拷呆,引用卻還在闲坎,所以如果沒有新的變量過來占用bytes變量所在的slot,是無法將bytes回收的茬斧,而一旦有新的變量過來占用slot腰懂,自然而然bytes對(duì)象的引用就被切斷了,從而被gc掉项秉。
3绣溜、GCRoot不可達(dá)的對(duì)象一定會(huì)被回收嗎
答案是不一定的。
即使在可達(dá)性分析法中被判定不可達(dá)的對(duì)象娄蔼,也并非是“非死不可”的怖喻,這時(shí)候它們暫時(shí)處于“緩刑階段”,對(duì)象依然有“逃生”的機(jī)會(huì)岁诉。
一個(gè)對(duì)象在第一次被標(biāo)記為不可達(dá)對(duì)象時(shí)锚沸,并不會(huì)立刻被回收,而是會(huì)進(jìn)行判斷是否有必要執(zhí)行?nalize()方法涕癣,那么什么時(shí)候會(huì)執(zhí)行?nalize()方法呢哗蜈?有兩種情況:
1、Java虛擬機(jī)已經(jīng)調(diào)用過當(dāng)前對(duì)象的?nalize()方法
-
2、?nalize()方法被我們重寫了
如果不滿足這兩種情況距潘,那么對(duì)象就相當(dāng)于是“死刑立即執(zhí)行”炼列,沒有機(jī)會(huì)逃生,但是一旦滿足執(zhí)行?nalize()方法的條件音比,而我們又在?nalize()方法中將對(duì)象重新和引用鏈中的對(duì)象進(jìn)行了關(guān)聯(lián)俭尖,這時(shí)候?qū)ο缶涂梢皂樌疤由薄?br> 我們來看下面一個(gè)例子:package com.zwx.jvm;
import java.util.ArrayList; import java.util.List;
public class ObjEscapeByFinalize { public static ObjEscapeByFinalize objEscapeByFinalize = null;
public static void main(String[] args) throws InterruptedException { objEscapeByFinalize = new ObjEscapeByFinalize(); //首次自救 objEscapeByFinalize = null; System.gc(); Thread.sleep(1000);//?nalize()方法優(yōu)先級(jí)比較低,稍微停頓一會(huì)等一等 print(); //再次自救 objEscapeByFinalize = null; System.gc(); Thread.sleep(1000); print(); } static void print(){ if (null == objEscapeByFinalize){ System.out.println("obj has been gc"); }else{ System.out.println("obj escape success"); } } @Override protected void finalize() throws Throwable { System.out.println("come in method:finalize"); super.finalize(); objEscapeByFinalize = this; }
}
運(yùn)行結(jié)果為:
come in method:finalize
obj escape success
obj has been gc
從結(jié)果可以看到洞翩,第一次自救成功稽犁,而第二次已經(jīng)沒有了自救機(jī)會(huì),因?yàn)楫?dāng)前對(duì)象已經(jīng)執(zhí)行過一次finalize()方法了菱农,而如果我們把finalize()方法中的:
objEscapeByFinalize = this;
替換為:
objEscapeByFinalize = new ObjEscapeByFinalize();
這時(shí)候就可以一直自救成功缭付,因?yàn)槊看巫跃戎缶彤a(chǎn)生了一個(gè)新的對(duì)象,新的對(duì)象并沒有執(zhí)行過finalize()方法循未。
上面的demo還有一點(diǎn)需要注意的是,finalize()方法針對(duì)的是對(duì)象秫舌,假如上面的靜態(tài)對(duì)象換成一個(gè)其他對(duì)象的妖,而finalize()方法又寫在當(dāng)前對(duì)象,那么是無效的足陨,例如如下例子:
package com.zwx.jvm;
import java.util.ArrayList;
import java.util.List;
public class ObjEscapeByFinalize1 {
public static List<Object> list = null;
public static void main(String[] args) throws InterruptedException {
list = new ArrayList<>();
//首次自救
list = null;
System.gc();
Thread.sleep(1000);
print();
}
static void print(){
if (null == list){
System.out.println("obj has been gc");
}else{
System.out.println("obj escape success");
}
}
@Override
protected void finalize() throws Throwable {
System.out.println("come in method:finalize");
super.finalize();
list = new ArrayList<>();
}
}
這里是無法實(shí)現(xiàn)自救的嫂粟,因?yàn)檫@里要救的對(duì)象是List,而finalize()并不屬于List墨缘,是屬于ObjEscapeByFinalize1對(duì)象星虹,所以這一點(diǎn)也是需要明確地。
不過雖然finalize()可以完成對(duì)象自救镊讼,但是由于這個(gè)方法的代價(jià)比較大而且運(yùn)行時(shí)有不確定性宽涌,一般情況下還是不建議使用
4、Young GC會(huì)有STW嗎
不管是什么類型的GC蝶棋,都會(huì)有 stop-the-world卸亮,只是發(fā)生時(shí)間的長(zhǎng)短,目前Java中所有的垃圾回收器均需要STW玩裙,唯一的區(qū)別只是時(shí)間的長(zhǎng)短問題兼贸。
5、Major GC和Full GC的區(qū)別
之前我們提到了吃溅,Major GC通常會(huì)伴隨著Minor GC溶诞,也就等于觸發(fā)了Full GC,但是雖然如此,Major GC和Full GC并不是完全等價(jià)的决侈,因?yàn)镕ull GC 的同時(shí)會(huì)對(duì)方法區(qū)(jdk1.8的metaspace螺垢,jdk1.7的永久代)進(jìn)行GC,所以嚴(yán)格來說:Full GC=Major GC+Minor GC+方法區(qū)GC
6、方法區(qū)會(huì)發(fā)生GC嗎
答案是肯定的甩苛。雖然方法區(qū)中的回收收益一般都不高蹂楣,但是也是會(huì)被GC的,而方法區(qū)中被回收的最主要的就是對(duì)廢棄常量和無用類的回收讯蒲,判定一個(gè)廢棄常量比較簡(jiǎn)單痊土,但是判定一個(gè)類是無用類是比較困難的,那么方法區(qū)中的怎么判斷一個(gè)類是無用類呢墨林?
判斷一個(gè)類是否無用赁酝,需要達(dá)到以下三個(gè)條件:
- 1殖属、該類所有的實(shí)例都已經(jīng)被回收智亮,也就是 Java 堆中不存在該類的任何實(shí)例。
- 2坊谁、加載該類的類加載器ClassLoader已經(jīng)被回收(從這個(gè)條件可以看出搔耕,一般只有大量使用了反射隙袁,動(dòng)態(tài)代理或者字節(jié)碼框架等場(chǎng)景條件下才會(huì)滿足這個(gè)條件)。
- 3弃榨、該類對(duì)應(yīng)的 java.lang.Class對(duì)象沒有在任何地方被引用菩收,無法在任何地方通過反射訪問該類的方法。
這三個(gè)條件實(shí)際上是非尘ňΓ苛刻的娜饵,而即使達(dá)到以上三個(gè)條件,無用類也僅僅是可以被回收官辈,但是是不是一定會(huì)被回收箱舞,還是取決于Java虛擬機(jī)。HotSpot虛擬機(jī)中提供了參數(shù)-Xnoclassgc來控制拳亿。
7晴股、什么是直接內(nèi)存
直接內(nèi)存(Direct Memory)不屬于運(yùn)行時(shí)數(shù)據(jù)區(qū),也被稱之為堆外內(nèi)存风瘦,通常訪問直接內(nèi)存的速度會(huì)優(yōu)于Java堆队魏。直接沒存也有可能會(huì)發(fā)生OutOfMemoryError異常,Java 1.4中新加入的nio包下的ByteBuffer就操作了直接內(nèi)存万搔,直接內(nèi)存可以通過參數(shù)-XX:MaxDirectMemorySize控制大小胡桨。
8、CMS收集器和G1收集器的區(qū)別
作為同樣是并行的2款垃圾收集器瞬雹,G1的目前是用來取代CMS收集器的昧谊,其主要有如下區(qū)別:
- 1、CMS收集器是老年代的收集器酗捌,需要和其他新生代收集器配合使用呢诬,而G1同時(shí)適用于新生代和老年代涌哲,不需要和其他收集器配合使用
- 2、CMS收集器以最小的停頓時(shí)間為目標(biāo)的并發(fā)收集器尚镰,G1收集器是一種可預(yù)測(cè)垃圾回收的停頓時(shí)間
- 3阀圾、CMS收集器是使用“標(biāo)記-清除”算法進(jìn)行的垃圾回收,容易產(chǎn)生內(nèi)存碎片狗唉,G1使用了 Region方式對(duì)堆內(nèi)存進(jìn)行了劃分初烘,且基于標(biāo)記整理算法實(shí)現(xiàn),整體減少了垃圾碎片的產(chǎn)生
9分俯、類加載機(jī)制經(jīng)過哪些步驟
類加載機(jī)制主要經(jīng)過了:加載(Loading)肾筐,連接(Linking),初始化(Initialization)缸剪,使用(Using)吗铐,卸載(Unloading) 五個(gè)大階段,而其中連接(Linking)又分為:驗(yàn)證(Verification),準(zhǔn)備(Preparation)杏节,解析(Resolution)三個(gè)階段唬渗。
10、系統(tǒng)CPU經(jīng)常100%拢锹,如何定位
- 1谣妻、首先要確認(rèn)哪個(gè)進(jìn)程占用CPU高,可以使用top命令
2卒稳、找到之后可以繼續(xù)執(zhí)行top -Hp PID命令查詢出占用最大的線程
3、執(zhí)行jstack命令生成線程快照信息:
jstack -l 進(jìn)程PID >jstack.log
1
輸出之后他巨,我們找到上面占用CPU最高的一個(gè)線程pid=11566充坑,將其轉(zhuǎn)換為16進(jìn)制,得到的結(jié)果是2d2e染突,然后進(jìn)入生成的jstack.log文件找到這個(gè)線程可以查看線程信息捻爷。
4、上面就可以定位到了線程調(diào)用的方法了份企,接下來就可以去分析對(duì)應(yīng)的代碼尋找問題了
總結(jié)
本文主要列舉了一些其他比較經(jīng)典的也榄,而前面在JVM系列其他文章中又沒有過多進(jìn)行說明的問題,JVM學(xué)習(xí)之后需要不斷實(shí)戰(zhàn)積累調(diào)優(yōu)經(jīng)驗(yàn)司志,雖然還有一些理論在JVM系列中沒有提及甜紫,但是我想如果可以認(rèn)真把我 JVM系列至本篇為止的8篇文章相關(guān)知識(shí)和理論都掌握的話,那至少可以說已經(jīng)具備了調(diào)優(yōu)的理論基礎(chǔ)了骂远,剩下的就是不斷積累經(jīng)驗(yàn)囚霸,當(dāng)然,推薦大家可以去通讀一下JVM規(guī)范激才,畢竟所有的Java虛擬機(jī)都是按照J(rèn)VM規(guī)范來實(shí)現(xiàn)的拓型,或者有必要的可以自己去編譯JDK來進(jìn)行更深一步的研究额嘿。
請(qǐng)關(guān)注我,一起學(xué)習(xí)進(jìn)步