1. JVM的主要組成部分及其作用
-
Class loader(類裝載)
根據給定的全限定名類名(如: java.lang.Object)來裝載class文件到 Runtime data area中的method area。
-
Execution engine(執(zhí)行引擎)
執(zhí)行classes中的指令。
-
Native Interface(本地接口)
與native libraries交互,是其它編程語言交互的接口。
-
Runtime data area(運行時數據區(qū)域)
這就是我們常說的JVM的內存。
首先通過編譯器把 Java 代碼轉換成字節(jié)碼,類加載器(ClassLoader) 再把字節(jié)碼加載到內存中,將其放在運行時數據區(qū)(Runtime data area)的方法區(qū)內,而字節(jié)碼文件只是 JVM 的一套指令集規(guī)范,并不能直接交給底層操作系統(tǒng)去執(zhí)行,因此需要特定的命令解析器執(zhí)行引擎(Execution Engine),將字節(jié)碼翻譯成底層系統(tǒng)指令,再交由 CPU 去執(zhí)行,而這個過程中需要調用其他語言的本地庫接口(Native Interface)來實現整個程序的功能。
2. JVM運行時數據區(qū)/內存模型
-
程序計數器(Program Counter Register)【私有】
當前線程所執(zhí)行的字節(jié)碼的行號指示器,字節(jié)碼解析器的工作是通過改變這個計數器的值,來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉、異常處理、線程恢復等基礎功能,都需要依賴這個計數器來完成;
-
Java 虛擬機棧(Java Virtual Machine Stacks)【私有】
每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態(tài)鏈接、方法出口等信息。
-
本地方法棧(Native Method Stack)【私有】
與虛擬機棧的作用是一樣的,只不過虛擬機棧是服務 Java 方法的,而本地方法棧是為虛擬機調用 Native 方法服務的;
-
Java 堆(Java Heap)【共享】
Java 虛擬機中內存大的一塊,是被所有線程共享的,幾乎所有的對象實例都在這里分配內存;
-
方法區(qū)(Methed Area)【共享】
用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯后的代碼等數據。
-
運行時常量池
運行時常量池是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等信息外,還有一項信息是常量池(Constant Poll Table)用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區(qū)的運行時常量池存放。
其中字符串常量池屬于運行時常量池的一部分,不過在HotSpot虛擬機中,JDK1.7將字符串常量池移到了java堆中。
-
-
直接內存
直接內存不是JVM運行時的數據區(qū)的一部分,也不是Java虛擬機規(guī)范中定義的內存區(qū)域。在JDK1.4中引 入了NIO(New Input/Output)類,引入了一種基于通道(Chanel)與緩沖區(qū)(Buffer)的I/O方式,他可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java中的DirectByteBuffer對象作為對這塊內存 的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java對和Native對中來回復制數據。
-
直接內存(堆外內存)與堆內存比較
- 直接內存申請空間耗費更高的性能,當頻繁申請到一定量時尤為明顯
- 直接內存IO讀寫的性能要優(yōu)于普通的堆內存,在多次讀寫操作的情況下差異明顯
-
使用場景
- 有很大的數據需要存儲,它的生命周期很長
- 適合頻繁的IO操作,例如網絡并發(fā)場景
-
為什么要主動調用System.gc
通過觸發(fā)一次gc操作來回收堆外內存
堆外內存的回收其實依賴于我們的gc機制,首先我們要知道在java層面和我們在堆外分配的這塊內存關聯的只有與之關聯的DirectByteBuffer對象了,它記錄了這塊內存的基地址以及大小,那么既然和gc也有關,那就是gc能通過操作DirectByteBuffer對象來間接操作對應的堆外內存了。DirectByteBuffer對象在創(chuàng)建的時候關聯了一個PhantomReference, 說到PhantomReference它其實主要是用來跟蹤對象何時被回收的,它不能影響gc決策,但是gc過程中如果發(fā)現某個對象除了只有PhantomReference引用它之外,并沒有其他的地方引用它了,那將會把這個引用放到java.lang.ref.Reference.pending隊列里,在gc完畢的時候通知ReferenceHandler這個守護線程去執(zhí)行一些后置處理,而DirectByteBuffer關聯的PhantomReference是PhantomReference的一個子類,在最終的處理里會通過Unsafe的free接口來釋放DirectByteBuffer對應的堆外內存塊
-
-
版本變化
-
Jdk1.6及之前:有永久代, 常量池在方法區(qū)
-
Jdk1.7:有永久代,但已經逐步“去永久代”,常量池在堆
-
Jdk1.8及之后:無永久代,常量池在元空間
-
3. 堆棧的區(qū)別
-
物理地址
堆的物理地址分配對對象是不連續(xù)的。因此性能慢些。
棧使用的是數據結構中的棧,先進后出的原則,物理地址分配是連續(xù)的。所以性能快。
-
內存分配
堆因為是不連續(xù)的,所以分配的內存是在運行期確認的,因此大小不固定。
一般堆大小遠遠大于棧。 棧是連續(xù)的,所以分配的內存大小要在編譯期就確認,大小是固定的。
-
存放的內容
堆存放的是對象的實例和數組。因此該區(qū)更關注的是數據的存儲
棧存放:局部變量,操作數棧,返回結果。該區(qū)更關注的是程序方法的執(zhí)行。
靜態(tài)變量放在方法區(qū),靜態(tài)的對象還是放在堆
-
程序的可見度
堆對于整個應用程序都是共享、可見的。
棧只對于線程是可見的。所以也是線程私有。他的生命周期和線程相同。
-
棧解決程序的運行問題,即程序如何執(zhí)行,或者說如何處理數據;
堆解決的是數據存儲的問題,即數據怎么放、放在哪兒。
4. 對象創(chuàng)建的五種方法
- new關鍵字
- Class的newInstance方法
- Constructor類的newInstance方法
- clone方法
- 反序列化
5. new的過程
-
虛擬機遇到一條new指令時,先檢查常量池是否已經加載相應的類,如果沒有,必須先執(zhí)行相應的類加載。
-
類加載通過后,接下來分配內存。
-
若Java堆中內存是絕對規(guī)整的,使用“指針碰撞“方式分配內存;
即所有用過的內存放在一邊,而空閑的的放在另一邊。分配內存時將位于中間的指針指示器向空閑的內存移動一段與對象大小相等的距離,這樣便完成分配內存工作。
-
如果不是規(guī)整的,就從空閑列表中分配,叫做”空閑列表“方式。
-
劃分內存時還需要考慮一個問題-并發(fā),也有兩種方式:
- CAS同步處理,
- 本地線程分配緩沖(Thread Local Allocation Buffer, TLAB)。
-
-
然后內存空間初始化操作,接著是做一些必要的對象設置(元信息、哈希碼…),后執(zhí)行方法。
-
對象的訪問定位
-
句柄
優(yōu)勢:引用中存儲的是穩(wěn)定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而引用本身不需要修改。
-
直接指針
優(yōu)勢:速度更快,節(jié)省了一次指針定位的時間開銷。由于對象的訪問在Java中非常頻繁,因此這類開銷積少成多后也是非常可觀的執(zhí)行成本。HotSpot 中采用的就是這種方式。
-
6. Java 中都有哪些引用類型
-
強引用(StrongReference)
強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的 對象來解決內存不足的問題。
-
軟引用(SoftReference)
如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。
-
弱引用(WeakReference)
弱引用與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內存區(qū)域的過程中,一旦發(fā)現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由于垃圾回收器是一個優(yōu)先級很低的線程,因此不一定會很快發(fā)現那些只具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收, Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。
-
虛引用(PhantomReference)
虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。 虛引用主要用來跟蹤對象被垃圾回收器回收的活動。
虛引用與軟引用和弱引用的一個區(qū)別在于:虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發(fā)現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。如果程序發(fā)現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。
7. 如何判斷對象是否可以被回收
-
引用計數器法
為每個對象創(chuàng)建一個引用計數,有對象引用時計數器 +1,引用被釋放時計數 -1, 當計數器為 0 時就可以被回收。它有一個缺點不能解決循環(huán)引用 的問題;
-
可達性分析算法
從 GC Roots 開始向下搜索,搜索所走過的路徑稱為引用鏈。 當一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是可以被回收的。
8. 垃圾回收算法
-
標記-清除算法:標記無用對象,然后進行清除回收。
-
優(yōu)點:實現簡單,不需要對象進行移動。
-
缺點:效率不高,無法清除垃圾碎片。
-
-
復制算法:按照容量劃分二個大小相等的內存區(qū)域,當一塊用完的時候將活著的對象復制到另一塊上,然后再把已使用的內存空間一次清理掉。
-
優(yōu)點:按順序分配內存即可,實現簡單、運行高效,不用考慮內存碎片。
-
缺點:內存使用率不高,只有原來的一半。
-
-
標記-整理算法:標記無用對象,讓所有存活的對象都向一端移動,然后直接清除掉端邊界以外的內存。
- 優(yōu)點:解決了標記-清理算法存在的內存碎片問題。
- 缺點:仍需要進行局部對象移動,一定程度上降低了效率。
-
分代算法:根據對象存活周期的不同將內存劃分為幾塊,一般是新生代和老年代,新生代基本采用復制算法,老年代采用標記整理算法。
- 并行算法是用多線程進行垃圾回收,回收期間會暫停程序的執(zhí)行,而并發(fā)算法,也是多線程回收,但期間不停止應用執(zhí)行。
9. 垃圾回收器
-
Serial收集器(復制算法)
新生代單線程收集器,標記和清理都是單線程
- 優(yōu)點:簡單高效
-
ParNew收集器 (復制算法)
新生代收并行集器,實際上是Serial收集器的多線程版本,在多核CPU 環(huán)境下有著比Serial更好的表現;
-
Parallel Scavenge收集器 (復制算法)
- 新生代并行收集器,追求高吞吐量,高效利用 CPU。
吞吐量 = 用戶線程時間/(用戶線程時間+GC線程時間),高吞吐量可以高效率的利用CPU時間,盡快完成程序的運算任務,適合后臺應用等對交互相應要求不高的場景;
-
Serial Old收集器 (標記-整理算法)
老年代單線程收集器,Serial收集器的老年代版本;
-
Parallel Old收集器 (標記-整理算法)
老年代并行收集器,吞吐量優(yōu)先
Parallel Scavenge收集器的老年代版本
-
CMS(Concurrent Mark Sweep)收集器(標記-清除算法)
-
老年代并行收集器,以獲取最短回收停頓時間為目標的收集器,具有高并發(fā)、低停頓的特點,追求最短GC回收停頓時間。
-
CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器。對于要求服務器響應速度的應用上,這種垃圾回收器非常適合。
-
在啟動 JVM 的參數加上“-XX:+UseConcMarkSweepGC”來指定使用 CMS 垃圾回收器。
-
CMS 使用的是標記-清除的算法實現的, 所以在gc的時候回產生大量的內存碎片,當剩余內存不能滿足程序運行要求時,系統(tǒng)將會出現 Concurrent Mode Failure,臨時CMS 會采用 Serial Old 回收器進行垃圾清除,此時的性能將會被降低。
-
整個過程分為6個步驟,其中初始標記、并發(fā)標記這兩個步驟仍然需要“Stop The World”
-
初始標記(CMS-initial-mark)
為了收集應用程序的對象引用需要暫停應用程序線程,該階段完成后,應用程序線程再次啟動。
-
并發(fā)標記(CMS-concurrent-mark)
從第一階段收集到的對象引用開始,遍歷所有其他的對象引用,此階段會打印2條日志:CMS-concurrent-mark-start,CMS-concurrent-mark。
-
并發(fā)預清理(CMS-concurrent-preclean)
改變當運行第二階段時,由應用程序線程產生的對象引用,以更新第二階段的結果。 此階段會打印2條日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean。
-
CMS-concurrent-abortable-preclean
加入此階段的目的是使cms gc更加可控一些,作用也是執(zhí)行一些預清理,以減少Rescan階段造成應用暫停的時間,通過兩個參數來來控制是否進行下一階段:
- -XX:CMSScheduleRemarkEdenSizeThreshold(默認2M):即當eden使用小于此值時;
- -XX:CMSScheduleRemarkEdenPenetratio(默認50%):在concurrent preclean階段之后,如果Eden占用率高于CMSScheduleRemarkEdenSizeThreshold,開啟’concurrent abortable preclean’,并且持續(xù)的precleanig直到Eden占比超過CMSScheduleRemarkEdenPenetratio,之后,開啟remark階段
-
重標記CMS-concurrent-remark
由于上面三階段是并發(fā)的,對象引用可能會發(fā)生進一步改變。因此,應用程序線程會再一次被暫停以更新這些變化,并且在進行實際的清理之前確保一個正確的對象引用視圖。這一階段十分重要,因為必須避免收集到仍被引用的對象。
-
并發(fā)清理(CMS-concurrent-sweep)
所有不再被應用的對象將從堆里清除掉。
-
并發(fā)重置(CMS-concurrent-reset)
收集器做一些收尾的工作,以便下一次GC周期能有一個干凈的狀態(tài)。
-
-
參數控制
-
-XX:+UseConcMarkSweepGC:
使用CMS收集器,-XX:UseParNewGC會自動開啟。因此,如果年輕代的并行GC不想開啟,可以通過設置-XX:-UseParNewGC來關掉
-
-XX:+CMSClassUnloadingEnabled
相對于并行收集器,CMS收集器默認不會對永久代進行垃圾回收。
-
-XX:+CMSConcurrentMTEnabled
當該標志被啟用時,并發(fā)的CMS階段將以多線程執(zhí)行(因此,多個GC線程會與所有的應用程序線程并行工作)。該標志已經默認開啟,如果順序執(zhí)行更好,這取決于所使用的硬件,多線程執(zhí)行可以通過-XX:-CMSConcurremntMTEnabled禁用(注意是-號)。
-
-XX:+UseCMSCompactAtFullCollection
Full GC后,進行一次碎片整理;整理過程是獨占的,會引起停頓時間變長
-
-XX:+CMSFullGCsBeforeCompaction
設置進行幾次Full GC后,進行一次碎片整理
-
-XX:ParallelCMSThreads
設定CMS的線程數量(一般情況約等于可用CPU數量)
-
-XX:CMSMaxAbortablePrecleanTime
當abortable-preclean階段執(zhí)行達到這個時間時才會結束
-
-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly來決定什么時間開始垃圾收集;如果設置了-XX:+UseCMSInitiatingOccupancyOnly,那么只有當old代占用確實達到了-XX:CMSInitiatingOccupancyFraction參數所設定的比例時才會觸發(fā)cms gc;如果沒有設置,系統(tǒng)會根據統(tǒng)計數據自行決定什么時候觸發(fā)cms gc;因此有時會遇到設置了80%比例才cms gc,但是50%時就已經觸發(fā)了,就是因為這個參數沒有設置的原因。
-
-
-
G1(Garbage First)收集器 (標記-整理算法)
Java堆并行收集器,G1收集器是 JDK1.7提供的一個新收集器,G1收集器基于“標記-整理”算法實現,也就是說不會產生內存碎片。
-
G1收集器不同于之前的收集器的一個重要特點是:G1回收的范圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的范圍僅限于新生代或老年代。
-
G1在壓縮空間方面有優(yōu)勢
- G1通過將內存空間分成區(qū)域(Region)的方式避免內存碎片問題
- Eden, Survivor, Old區(qū)不再固定、在內存使用效率上來說更靈活
- G1可以通過設置預期停頓時間(Pause Time)來控制垃圾收集時間避免應用雪崩現象
- G1在回收內存后會馬上同時做合并空閑內存的工作、而CMS默認是在STW(stop the world)的時候做
- G1會在Young GC中使用、而CMS只能在O區(qū)使用
-
以下場景下G1更適合:
- Full GC 次數太頻繁或者消耗時間太長
- 應用在運行過程中會產生大量內存碎片、需要經常壓縮空間
- 想要更可控、可預期的GC停頓周期;防止高并發(fā)下應用雪崩現象
- 對象分配的頻率或代數提升(promotion)顯著變化
- 太長的垃圾回收或內存整理時間(超過0.5~1秒)
-
參數
-
-XX:+UseG1GC
使用G1 GC
-
-XX:MaxGCPauseMillis=n
設置一個暫停時間期望目標,這是一個軟目標,JVM會近可能的保證這個目標
-
-XX:InitiatingHeapOccupancyPercent=n
內存占用達到整個堆百分之多少的時候開啟一個GC周期,G1 GC會根據整個棧的占用,而不是某個代的占用情況去觸發(fā)一個并發(fā)GC周期,0表示一直在 GC,默認值是45
-
-XX:NewRatio=n
年輕代和老年代大小的比例,默認是2
-
-XX:SurvivorRatio=n
eden和survivor區(qū)域空間大小的比例,默認是8
-
-XX:MaxTenuringThreshold=n
晉升的閾值,默認是15(譯者注:一個存活對象經歷多少次GC周期之后晉升到老年代)
-
-XX:ParallelGCThreads=n
GC在并行處理階段試驗多少個線程,默認值和平臺有關。
-
-XX:ConcGCThreads=n
并發(fā)收集的時候使用多少個線程,默認值和平臺有關。
-
-XX:G1ReservePercent=n
預留多少內存,防止晉升失敗的情況,默認值是10
-
-XX:G1HeapRegionSize=n
G1 GC的堆內存會分割成均勻大小的區(qū)域,這個值設置每個劃區(qū)域的大小,這個值的默認值是根據堆的大小決定的。最小值是1Mb,最大值是32Mb
-
-
10. 分代垃圾回收器是怎么工作的
-
分代回收器有兩個分區(qū):老生代和新生代,新生代默認的空間占比總空間的 1/3,老生代的默認占比是 2/3。
-
新生代使用的是復制算法,新生代里有 3 個分區(qū):Eden、To Survivor、From Survivor,它們的默認占比是 8:1:1,它的執(zhí)行流程如下:
- 把 Eden + From Survivor 存活的對象放入 To Survivor 區(qū);
- 清空 Eden 和 From Survivor 分區(qū);
- From Survivor 和 To Survivor 分區(qū)交換;
- 每次在 From Survivor 到 To Survivor 移動時都存活的對象,年齡就 +1,當年齡到達 15(默認配置是 15)時,升級為老生代。大對象也會直接進入老生代。
-
老生代當空間占用到達某個值之后就會觸發(fā)全局垃圾收回,一般使用標記整理的執(zhí)行算法。以上這些循環(huán)往復就構成了整個分代垃圾回收的整體執(zhí)行流程。
11. 內存分配策略
-
對象優(yōu)先在 Eden 區(qū)分配
-
大對象直接進入老年代
虛擬機提供了一個 -XX:PretenureSizeThreshold 參數,令大于這個設置值的對象直接在老年代分配。
-
長期存活對象將進入老年代
-
動態(tài)對象年齡判定
為了能更好地適應不同程序的內存狀況,虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代.
如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到 MaxTenuringThreshold 中要求的年齡。
-
空間分配擔保
-
在發(fā)生Minor GC之前,虛擬機會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是安全的。
-
如果不成立,則虛擬機會查看 HandlePromotionFailure設置值是否允許擔保失敗。
-
如果允許,那么會繼續(xù)檢查老年代最大可用的連續(xù) 空間是否大于歷次晉升到老年代對象的平均大小
-
如果大于,將嘗試著進行一次Minor GC,盡管這次 Minor GC是有風險的
-
如果小于,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC。
-
-
12. 類加載機制
- 隱式裝載, 程序在運行過程中當碰到通過new 等方式生成對象時,隱式調用 類裝載器加載對應的類到jvm中,
- 顯式裝載, 通過class.forname() 等方法,顯式加載需要的類
(1)類加載器分類
實現通過類的權限定名獲取該類的二進制字節(jié)流的代碼塊叫做類加載器。 主要有一下四種類加載器:
-
啟動類加載器(Bootstrap ClassLoader):
用來加載java核心類庫,無法被 java程序直接引用。
-
擴展類加載器(extensions class loader)
它用來加載 Java 的擴展庫。 Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載 Java 類。
-
系統(tǒng)類加載器(system class loader)
它根據 Java 應用的類路徑 (CLASSPATH) 來加載 Java類。一般來說,Java 應用的類都是由它來完成加載的。可以通過ClassLoader.getSystemClassLoader()來獲取它。
-
用戶自定義類加載器
通過繼承 java.lang.ClassLoader類的方式實現,然后覆蓋findClass()方法。
ClassLoader超類的loadClass方法用于將類的加載操作委托給父類加載器去進行,只有該類尚未加載并且父類加載器也無法加載該類時,才調用findClass()方法。 如果要實現該方法,必須做到以下幾點:
- 為來自本地文件系統(tǒng)或者其他來源的類加載其字節(jié)碼
- 調用ClassLoader超類的defineClass()方法,向虛擬機提供字節(jié)碼
(2)類裝載過程
-
加載
根據查找路徑找到相應的 class 文件然后導入;
- 通過一個類的全限定名來獲取其定義的二進制字節(jié)流。
- 將這個字節(jié)流所代表的靜態(tài)存儲結構轉化為方法區(qū)的運行時數據結構。
- 在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區(qū)中這些數據的訪問入口。
-
驗證
檢查加載的 class 文件的正確性;
-
準備
給類中的靜態(tài)變量分配內存空間,并將其初始化為默認值
-
解析
虛擬機將常量池中的符號引用替換成直接引用的過程。
符號引用就理解為一個標示,而在直接引用直接指向內存中的地址;
-
初始化
對靜態(tài)變量和靜態(tài)代碼塊執(zhí)行初始化工作。
為類的靜態(tài)變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化,包括給聲明類變量指定初始值,和為類static變量指定靜態(tài)代碼塊地址。
-
OSGi
OSGi 是 Java 上的動態(tài)模塊系統(tǒng)。它為開發(fā)人員提供了面向服務和基于組件的運行環(huán)境,并提供標準的方式用來管理軟件的生命周期。
13. JVM調優(yōu)
(1) 工具
- jconsole:用于對 JVM 中的內存、線程和類等進行監(jiān)控;
- jvisualvm:JDK 自帶的全能分析工具,可以分析:內存快照、線程快照、程序死鎖、監(jiān)控內存的變化、gc 變化等。
- 命令行:
- top查看CPU使用情況,或通過CPU使用率收集,找到CPU占用率高Java進程,假設其進程編號為pid;
- 使用top -Hp pid(或ps -Lfp pid或者ps -mp pid -o THREAD,tid,time)查看pid進程中占用CPU較高的線程,假設其編號為tid;
- 使用Linux命令,將tid轉換為16進制數,printf ‘%0x\n’ tid,假設得到值txid;
- 使用jstack pid | grep txid查看線程CPU占用代碼,然后根據得到的對象信息,去追蹤代碼,定位問題。
(2) 參數
- -Xms2g:初始化推大小為 2g;
- -Xmx2g:堆最大內存為 2g;
- -XX:NewRatio=4:設置年輕的和老年代的內存比例為 1:4;
- -XX:SurvivorRatio=8:設置新生代 Eden 和 Survivor 比例為 8:2;
- -XX:NewSize:設置新生代最小空間大小。
- -XX:MaxNewSize:設置新生代最大空間大小。
- -XX:PermSize:設置永久代最小空間大小。
- -XX:MaxPermSize:設置永久代最大空間大小。
- -Xss:設置每個線程的堆棧大小。
- –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器組合;
- -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器組合;
- -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器組合;
- -XX:+PrintGC:開啟打印 gc 信息;
- -XX:+PrintGCDetails:打印 gc 詳細信息。
(3)調優(yōu)思路
-
如果滿足下面的指標,則一般不需要進行GC優(yōu)化:
- Minor GC執(zhí)行時間不到50ms;約10秒一次;
- Full GC執(zhí)行時間不到1s;不低于10分鐘1次;
-
線程池(java.util.concurrent.ThreadPoolExecutor)
解決用戶響應時間長的問題
-
corePoolSize:核心線程數(最新線程數)
-
maximumPoolSize:最大線程數
超過這個數量的任務會被拒絕,用戶可以通過 RejectedExecutionHandler接口自定義處理方式
-
keepAliveTime:線程保持活動的時間
-
workQueue:工作隊列,存放執(zhí)行的任務
Java線程池需要傳入一個Queue參數(workQueue)用來存放執(zhí)行的任務,而對Queue的不同選擇,線程池有完全不同的行為:
- SynchronousQueue:一個無容量的等待隊列,一個線程的insert操作必須等待另一線程的remove操作,采用這個Queue線程池將會為每個任務分配一個新線程
- LinkedBlockingQueue:無界隊列,采用該Queue,線程池將忽略maximumPoolSize參數,僅用corePoolSize的線程處理所有的任務,未處理的任務便在 LinkedBlockingQueue中排隊
- ArrayBlockingQueue:有界隊列,在有界隊列和maximumPoolSize的作用下,程序將很難被調優(yōu):更大的Queue和小的maximumPoolSize將導致CPU的低負載;小的Queue和大的池,Queue就沒起動應有的作用。
封裝方式:
- 以SynchronousQueue作為參數,使maximumPoolSize發(fā)揮作用,以防止線程被無限制的分配, 同時可以通過提高maximumPoolSize來提高系統(tǒng)吞吐量
- 自定義一個RejectedExecutionHandler,當線程數超過maximumPoolSize時進行處理,處理方式為隔一段時間檢查線程池是否可以執(zhí)行新Task,如果可以把拒絕的Task重新放入到線程池,檢查的時間依賴keepAliveTime的大小。
-
-
連接池(org.apache.commons.dbcp.BasicDataSource)
在使用org.apache.commons.dbcp.BasicDataSource的時候,因為之前采用了默認配置,所以當訪問量大時,通過JMX觀察到很多Tomcat線程都阻塞在BasicDataSource使用的Apache ObjectPool的鎖上,直接原因當時是因為BasicDataSource連接池的最大連接數設置的太小,默認的BasicDataSource 配置,僅使用8個最大連接。
當較長的時間不訪問系統(tǒng),比如2天,DB上的Mysql會斷掉所以的連接,導致連接池中緩存的連接不能用。
Mysql默認支持100個鏈接,所以每個連接池的配置要根據集群中的機器數進行,如有2臺服務器, 可每個設置為60
-
initialSize:一直打開的連接數
-
minEvictableIdleTimeMillis:該參數設置每個連接的空閑時間,超過這個時間連接將被關閉
-
timeBetweenEvictionRunsMillis:后臺線程的運行周期,用來檢測過期連接
-
maxActive:最大能分配的連接數
-
maxIdle:最大空閑數,當連接使用完畢后發(fā)現連接數大于maxIdle,連接將被直接關閉。只有 initialSize < x < maxIdle的連接將被定期檢測是否超期。這個參數主要用來在峰值訪問時提高吞吐量。
-
如何保存連接
BasicDataSource會關閉所有超期的連接,然后再打開initialSize數量的連接,這個特性與minEvictableIdleTimeMillis、 timeBetweenEvictionRunsMillis一起保證了所有超期的initialSize連接都會被重新連接,從而避免了Mysql長時間無動作會斷掉連接的問題。
-
-
JVM啟動參數:調整各代的內存比例和垃圾回收算法,提高吞吐量
-
目標:
- GC的時間足夠的小
- GC的次數足夠的少
- 發(fā)生Full GC的周期足夠的長
前兩個目前是相悖的,要想GC時間小必須要一個更小的堆,要保證GC次數足夠少,必須保證一個更大的堆,我們只能取其平衡。
-
針對JVM堆的設置,一般可以通過-Xms -Xmx限定其最小、最大值,為了防止垃圾收集器在最小、最大之間收縮堆而產生額外的時間,我們通常把最大、最小設置為相同的值
-
年輕代和年老代將根據默認的比例(1:2)分配堆內存,可以通過調整二者之間的比率 NewRadio來調整二者之間的大小,也可以針對回收代,比如年輕代,通過 -XX:newSize - XX:MaxNewSize來設置其絕對大小。同樣,為了防止年輕代的堆收縮,我們通常會把-XX:newSize - XX:MaxNewSize設置為同樣大小
如果應用存在大量的臨時對象,應該選擇更大的年輕代;如果存在相對較多的持久對象,年老代應該適當增大。但很多應用都沒有這樣明顯的特性,在抉擇時應該根據以下兩點
本著Full GC盡量少的原則,讓年老代盡量緩存常用對象,JVM的默認比例1:2也是這個道理
-
在配置較好的機器上(比如多核、大內存),可以為年老代選擇并行收集算法**: **- XX:+UseParallelOldGC,默認為Serial收集
-
線程堆棧的設置
每個線程默認會開啟1M的堆棧,用于存放棧幀、調用參數、局部變量等,對大多數應用而言這個默認值太大了,一般256K就足用。理論上,在內存不變的情況下,減少每個線程的堆棧,可以產生更多的線程,但這實際上還受限于操作系統(tǒng)。
-
可以通過下面的參數打Heap Dump信息
- -XX:HeapDumpPath
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:/usr/aaa/dump/heap_trace.txt
- XX:+HeapDumpOnOutOfMemoryError
-
-
程序算法:改進程序邏輯算法提高性能
(4)原則
- 多數的Java應用不需要在服務器上進行GC優(yōu)化;
- 多數導致GC問題的Java應用,都不是因為我們參數設置錯誤,而是代碼問題;
- 在應用上線之前,先考慮將機器的JVM參數設置到最優(yōu)(最適合);
- 減少創(chuàng)建對象的數量;
- 減少使用全局變量和大對象;
- GC優(yōu)化是到最后不得已才采用的手段;
- 在實際使用中,分析GC情況優(yōu)化代碼比優(yōu)化GC參數要多得多;
(5)層級 409
-
架構調優(yōu)
-
代碼調優(yōu)
算法、數據結構
-
JVM調優(yōu)
垃圾回收器、內存分配
-
數據庫調優(yōu)
數據表分配、sql優(yōu)化
-
操作系統(tǒng)調優(yōu)
(6) 性能定義
-
吞吐量
重要指標之一,是指不考慮垃圾收集引起的停頓時間或內存消耗,垃圾收集器能支撐應用達到的最高性能指標。
-
延遲
其度量標準是縮短由于垃圾啊收集引起的停頓時間或者完全消除因垃圾收集所引起的停頓,避免應用運行時發(fā)生抖動。
-
內存占用
垃圾收集器流暢運行所需要的內存數量。
這三個屬性中,其中一個任何一個屬性性能的提高,幾乎都是以另外一個或者兩個屬性性能的損失作代價,不可兼得,具體某一個屬性或者兩個屬性的性能對應用來說比較重要,要基于應用的業(yè)務需求來確定。
14. Minor GC、Major GC、Full GC
-
Minor GC 是指發(fā)生在新生代的 GC,因為 Java 對象大多都是朝生夕死,所有 Minor GC 非常頻繁,一般回收速度也非???
-
Major GC是指發(fā)生在老年代的 GC,OldGen區(qū)內存不足,觸發(fā)Major GC,出現了 Major GC 通常會伴隨至少一次 Minor GC。 Major GC 的速度通常會比 Minor GC 慢 10 倍以上。
-
Full GC
-
清理整個堆空間—包括年輕代和永久代
-
觸發(fā)的場景
-
System.gc
-
promotion failed (年代晉升失敗,比如eden區(qū)的存活對象晉升到S區(qū)放不下,又嘗試直接晉升到Old 區(qū)又放不下,那么Promotion Failed,會觸發(fā)FullGC)
-
CMS的Concurrent-Mode-Failure
CMS回收過程中主要分為四步:
- CMS initial mark
- CMS Concurrent mark
- CMS remark
- CMS Concurrent sweep。
在2中gc線程與用戶線程同時執(zhí)行,那么用戶線程依舊可能同時產生垃圾, 如果這個垃圾較多無法放入預留的空間就會產生CMS-Mode-Failure, 切換為SerialOld單線程做mark-sweep-compact。
-
新生代晉升的平均大小大于老年代的剩余空間 (為了避免新生代晉升到老年代失敗)
- 當使用 G1、CMS 時,FullGC發(fā)生的時候是Serial+SerialOld。
- 當使用ParalOld時,FullGC發(fā)生的時候是 ParallNew+ParallOld.
-
-
15. 類文件結構
16. User user = new User() 做了什么操作,申請了哪些內存?
- new User(); 創(chuàng)建一個User對象,內存分配在堆上
- User user; 創(chuàng)建一個引用,內存分配在棧上
- = 將User對象地址賦值給引用
17. 內存泄漏及解決方法
-
系統(tǒng)崩潰前的一些現象:
- 每次垃圾回收的時間越來越長,由之前的10ms延長到50ms左右,FullGC的時間也有之前的0.5s延 長到4、5s
- FullGC的次數越來越多,最頻繁時隔不到1分鐘就進行一次FullGC
- 年老代的內存越來越大并且每次FullGC后年老代沒有內存被釋放
- 之后系統(tǒng)會無法響應新的請求,逐漸到達OutOfMemoryError的臨界值。
-
解決方法
-
生成堆的dump文件
-
分析dump文件
Eclipse專門的靜態(tài)內存分析工具:Mat。
-
分析內存泄漏
通過Mat我們能清楚地看到,哪些對象被懷疑為內存泄漏,哪些對象占的空間最大及對象的調用關系。
-
回歸問題
-
為什么崩潰前垃圾回收的時間越來越長?
根據內存模型和垃圾回收算法,垃圾回收分兩部分:內存標記、清除(復制),標記部分只要內存大小固定時間是不變的,變的是復制部分,因為每次垃圾回收都有一些回收不掉的內存,所以增加了復制量,導致時間延長。所以,垃圾回收的時間也可以作為判斷內存泄漏的依據
-
為什么Full GC的次數越來越多?
因此內存的積累,逐漸耗盡了年老代的內存,導致新對象分配沒有更多的空間,從而導致頻繁的垃圾回收
-
為什么年老代占用的內存越來越大?
因為年輕代的內存無法被回收,越來越多地被Copy到年老代
-
-
-
怎樣阻止內存泄露
- 使用List、Map等集合時,在使用完成后賦值為null
- 使用大對象時,在用完后賦值為null
- 目前已知的jdk1.6的substring()方法會導致內存泄露
- 避免一些死循環(huán)等重復創(chuàng)建或對集合添加元素,撐爆內存
- 簡潔數據結構、少用靜態(tài)集合等
- 及時的關閉打開的文件,socket句柄等
- 多關注事件監(jiān)聽(listeners)和回調(callbacks),比如注冊了一個listener,當它不再被使用的時候,忘了注銷該listener,可能就會產生內存泄露
18. HotSpot逃逸分析
19. Java中的常量池
-
靜態(tài)常量池
即.class文件中的常量池,class文件中的常量池不僅僅包含字符串(數字)字面量, 還包含類、方法的信息,占用class文件絕大部分空間。
-
運行時常量池
則是jvm虛擬機在完成類裝載操作后,將class文件中的常量池載入到內存中,并保存在方法區(qū)中,我們常說的常量池,就是指方法區(qū)中的運行時常量池。
20. -XX:+UseCompressedOops 的作用
? 當你將你的應用從 32 位的 JVM 遷移到 64 位的 JVM 時,由于對象的指針從 32 位增加到了 64 位,因此堆內存會突然增加,差不多要翻倍。這也會對 CPU 緩存(容量比內存小很多)的數據產生不利的影響。 因為,遷移到 64 位的 JVM 主要動機在于可以指定最大堆大小,通過壓縮 OOP 可以節(jié)省一定的內存。 通過 -XX:+UseCompressedOops 選項,JVM 會使用 32 位的 OOP,而不是 64 位的 OOP。
21. 內存溢出和內存泄漏的區(qū)別
-
內存溢出 out of memory
是指程序在申請內存時,沒有足夠的內存空間供其使用,出現out of memory
-
內存泄露 memory leak
是指程序在申請內存后,無法釋放已申請的內存空間,一次內存泄露危害可以忽略,但內存泄露堆積后果很嚴重,無論多少內存,遲早會被占光。
-
實現思路
-
棧內存溢出(StackOverflowError)
- 程序所要求的棧深度過大導致,可以寫一個死遞歸程序觸發(fā)。
-
堆內存溢出(OutOfMemoryError:java heap space)
-
泄露則看對象如何被 GC Root 引用。
-
溢出則通過 調大 -Xms,-Xmx參數。文章來源:http://www.zghlxwxcb.cn/news/detail-838099.html
-
-
持久帶內存溢出(OutOfMemoryError: PermGen space)文章來源地址http://www.zghlxwxcb.cn/news/detail-838099.html
- 用String.intern()觸發(fā)常量池溢出
- Class對象未被釋放,Class對象占用信息過多,有過多的Class對象??梢詫е鲁志脦却嬉绯?/li>
-
22. threadlocal
23. AOP面向切面編程
- AOP 專門用于處理系統(tǒng)中分布于各個模塊(不同方法)中的交叉關注點的問題,在 Java EE 應用中,常常通過AOP來處理一些具有橫切性質的系統(tǒng)級服務,如事務管理、安全檢查、緩存、對象池管理等,在不改變已有代碼的情況下,靜態(tài)/動態(tài)的插入代碼。
- 將AOP放到這里的主要原因是因為AOP改變的class文件,達到嵌入方法的目的, 使用 AspectJ進行由.java到.class文件編譯。而使用CGLIB載入使用javac編譯的.class文件后,使用動態(tài)代理的方式,將要執(zhí)行的方法嵌入到原有class方法中,完成在內存中對class對象的 式模態(tài)動式模態(tài)靜動態(tài)方式在.class載入時需要做額外的處理,導致性能受到一定影響,但其優(yōu)勢是無須使用額外的構造,這也就是所謂 的內在原理。同時靜態(tài)方式在載入前已經修好完.class文件,而術技理代態(tài)動 編譯器??傮w的技術的切入點在于在修改機器執(zhí)行碼,達到增加執(zhí)行方法的目的。
24. StringTable
到了這里,關于JAVA后端開發(fā)面試基礎知識(一)——JVM的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!