,JVM參數問題
1、問題:JVM怎樣通過參數調整內存大小
來源:阿里巴巴
問題描述:如題
解決方案:
-Xmx 堆最大值,默認值爲物理內存的1/4,最佳設值應該視物理內存大小及計算機內其他內存開銷而定;
-Xms 堆最小值,Server端JVM最好將-Xms和-Xmx設爲相同值,開發測試機JVM可以保留默認值;
-Xmn 新生代大小,
-Xss 每個線程的Stack大小;
-XXSurvivorRatio:Eden區和Survior區的佔用比例.
-XX:PermSize=64M JVM初始分配的永久代內存
-XX:MaxPermSize=128M JVM最大允許分配的永久代內存
說明:
1.如果-Xmx 不指定或者指定偏小,應用可能會導致java.lang.OutOfMemory錯誤,此錯誤來自JVM,不是Throwable的,無法用try...catch捕捉。
2.增加Heap的大小雖然會降低GC的頻率,但也增加了每次GC的時間
3. 當棧中存儲數據比較多時,需要適當調大-Xss這個值,否則會出現java.lang.StackOverflowError異常
2、問題:對於JVM內存配置參數-Xms 10240m -Xmx 10240m -Xmn 5120m -XXSurvivorRatio=3 Survivor區總大小分別爲()、()?
來源:
解決方法:
1:-Xms 10240m:指定JVM 堆(heap) 最小值爲10240m.
2:-Xmx 10240m:指定 JVM 堆(heap) 最大值爲 10240m. 即最小和最大值
相同,指定堆不可擴展。
3:-Xmn 5120m:指定新生代(Young Generation)大小爲 5120m。
4:-XX:SurvivorRatio = 3:指定新生代中Eden區和一個Survivor 區的空間比例爲:3:1。
下圖爲JVM Heap簡單的內存分佈圖
整個Java 堆分爲三個區域:新生代,老年代,永久代(新版的Hotspot JVM去了永久代),本題只涉及新生代。新生代分爲一個Eden 區和兩個大小相同的Survivor區,用來實現複製GC算法。根據上面的參數,新生代爲5120m,Eden區和一個Survivor區爲 3 : 1。可算得一個Survivor區大小爲: 5120/5 = 1024m。兩個Survivor區的大小相同,答案爲:1024m 1024m。
二,JVM垃圾回收(GC)問題
1、GC算法
GC判斷對象是否存活算法
① 引用計數算法
② 根搜索算法(GC Root)可達性分析法
GC垃圾收集算法
① 複製算法(Copying)新生代:Eden和兩個存活區,一次Minor GC後會將伊甸園中存活的對象複製到存活區,將內存劃分成大小相等的兩塊,每次只使用其中的一塊,當需要清理時,就直接將存活的對象複製到另一塊。這種方式實現簡單效率高,也不會存在碎片問題,缺點:實際可用內存縮小爲原來的一半。
② 標記清除算法(Mark-Sweep)老生代:分爲標記和清除兩個階段,首先標記出可以回收的對象,標記完後統一回收。缺點如下:
a) 效率低:標記和清除過程效率都不高;
b) 空間問題:清除之後產生大量不連續的內存碎片。
③ 標記整理算法(Mark-Compact):與標記清除相似,不相同點在於整理對將存活的對象向一端進行移動,不會產生碎片。該算法主要是爲了解決標記-清除,產生大量內存碎片的問題
④ 分代收集算法(Generational Collection):根據存活週期的不同將內存劃分爲幾塊。
JVM虛擬機GC回收算法
① 分代收集:分爲年青代和老年代
② 複製收集:年青代使用了2個倖存區來實現複製(默認比例:8:1:1,其中1用於交換,因此實際可用爲90%)
③ 標記整理:老年代由於對象的存活率高,所以適合使用標記整理或標記清除來進行回收
2、爲什麼要區分新生代和老生代?不同代採用的算法區別?
問題描述:
解決方法:
堆中區分的新生代和老年代是爲了垃圾回收,新生代中的對象存活期一般不長,而老年代中的對象存活期較長,所以當垃圾回收器回收內存時,新生代中垃圾回收效果較好,會回收大量的內存,而老年代中回收效果較差,內存回收不會太多。
基於以上特性,新生代中一般採用複製算法,因爲存活下來的對象是少數,所需要複製的對象少,而老年代對象存活多,不適合採用複製算法,一般是標記整理和標記清除算法。
因爲複製算法需要留出一塊單獨的內存空間來以備垃圾回收時複製對象使用,所以將新生代分爲eden區和兩個survivor區,每次使用eden和一個survivor區,另一個survivor作爲備用的對象複製內存區。
3、minor GC和Full GC的觸發時機?
Minor GC觸發條件:新生代中
(1)程序調用System.gc時可以觸發;
(2)系統自身來決定GC觸發的時機。
Minor GC觸發條件:當Eden區滿時,觸發Minor GC(年輕代 )。
Full GC觸發條件:全局下
(1)調用System.gc時,系統建議執行Full GC,但是不必然執行
(2)老年代空間不足
(3)方法區空間不足
(4)通過Minor GC後進入老年代的平均大小大於老年代的可用內存
(5)由Eden區、From Space區向To Space區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
4、爲什麼java要有垃圾回收
解決方法:它使java程序員在編程的時候不用再考慮內存空間的使用情況,垃圾回收(GC)會在不可預知的情況下對已經死亡或者長期未使用的對象進行清查和回收。
5、如果對象的引用被置爲null,垃圾收集器是否會立即釋放對象佔用的內存?
不會,在下一個垃圾回收週期中,這個對象將是可被回收的。
6、如和判斷一個對象是否存活?(或者GC對象的判定方法)
判斷一個對象是否存活有兩種方法:
(1)引用計數法
所謂引用計數法就是給每一個對象設置一個引用計數器,每當有一個地方引用這個對象時,就將計數器加一,引用失效時,計數器就減一。當一個對象的引用計數器爲零時,說明此對象沒有被引用,也就是“死對象”,將會被垃圾回收.
引用計數法有一個缺陷就是無法解決循環引用問題,也就是說當對象A引用對象B,對象B又引用者對象A,那麼此時A,B對象的引用計數器都不爲零,也就造成無法完成垃圾回收,所以主流的虛擬機都沒有采用這種算法。
(2)可達性算法(引用鏈法)
該算法的思想是:從一個被稱爲GC Roots的對象開始向下搜索,如果一個對象到GC Roots沒有任何引用鏈相連時,則說明此對象不可用。
7、在java中可以作爲GC Roots的對象有以下幾種:
(1)虛擬機棧中引用的對象
(2)方法區類靜態屬性引用的對象
(3)方法區常量池引用的對象
(4)本地方法棧JNI引用的對象
雖然這些算法可以判定一個對象是否能被回收,但是當滿足上述條件時,一個對象並不一定會被回收。當一個對象不可達GC Root時,這個對象並不會立馬被回收,而是出於一個死緩的階段,若要被真正的回收需要經歷兩次標記。如果對象在可達性分析中沒有與GC Root的引用鏈,那麼此時就會被第一次標記並且進行一次篩選,篩選的條件是是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法或者已被虛擬機調用過,那麼就認爲是沒必要的。
如果該對象有必要執行finalize()方法,那麼這個對象將會放在一個稱爲F-Queue的對隊列中,虛擬機會觸發一個Finalize()線程去執行,此線程是低優先級的,並且虛擬機不會承諾一直等待它運行完,這是因爲如果finalize()執行緩慢或者發生了死鎖,那麼就會造成F-Queue隊列一直等待,造成了內存回收系統的崩潰。GC對處於F-Queue中的對象進行第二次被標記,這時,該對象將被移除”即將回收”集合,等待回收。
9、簡述java內存分配與回收策率以及Minor GC和Major GC
來源:
解決方法:
對象優先在堆的Eden區分配。
大對象直接進入老年代.
長期存活的對象將直接進入老年代.
當Eden區沒有足夠的空間進行分配時,虛擬機會執行一次Minor GC.Minor Gc通常發生在新生代的Eden區,在這個區的對象生存期短,往往發生Gc的頻率較高,回收速度比較快;Full Gc/Major GC 發生在老年代,一般情況下,觸發老年代GC的時候不會觸發Minor GC,但是通過配置,可以在Full GC之前進行一次Minor GC這樣可以加快老年代的回收速度
10、Java引用的四種狀態
強引用:
用的最廣。我們平時寫代碼時,new一個Object存放在堆內存,然後用一個引用指向它,這就是強引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。
軟引用:
如果一個對象只具有軟引用,則內存空間足夠時,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。(備註:如果內存不足,隨時有可能被回收。)
只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。
弱引用:
弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命週期。
每次執行GC的時候,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。
虛引用:
“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
虛引用主要用來跟蹤對象被垃圾回收器回收的活動。
附加:如果一個對象的應用類型有多個,如何判斷它的可達性?
答:單弱多強
- 單條引用鏈的可達性以最弱的一個引用來決定
- 多條引用鏈的可達性以最強的一個引用來決定
- 如何減少GC出現的次數
- 對象不用時最好顯示置爲null
- 減少system.gc
- 儘量少使用靜態變量
- 儘量使用StringBuffer,而不用string來累加字符串
- 分散對象創建和刪除時間
- 減少finalize函數
- 儘量使用基本類型,少使用封裝類型
- 創建對象的方法
- new方法創建對象
- Clone()方法:重複創建對象時使用
原型模式主要用於對象的複製,實現一個cloneable接口或者重寫一個clone()方法。
淺拷貝:對值類型的成員變量進行值複製,對引用類型的對象只引用而不復制
深拷貝:對值類型的成員變量進行值複製,對引用類型的對象也進行復制
- 內存泄露與溢出
- 內存泄露:一個對象以及不再使用但是任然佔用空間,堆中申請的空間沒有被釋放
解決方法:避免循環中創建內存。儘早釋放無用對象。儘量少用靜態變量。用stringBuffer替代String
- 內存溢出:空間不足
原因:請求數據量過大。集合中對象被引用,引用完未清理。啓動內存參數設置過小。代碼中產生死循環
解決:設置JVM啓動參數。查看錯誤日誌。檢查代碼。Jconsole等內存檢查工具
13、垃圾收集器
新生代:serial parnew
老生代: serialOld parOld
解決方法:
(1)Serial收集器:(串行收集器)
這個收集器是一個單線程的收集器,但它的單線程的意義並不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程(Stop-The-World:將用戶正常工作的線程全部暫停掉),直到它收集結束。收集器的運行過程如下圖所示:
上圖中:
新生代採用複製算法,Stop-The-World
老年代採用標記-整理算法,Stop-The-World
當它進行GC工作的時候,雖然會造成Stop-The-World,但它存在有存在的原因:正是因爲它的簡單而高效(與其他收集器的單線程比),對於限定單個CPU的環境來說,沒有線程交互的開銷,專心做GC,自然可以獲得最高的單線程手機效率。所以Serial收集器對於運行在client模式下是一個很好的選擇(它依然是虛擬機運行在client模式下的默認新生代收集器)。
(2)ParNew收集器:Serial收集器的多線程版本(使用多條線程進行GC)
ParNew收集器是Serial收集器的多線程版本。
它是運行在server模式下的首選新生代收集器,除了Serial收集器外,目前只有它能與CMS收集器配合工作。CMS收集器是一個被認爲具有劃時代意義的併發收集器,因此如果有一個垃圾收集器能和它一起搭配使用讓其更加完美,那這個收集器必然也是一個不可或缺的部分了。收集器的運行過程如下圖所示:
上圖中:
新生代採用複製算法,Stop-The-World
老年代採用標記-整理算法,Stop-The-World
(3)ParNew Scanvenge收集器
類似ParNew,但更加關注吞吐量。目標是:達到一個可控制吞吐量的收集器。
停頓時間和吞吐量不可能同時調優。我們一方買希望停頓時間少,另外一方面希望吞吐量高,其實這是矛盾的。因爲:在GC的時候,垃圾回收的工作總量是不變的,如果將停頓時間減少,那頻率就會提高;既然頻率提高了,說明就會頻繁的進行GC,那吞吐量就會減少,性能就會降低。
吞吐量:CPU用於用戶代碼的時間/CPU總消耗時間的比值,即=運行用戶代碼的時間/(運行用戶代碼時間+垃圾收集時間)。比如,虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
(4)G1收集器:
是當今收集器發展的最前言成果之一,知道jdk1.7,sun公司才認爲它達到了足夠成熟的商用程度。
優點:
它最大的優點是結合了空間整合,不會產生大量的碎片,也降低了進行gc的頻率。
二是可以讓使用者明確指定指定停頓時間。(可以指定一個最小時間,超過這個時間,就不會進行回收了)
它有了這麼高效率的原因之一就是:對垃圾回收進行了劃分優先級的操作,這種有優先級的區域回收方式保證了它的高效率。
如果你的應用追求停頓,那G1現在已經可以作爲一個可嘗試的選擇;如果你的應用追求吞吐量,那G1並不會爲你帶來什麼特別的好處。
注:以上所有的收集器當中,當執行GC時,都會stop the world,但是下面的CMS收集器卻不會這樣。
(5)CMS收集器:(老年代收集器)
CMS收集器(Concurrent Mark Sweep:併發標記清除)是一種以獲取最短回收停頓時間爲目標的收集器。適合應用在互聯網站或者B/S系統的服務器上,這類應用尤其重視服務器的響應速度,希望系統停頓時間最短。
CMS收集器運行過程:(着重實現了標記的過程)
①初始標記
根可以直接關聯到的對象
速度快
②併發標記(和用戶線程一起)
主要標記過程,標記全部對象
③重新標記
由於併發標記時,用戶線程依然運行,因此在正式清理前,再做修正
④併發清除(和用戶線程一起)
基於標記結果,直接清理對象
整個過程如下圖所示:
上圖中,初始標記和重新標記時,需要stop the world。整個過程中耗時最長的是併發標記和併發清除,這兩個過程都可以和用戶線程一起工作。
優點:併發收集,低停頓
缺點:
(1)導致用戶的執行速度降低。
(2)無法處理浮動垃圾。因爲它採用的是標記-清除算法。有可能有些垃圾在標記之後,需要等到下一次GC纔會被回收。如果CMS運行期間無法滿足程序需要,那麼就會臨時啓用Serial Old收集器來重新進行老年代的手機。
(3)由於採用的是標記-清除算法,那麼就會產生大量的碎片。往往會出現老年代還有很大的空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次full GC
疑問:既然標記-清除算法會造成內存空間的碎片化,CMS收集器爲什麼使用標記清除算法而不是使用標記整理算法:
答案:
CMS收集器更加關注停頓,它在做GC的時候是和用戶線程一起工作的(併發執行),如果使用標記整理算法的話,那麼在清理的時候就會去移動可用對象的內存空間,那麼應用程序的線程就很有可能找不到應用對象在哪裏。
14、jvm內存模型
①堆:所有線程共享的一塊內存區域,對象實例幾乎都在這分配內存。
②虛擬機棧:線程私有的,與線程生命週期相同,用於存儲局部變量表,操作棧,方法返回值。局部變量表放着基本數據類型,還有對象的引用。
③本地方法棧:跟虛擬機棧很像,不過它是爲虛擬機使用到的Native方法服務。
④方法區(java8取消該方法):各個線程共享的區域,儲存虛擬機加載的類信息,常量,靜態變量,編譯後的代碼。
⑤程序計數器:是一個數據結構,用於保存當前正常執行的程序的內存地址。Java虛擬機的多線程就是通過線程輪流切換並分配處理器時間來實現的,爲了線程切換後能恢復到正確的位置,每條線程都需要一個獨立的程序計數器,互不影響,該區域爲“線程私有”。
15、JAVA 中堆和棧的區別,說下java 的內存機制
a.基本數據類型比如變量和對象的引用都是在棧分配的
b.堆內存用來存放由new創建的對象和數組
c.類變量(static修飾的變量),程序在一加載的時候就在堆中爲類變量分配內存,堆中的內存地址存放在棧中
d.實例變量:當你使用java關鍵字new的時候,系統在堆中開闢並不一定是連續的空間分配給變量,是根據零散的堆內存地址,通過哈希算法換算爲一長串數字以表徵這個變量在堆中的”物理位置”,實例變量的生命週期–當實例變量的引用丟失後,將被GC(垃圾回收器)列入可回收“名單”中,但並不是馬上就釋放堆中內存
e.局部變量: 由聲明在某方法,或某代碼段裏(比如for循環),執行到它的時候在棧中開闢內存,當局部變量一但脫離作用域,內存立即釋放
三,JVM類問題
1、java類加載過程?
來源:
解決方法:
java類加載需要經歷一下7個過程:
(1)加載
加載時類加載的第一個過程,在這個階段,將完成一下三件事情:
1. 通過一個類的全限定名獲取該類的二進制流。
2. 將該二進制流中的靜態存儲結構轉化爲方法去運行時數據結構。
3. 在內存中生成該類的Class對象,作爲該類的數據訪問入口。
(2)驗證
驗證的目的是爲了確保Class文件的字節流中的信息不會危害到虛擬機.在該階段主要完成以下四鍾驗證:
1. 文件格式驗證:驗證字節流是否符合Class文件的規範,如主次版本號是否在當前虛擬機範圍內,常量池中的常量是否有不被支持的類型.
2. 元數據驗證:對字節碼描述的信息進行語義分析,如這個類是否有父類,是否集成了不被繼承的類等。
3. 字節碼驗證:是整個驗證過程中最複雜的一個階段,通過驗證數據流和控制流的分析,確定程序語義是否正確,主要針對方法體的驗證。如:方法中的類型轉換是否正確,跳轉指令是否正確等。
4. 符號引用驗證:這個動作在後面的解析過程中發生,主要是爲了確保解析動作能正確執行。
(3)準備
準備階段是爲類的靜態變量分配內存並將其初始化爲默認值,這些內存都將在方法區中進行分配。準備階段不分配類中的實例變量的內存,實例變量將會在對象實例化時隨着對象一起分配在Java堆中。
public static int value=123;//在準備階段value初始值爲0 。在初始化階段纔會變爲123 。
(4)解析
該階段主要完成符號引用到直接引用的轉換動作。解析動作並不一定在初始化動作完成之前,也有可能在初始化之後。
(5)初始化
初始化時類加載的最後一步,前面的類加載過程,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼
2、類加載器雙親委派模型機制?
當一個類收到了類加載請求時,不會自己先去加載這個類,而是將其委派給父類,由父類去加載,如果此時父類不能加載,反饋給子類,由子類去完成類的加載。
Java類加載器的工作原理及其組織結構:
Java類加載基於三個機制:委託性,可見性和單一性
①委託機制是指雙親委派模型:當一個雷加載和初始化的時候,類僅僅在需要的時候被加載。加載一個類的請求時由application委託給extension,然後再委託給bootstrap類加載。bootstrap類中如果沒有這個類,這個請求又回到extension,如果extension也沒有,會回到
application類中,如果application還是沒有找到,會拋出異常。
雙親委派的優點:提高軟件系統的安全性。因爲用戶自定義的類加載器不可能加載本應該由父類加載器加載的可靠類。
Java類加載器有三個:
啓動類加載器 Bootstrap ClassLoader:負責加載系統類(指的是內置類,像string)
擴展類加載器 Extension ClassLoader:負責加載擴展類(繼承類和實現類)
應用程序類加載器 Application ClassLoader:負責加載應用類(程序自定義的類)
②可見性:是子類加載器可以看到父類加載器加載的類,而父類加載器看不到子類加載器加載的類。
③單一性:指僅加載類一次,確保子類加載器不會加載已經被父類加載過的類。
雙親委派用到的方法:
FindLoadedClass()
loadClass()
findBootstrapClassOrNull()
findClass()
defineClass():把二進制數據轉換成字節碼
ResolveClass()