本篇重點講解JVM內(nèi)存管理和垃圾回收,如下圖JVM的基本結(jié)構(gòu):

首先理解下JVM工作原理
JVM俗稱java虛擬機(jī),它是用來執(zhí)行.class文件的,大家通常編寫的.java文件最后都會被javac編譯成.class字節(jié)碼文件,JVM將這些.class文件加載到內(nèi)存中,然后再由JVM中的執(zhí)行引擎,執(zhí)行.class中的字節(jié)碼指令。類加載器ClassLoader就是負(fù)責(zé)將.class文件裝載到JVM的內(nèi)存。
JVM中默認(rèn)有三個ClassLoader分別是:
1.Bootstrap ClassLoader,負(fù)責(zé)加載%JRE_HOME%\lib下面的包,如:rt.jar、resources.jar、charsets.jar和class等。
2.ExtClassLoader,負(fù)責(zé)加載%JRE_HOME%\lib\ext下面的包。
3.AppClassLoader,負(fù)責(zé)加載當(dāng)前應(yīng)用的classpath的所有類。
這個順序也是jvm啟動的時候類的加載順序。
下圖是類加載的繼承關(guān)系:
從上面我們可以看到以上3個類只能加載固定目錄的class,其實我們還可以實現(xiàn)自定義ClassLoader加載任意地方的class,比如某個磁盤,或者網(wǎng)絡(luò)上class資源等。
類加載有三種方式
1、命令行啟動應(yīng)用時候由JVM初始化加載
2、通過Class.forName()方法動態(tài)加載
3、通過ClassLoader.loadClass()方法動態(tài)加載
Class.forName()和ClassLoader.loadClass()區(qū)別
Class.forName():將類的.class文件加載到j(luò)vm中之外,還會對類進(jìn)行解釋,執(zhí)行類中的static塊;
ClassLoader.loadClass():只干一件事情,就是將.class文件加載到j(luò)vm中,不會執(zhí)行static中的內(nèi)容,只有在newInstance才會去執(zhí)行static塊。
注:
Class.forName(name, initialize, loader)帶參函數(shù)也可控制是否加載static塊。并且只有調(diào)用了newInstance()方法采用調(diào)用構(gòu)造函數(shù),創(chuàng)建類的對象 。
類的加載時按照雙親委派模型進(jìn)行,如下:
1、當(dāng)AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。
2、當(dāng)ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加載失?。ɡ缭?JAVA_HOME/jre/lib里未查找到該class),會使用ExtClassLoader來嘗試加載;
4、若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,如果AppClassLoader也加載失敗,則會報出異常ClassNotFoundException。
雙親委派模型意義:
-系統(tǒng)類防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼
-保證Java程序安全穩(wěn)定運(yùn)行
以下是jdk1.8中類的加載源代碼
protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(var1)) {
Class var4 = this.findLoadedClass(var1);
if(var4 == null) {
long var5 = System.nanoTime();
try {
if(this.parent != null) {
var4 = this.parent.loadClass(var1, false);
} else {
var4 = this.findBootstrapClassOrNull(var1);
}
} catch (ClassNotFoundException var10) {
;
}
if(var4 == null) {
long var7 = System.nanoTime();
var4 = this.findClass(var1);
PerfCounter.getParentDelegationTime().addTime(var7 - var5);
PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
PerfCounter.getFindClasses().increment();
}
}
if(var2) {
this.resolveClass(var4);
}
return var4;
}
}
ClassLoader將類加載到內(nèi)存中,會分配到不同的內(nèi)存塊如下所示:
1.棧內(nèi)存,jvm參數(shù) -XSS可以調(diào)整單個線程棧大小,每次方法調(diào)用就會創(chuàng)建一個貞棧壓入棧,線程棧中每個棧貞包含了當(dāng)前方法的本地變量信息,如:局部變量,常量池指針,操作棧數(shù)。每個線程只能讀取自己本地變量信息,即使執(zhí)行了同一段代碼,它們也是copy一份到本地變量,因此線程之間的本地變量是完全隔離的。java中原始變量(boolean,byte,short,char,int,long,float,double)都存在線程的棧中,各個線程獨自占有,無法共享。
2.堆內(nèi)存,通過參數(shù)-Xmx和-Xms來調(diào)整堆的大小,對于32位機(jī)器最大2G,64位無限制。對于分代GC來說:包含新生代,舊生代和持久代。持久代最小值為16MB,最大值為64MB。在JDK1.8以后合并為元生代。java中所有的對象信息包括原始類型(Byte、Integer、Long等),不管是哪個線程創(chuàng)建的,無論成員變量還是方法中的變量,都會被存儲在堆中。
3.靜態(tài)區(qū)(或者叫方法區(qū))也被稱為永久代,可以通過-XX:MaxPermSize=512m調(diào)整,它也是全部存儲在堆中。
JVM內(nèi)存回收
大家都知道Java優(yōu)于其他編程語言最大的好處是JVM自動管理內(nèi)存,內(nèi)存由GC自動回收,但是GC回收也有自己的缺陷:
1.垃圾并不會按照我們的要求隨時進(jìn)行回收
2.程序編程人員不能對垃圾回收進(jìn)行控制
3.垃圾回收并不會及時的進(jìn)行內(nèi)存清理,盡管有時候內(nèi)存已經(jīng)不夠使用了
因此就要求我們在寫代碼的時候能夠?qū)懗龇螱C回收規(guī)律的代碼,以便GC能快速回收,釋放內(nèi)存,保證程序的正常運(yùn)行。
從前面JVM內(nèi)存的結(jié)構(gòu)我們知道,程序計數(shù)器、JVM棧、本地方法棧。因為它們的生命周期是和線程同步的,隨著線程的銷毀,它們占用的內(nèi)存會自動釋放,所以它們不需要被回收。GC回收最大的塊就是堆靜態(tài)區(qū)。簡單的說就是,如果某個對象已經(jīng)不存在任何引用,那么它可以被回收。
Sun的jvm采用了一種叫做“根搜索算法”,基本思想就是:從一個叫GC Roots的對象開始,向下搜索,如果一個對象不能到達(dá)GC Roots對象的時候,說明它已經(jīng)不再被引用,即可被進(jìn)行垃圾回收。
常見的GC回收算法
1、標(biāo)記-清除算法(Mark-Sweep):最基礎(chǔ)的GC算法,將需要進(jìn)行回收的對象做標(biāo)記,之后掃描,有標(biāo)記的進(jìn)行回收,這樣就產(chǎn)生兩個步驟:標(biāo)記和清除。這個算法效率不高,而且在清理完成后會產(chǎn)生內(nèi)存碎片,這樣,如果有大對象需要連續(xù)的內(nèi)存空間時,還需要進(jìn)行碎片整理,所以,此算法需要改進(jìn)。
2、復(fù)制算法(Copying)
前面我們談過,新生代內(nèi)存分為了三份,Eden區(qū)和2塊Survivor區(qū),一般Sun的JVM會將Eden區(qū)和Survivor區(qū)的比例調(diào)為8:1,保證有一塊Survivor區(qū)是空閑的,這樣,在垃圾回收的時候,將不需要進(jìn)行回收的對象放在空閑的Survivor區(qū),然后將Eden區(qū)和第一塊Survivor區(qū)進(jìn)行完全清理,這樣有一個問題,就是如果第二塊Survivor區(qū)的空間不夠大怎么辦?這個時候,就需要當(dāng)Survivor區(qū)不夠用的時候,暫時借持久代的內(nèi)存用一下。此算法適用于新生代。
3、標(biāo)記-整理(或叫壓縮)算法(Mark-Compact)
和標(biāo)記-清楚算法前半段一樣,只是在標(biāo)記了不需要進(jìn)行回收的對象后,將標(biāo)記過的對象移動到一起,使得內(nèi)存連續(xù),這樣,只要將標(biāo)記邊界以外的內(nèi)存清理就行了。此算法適用于持久代。
常見的垃圾收集器:
1、Serial GC。是最基本、最古老的收集器,但是現(xiàn)在依然被廣泛使用,是一種單線程垃圾回收機(jī)制,而且不僅如此,它最大的特點就是在進(jìn)行垃圾回收的時候,需要將所有正在執(zhí)行的線程暫停(Stop The World),對于有些應(yīng)用這是難以接受的,但是我們可以這樣想,只要我們能夠做到將它所停頓的時間控制在N個毫秒范圍內(nèi),大多數(shù)應(yīng)用我們還是可以接受的,而且事實是它并沒有讓我們失望,幾十毫米的停頓我們作為客戶機(jī)(Client)是完全可以接受的,該收集器適用于單CPU、新生代空間較小及對暫停時間要求不是非常高的應(yīng)用上,是client級別默認(rèn)的GC方式,可以通過-XX:+UseSerialGC來強(qiáng)制指定。
2、ParNew GC?;竞蚐erial GC一樣,但本質(zhì)區(qū)別是加入了多線程機(jī)制,提高了效率,這樣它就可以被用在服務(wù)器端(Server)上,同時它可以與CMS GC配合,所以,更加有理由將它置于Server端。
3、Parallel Scavenge GC。在整個掃描和復(fù)制過程采用多線程的方式來進(jìn)行,適用于多CPU、對暫停時間要求較短的應(yīng)用上,是server級別默認(rèn)采用的GC方式,可用-XX:+UseParallelGC來強(qiáng)制指定,用-XX:ParallelGCThreads=4來指定線程數(shù)。以下給出幾組使用組合:


有連線的的部分代表可以聯(lián)合使用
4、CMS (Concurrent Mark Sweep)收集器。該收集器目標(biāo)就是解決Serial GC 的停頓問題,以達(dá)到最短回收時間。常見的B/S架構(gòu)的應(yīng)用就適合用這種收集器,因為其高并發(fā)、高響應(yīng)的特點。CMS收集器是基于“標(biāo)記-清除”算法實現(xiàn)的,整個收集過程大致分為4個步驟:
初始標(biāo)記(CMS initial mark)、并發(fā)標(biāo)記(CMS concurrenr mark)、重新標(biāo)記(CMS remark)、并發(fā)清除(CMS concurrent sweep)。
Java程序性能優(yōu)化
1、gc調(diào)用,調(diào)用gc 方法暗示著Java 虛擬機(jī)做了一些努力來回收未用對象,以便能夠快速地重用這些對象當(dāng)前占用的內(nèi)存。當(dāng)控制權(quán)從方法調(diào)用中返回時,虛擬機(jī)已經(jīng)盡最大努力從所有丟棄的對象中回收了空間,調(diào)用System.gc() 等效于調(diào)用Runtime.getRuntime().gc()。
2、finalize()的調(diào)用及重寫,gc 只能清除在堆上分配的內(nèi)存(純java語言的所有對象都在堆上使用new分配內(nèi)存),而不能清除棧上分配的內(nèi)存(當(dāng)使用JNI技術(shù)時,可能會在棧上分配內(nèi)存,例如java調(diào)用c程序,而該c程序使用malloc分配內(nèi)存時)。因此,如果某些對象被分配了棧上的內(nèi)存區(qū)域,那gc就管不著了,對棧上的對象進(jìn)行內(nèi)存回收就要靠finalize()。舉個例子來說,當(dāng)java 調(diào)用非java方法時(這種方法可能是c或是c++的),在非java代碼內(nèi)部也許調(diào)用了c的malloc()函數(shù)來分配內(nèi)存,而且除非調(diào)用那個了 free() 否則不會釋放內(nèi)存(因為free()是c的函數(shù)),這個時候要進(jìn)行釋放內(nèi)存的工作,gc是不起作用的,因而需要在finalize()內(nèi)部的一個固有方法調(diào)用free()。
優(yōu)秀的編程習(xí)慣
(1)避免在循環(huán)體中創(chuàng)建對象,即使該對象占用內(nèi)存空間不大。
(2)盡量及時使對象符合垃圾回收標(biāo)準(zhǔn)(1.對象賦值null,并且以后再也沒有使用過,2.對象賦予了新的值,即重新分配了空間)
(3)不要采用過深的繼承層次。
(4)訪問本地變量優(yōu)于訪問類中的變量。
常見問題
1、內(nèi)存溢出
就是你要求分配的java虛擬機(jī)內(nèi)存超出了系統(tǒng)能給你的,系統(tǒng)不能滿足需求,于是產(chǎn)生溢出。
2、內(nèi)存泄漏
是指你向系統(tǒng)申請分配內(nèi)存進(jìn)行使用(new),可是使用完了以后卻不歸還(delete),結(jié)果你申請到的那塊內(nèi)存你自己也不能再訪問,該塊已分配出來的內(nèi)存也無法再使用,隨著服務(wù)器內(nèi)存的不斷消耗,而無法使用的內(nèi)存越來越多,系統(tǒng)也不能再次將它分配給需要的程序,產(chǎn)生泄露。一直下去,程序也逐漸無內(nèi)存使用,就會溢出。
java線程數(shù) = (系統(tǒng)空閑內(nèi)存-堆內(nèi)存(-Xms, -Xmx)- perm區(qū)內(nèi)存(-XX:MaxPermSize)) / 線程棧大小(-Xss)
以8核16G機(jī)器為例jvm設(shè)置參數(shù):
set JAVA_OPTS=%JAVA_OPTS% -server -Xms3G -Xmx3G -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m -XX:+UseParallelOldGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/aaa/dump -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/usr/aaa/dump/heap_trace.txt -XX:NewSize=1G -XX:MaxNewSize=1G
參數(shù)調(diào)優(yōu)可以參照https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html
總結(jié)
Java虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法被調(diào)用的時候都會創(chuàng)建一個棧幀,用于存儲局部變量表、操作棧、動態(tài)鏈接、方法出口等信息。每一個方法被調(diào)用直至執(zhí)行完成的過程就對應(yīng)著一個棧幀在虛擬機(jī)中從入棧到出棧的過程。
在Java虛擬機(jī)規(guī)范中,對這個區(qū)域規(guī)定了兩種異常情況:
(1)如果線程請求的棧深度太深,超出了虛擬機(jī)所允許的深度,就會出現(xiàn)StackOverFlowError(比如無限遞歸。因為每一層棧幀都占用一定空間,而 Xss 規(guī)定了棧的最大空間,超出這個值就會報錯)
(2)虛擬機(jī)??梢詣討B(tài)擴(kuò)展,如果擴(kuò)展到無法申請足夠的內(nèi)存空間,會出現(xiàn)OOM
下面這篇文章講的也很好
https://www.cnblogs.com/lcword/p/5857918.html
https://www.cnblogs.com/xiaoxi/p/6486852.html
常見參數(shù)調(diào)優(yōu)以及日志查看
1.2G內(nèi)存(jdk8)
-server -Xmx1550m -Xms1550m -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
2. 4G內(nèi)存(jdk8)
-server -Xmx3550m -Xms3550m -Xmn2g -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
3. 2G內(nèi)存(jdk7)
-server -Xmx1550m -Xms1550m -XX:PermSize=256m -XX:MaxPermSize=512m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MaxGCPauseMillis=100 -XX:+DisableExplicitGC -XX:+UseAdaptiveSizePolicy
4. 4G內(nèi)存(jdk7)
-server -Xmx3550m -Xms3550m -XX:PermSize=512m -XX:MaxPermSize=512m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MaxGCPauseMillis=100 -XX:+DisableExplicitGC -XX:+UseAdaptiveSizePolicy
k12 JVM設(shè)置(jdk7)
-server -Xms1400m -Xmx1400m -Xss256k -XX:NewSize=940M -XX:MaxNewSize=940M -XX:NewRatio=2 -XX:PermSize=128m -XX:MaxPermSize=300m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
2. 實時內(nèi)存參數(shù)查看命令
> jinfo pid
> jmap -head pid
3.線上jvm問題排查步驟
線上cpu飆高,一定要第一時間dump文件保留現(xiàn)場,dump文件找蔣歡
命令:jmap -dump:format=b,file=文件名 [pid]
分析dump文件--個人用mat最方便
3.1 通過 top 命令找到 CPU 消耗最高的進(jìn)程,并記住進(jìn)程 ID。
3.2 再次通過 top -Hp [進(jìn)程 ID] 找到 CPU 消耗最高的線程 ID,并記住線程 ID.
3.3 通過 JDK 提供的 jstack 工具 dump 線程堆棧信息到指定文件中。具體命令:jstack -l [進(jìn)程 ID] >jstack.log。
3.4 由于剛剛的線程 ID 是十進(jìn)制的,而堆棧信息中的線程 ID 是16進(jìn)制的,因此我們需要將10進(jìn)制的轉(zhuǎn)換成16進(jìn)制的,并用這個線程 ID 在堆棧中查找。使用 printf "%x\n" [十進(jìn)制數(shù)字] ,可以將10進(jìn)制轉(zhuǎn)換成16進(jìn)制。
3.5 通過剛剛轉(zhuǎn)換的16進(jìn)制數(shù)字從堆棧信息里找到對應(yīng)的線程堆棧。就可以從該堆棧中看出端倪。
