目錄
5.程序計數器(Program Counter Register)
JVM的內存結構
根據JVM規範,JVM把內存劃分成瞭如下幾個區域:
1.方法區(Method Area)
方法區存放了要加載的類的信息(如類名、修飾符等)、靜態變量、構造函數、final定義的常量、類中的字段和方法等信息。方法區是全局共享的,在一定條件下也會被GC。當方法區超過它允許的大小時,就會拋出OutOfMemory:PermGen Space異常。
在Hotspot虛擬機中,這塊區域對應持久代(Permanent Generation),一般來說,方法區上執行GC的情況很少,因此方法區被稱爲持久代的原因之一,但這並不代表方法區上完全沒有GC,其上的GC主要針對常量池的回收和已加載類的卸載。在方法區上進行GC,條件相當苛刻而且困難。
運行時常量池(Runtime Constant Pool)是方法區的一部分,用於存儲編譯器生成的常量和引用。一般來說,常量的分配在編譯時就能確定,但也不全是,也可以存儲在運行時期產生的常量。比如String類的intern()方法,作用是String類維護了一個常量池,如果調用的字符”hello”已經在常量池中,則直接返回常量池中的地址,否則新建一個常量加入池中,並返回地址。
2.堆區(Heap)
堆區是GC最頻繁的,也是理解GC機制最重要的區域。堆區由所有線程共享,在虛擬機啓動時創建。堆區主要用於存放對象實例及數組,所有new出來的對象都存儲在該區域。
3.虛擬機棧(VM Stack)
虛擬機棧佔用的是操作系統內存,每個線程對應一個虛擬機棧,它是線程私有的,生命週期和線程一樣,每個方法被執行時產生一個棧幀(Statck Frame),棧幀用於存儲局部變量表、動態鏈接、操作數和方法出口等信息,當方法被調用時,棧幀入棧,當方法調用結束時,棧幀出棧。
局部變量表中存儲着方法相關的局部變量,包括各種基本數據類型及對象的引用地址等,因此他有個特點:內存空間可以在編譯期間就確定,運行時不再改變。
虛擬機棧定義了兩種異常類型:StackOverFlowError(棧溢出)和OutOfMemoryError(內存溢出)。如果線程調用的棧深度大於虛擬機允許的最大深度,則拋出StackOverFlowError;不過大多數虛擬機都允許動態擴展虛擬機棧的大小,所以線程可以一直申請棧,直到內存不足時,拋出OutOfMemoryError。
4.本地方法棧(Native Method Stack)
本地方法棧用於支持native(關鍵字)方法(本地方法)的執行,存儲了每個native方法的執行狀態。本地方法棧和虛擬機棧他們的運行機制一致,唯一的區別是,虛擬機棧執行Java方法,本地方法棧執行native方法。在很多虛擬機中(如Sun的JDK默認的HotSpot虛擬機),會將虛擬機棧和本地方法棧一起使用。
5.程序計數器(Program Counter Register)
程序計數器是一個很小的內存區域,不在RAM上,而是直接劃分在CPU上,程序猿無法操作它,它的作用是:JVM在解釋字節碼(.class)文件時,存儲當前線程執行的字節碼行號,只是一種概念模型,各種JVM所採用的方式不一樣。字節碼解釋器工作時,就是通過改變程序計數器的值來取下一條要執行的指令,分支、循環、跳轉等基礎功能都是依賴此技術區完成的。
每個程序計數器只能記錄一個線程的行號,因此它是線程私有的。
如果程序當前正在執行的是一個java方法,則程序計數器記錄的是正在執行的虛擬機字節碼指令地址,如果執行的是native方法,則計數器的值爲空,此內存區是唯一不會拋出OutOfMemoryError的區域。
JVM類加載過程
類被加載到虛擬機內存開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段,其中驗證、準備、解析3個部分統稱爲連接。
1.加載
加載指的是將類的class文件讀入到內存,併爲之創建一個java.lang.Class對象,也就是說,當程序中使用任何類時,系統都會爲之建立一個java.lang.Class對象。
類的加載由類加載器完成,類加載器通常由JVM提供,這些類加載器也是前面所有程序運行的基礎,JVM提供的這些類加載器通常被稱爲系統類加載器。除此之外,開發者可以通過繼承ClassLoader基類來創建自己的類加載器。
通過使用不同的類加載器,可以從不同來源加載類的二進制數據,通常有如下幾種來源。
- 從本地文件系統加載class文件,這是前面絕大部分示例程序的類加載方式。
- 從JAR包加載class文件,這種方式也是很常見的,前面介紹JDBC編程時用到的數據庫驅動類就放在JAR文件中,JVM可以從JAR文件中直接加載該class文件。
- 通過網絡加載class文件,最典型應用就是Applet。
- 把一個Java源文件動態編譯,並執行加載。
類加載器通常無須等到“首次使用”該類時才加載該類,Java虛擬機規範允許系統預先加載某些類。
2.連接
當類被加載之後,系統爲之生成一個對應的Class對象,接着將會進入連接階段,連接階段負責把類的二進制數據合併到JRE中。類連接又可分爲如下3個階段。
2.1.驗證
驗證階段用於檢驗被加載的類是否有正確的內部結構,並和其他類協調一致。其主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
2.2.準備
類準備階段負責爲類的靜態變量分配內存,並設置默認初始值。
2.3.解析
將類的二進制數據中的符號引用替換成直接引用。說明一下:符號引用:符號引用是以一組符號來描述所引用的目標,符號可以是任何的字面形式的字面量,只要不會出現衝突能夠定位到就行。佈局和內存無關。直接引用:是指向目標的指針,偏移量或者能夠直接定位的句柄。該引用是和內存中的佈局有關的,並且一定加載進來的。
3.初始化
初始化是爲類的靜態變量賦予正確的初始值,準備階段和初始化階段看似有點矛盾,其實是不矛盾的,如果類中有語句:private static int a = 10,它的執行過程是這樣的,首先字節碼文件被加載到內存後,先進行鏈接的驗證這一步驟,驗證通過後準備階段,給a分配內存,因爲變量a是static的,所以此時a等於int類型的默認初始值0,即a=0,然後到解析(後面在說),到初始化這一步驟時,才把a的真正的值10賦給a,此時a=10。
4.類加載的時機
- 創建類的實例,也就是new一個對象
- 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
- 調用類的靜態方法
- 反射(Class.forName("com.lyj.load"))
- 初始化一個類的子類(會首先初始化子類的父類)
- JVM啓動時標明的啓動類,即文件名和類名相同的那個類
5.類加載器
- 根類加載器
- 擴展類加載器
- 系統類加載器
類加載機制
- 全盤負責
- 雙親委派
工作原理的是,如果一個類加載器收到了類加載請求,它並不會自己先去加載,而是把這個請求委託給父類的加載器去執行,如果父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器,如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器纔會嘗試自己去加載,這就是雙親委派模式,即每個兒子都很懶,每次有活就丟給父親去幹,直到父親說這件事我也幹不了時,兒子自己纔想辦法去完成。
3. 緩存機制
GC機制
隨着程序的運行,內存中的實例對象、變量等佔據的內存越來越多,如果不及時進行回收,會降低程序運行效率,甚至引發系統異常。
在上面介紹的五個內存區域中,有3個是不需要進行垃圾回收的:本地方法棧、程序計數器、虛擬機棧。因爲他們的生命週期是和線程同步的,隨着線程的銷燬,他們佔用的內存會自動釋放。所以,只有方法區和堆區需要進行垃圾回收,回收的對象就是那些不存在任何引用的對象。
1.查找算法(判斷對象是否存活)
經典的引用計數算法,每個對象添加到引用計數器,每被引用一次,計數器+1,失去引用,計數器-1,當計數器在一段時間內爲0時,即認爲該對象可以被回收了。但是這個算法有個明顯的缺陷:當兩個對象相互引用,但是二者都已經沒有作用時,理應把它們都回收,但是由於它們相互引用,不符合垃圾回收的條件,所以就導致無法處理掉這一塊內存區域。主流java虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的原因時它很難解決對象之間相互循環引用的問題。
因此,Sun的JVM並沒有採用這種算法,而是採用一個叫——根搜索算法(可達性分析),如圖:
基本思想是:從一個叫GC Roots的根節點出發,向下搜索,如果一個對象不能達到GC Roots的時候,說明該對象不再被引用,可以被回收。如上圖中的Object5、Object6、Object7,雖然它們三個依然相互引用,但是它們其實已經沒有作用了,這樣就解決了引用計數算法的缺陷。
補充概念,在JDK1.2之後引入了四個概念:強引用、軟引用、弱引用、虛引用。
強引用:new出來的對象都是強引用,GC無論如何都不會回收,即使拋出OOM異常。
軟引用:只有當JVM內存不足時纔會被回收。
弱引用:只要GC,就會立馬回收,不管內存是否充足。
虛引用:可以忽略不計,JVM完全不會在乎虛引用,你可以理解爲它是來湊數的,湊夠”四大天王”。它唯一的作用就是做一些跟蹤記錄,輔助finalize函數的使用。
最後總結,什麼樣的類需要被回收:
a.該類的所有實例都已經被回收;
b.加載該類的ClassLoad已經被回收;
c.該類對應的反射類java.lang.Class對象沒有被任何地方引用。
1.1.生存還是死亡
要真正宣告一個對象死亡,至少要經過兩次標記過程:在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。
如果對象重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那麼第二次標記時它將被已出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就被真的被回收了。
2.內存分區
內存主要被分爲三塊:新生代(Youn Generation)、舊生代(Old Generation)、持久代(Permanent Generation)。三代的特點不同,造就了他們使用的GC算法不同,新生代適合生命週期較短,快速創建和銷燬的對象,舊生代適合生命週期較長的對象,持久代在Sun Hotpot虛擬機中就是指方法區(有些JVM根本就沒有持久代這一說法)。
新生代(Youn Generation):大致分爲Eden區和Survivor區,Survivor區又分爲大小相同的兩部分:FromSpace和ToSpace。新建的對象都是從新生代分配內存,Eden區不足的時候,會把存活的對象轉移到Survivor區。當新生代進行垃圾回收時會出發Minor GC(也稱作Youn GC)。
舊生代(Old Generation):舊生代用於存放新生代多次回收依然存活的對象,如緩存對象。當舊生代滿了的時候就需要對舊生代進行回收,舊生代的垃圾回收稱作Major GC(也稱作Full GC)。
持久代(Permanent Generation):在Sun 的JVM中就是方法區的意思,儘管大多數JVM沒有這一代。
3.1.判斷對象是否存活算法
引用計數算法、可達性分析算法、
引用計數算法:給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能再被使用的。
主流java虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的原因時它很難解決對象之間相互循環引用的問題。
可達性分析算法:通過一系列的稱爲“GC Roots”的對象作爲起始點。從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈路相連(就是從GC Roots 到這個對象不可達)時,則證明此對象是不可用的,所以它們會被判定爲是可回收的對象。。,
4.GC算法
常見的GC算法:複製、標記-清除和標記-壓縮 分代收集算法
標記-清除:該算法採用的方式是從跟集合開始掃描,對存活的對象進行標記,標記完畢後,再掃描整個空間中未被標記的對象,並進行清除。
在Marking階段,需要進行全盤掃描,這個過程是比較耗時的。
清除階段清理的是沒有被引用的對象,存活的對象被保留。
標記-清除動作不需要移動對象,且僅對不存活的對象進行清理,在空間中存活對象較多的時候,效率較高,但由於只是清除,沒有重新整理,因此會造成內存碎片。
複製:複製算法採用的方式爲從根集合進行掃描,將存活的對象移動到一塊空閒的區域,如圖所示:
當存活的對象較少時,複製算法會比較高效(新生代的Eden區就是採用這種算法),其帶來的成本是需要一塊額外的空閒空間和對象的移動。
標記-壓縮(標記-整理):該算法與標記-清除算法類似,都是先對存活的對象進行標記,但是在清除後會把活的對象向左端空閒空間移動,然後再更新其引用對象的指針
由於進行了移動規整動作,該算法避免了標記-清除的碎片問題,但由於需要進行移動,因此成本也增加了。(該算法適用於舊生代)
分代收集算法:根據對象存活週期的不同將內存劃分爲幾塊。一般是把java堆分爲新生代和老年代。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要符出少量存活對象的複製成本就可以完成收集;而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或者“標記-整理”算法來進行回收。
Serial收集器
Serial(串行)垃圾收集器是最基本、發展歷史最悠久的收集器;
JDK1.3.1前是HotSpot新生代收集的唯一選擇;
1、特點
針對新生代;
採用複製算法;
單線程收集;
進行垃圾收集時,必須暫停所有工作線程,直到完成;
即會"Stop The World";
Serial/Serial Old組合收集器運行示意圖如下:
JVM在後臺自動發起和自動完成的,在用戶不可見的情況下,把用戶正常的工作線程全部停掉,即GC停頓;
會帶給用戶不良的體驗;
從JDK1.3到現在,從Serial收集器-》Parallel收集器-》CMS-》G1,用戶線程停頓時間不斷縮短,但仍然無法完全消除;
CMS收集器
併發標記清理(Concurrent Mark Sweep,CMS)收集器也稱爲併發低停頓收集器(Concurrent Low Pause Collector)或低延遲(low-latency)垃圾收集器;
在前面ParNew收集器曾簡單介紹過其特點;
1、特點
針對老年代;
基於"標記-清除"算法(不進行壓縮操作,產生內存碎片);
以獲取最短回收停頓時間爲目標;
併發收集、低停頓;
需要更多的內存(看後面的缺點);
是HotSpot在JDK1.5推出的第一款真正意義上的併發(Concurrent)收集器;
第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作;
2、應用場景
與用戶交互較多的場景;
希望系統停頓時間最短,注重服務的響應速度;
以給用戶帶來較好的體驗;
如常見WEB、B/S系統的服務器上的應用;
3、設置參數
"-XX:+UseConcMarkSweepGC":指定使用CMS收集器;
4、CMS收集器運作過程
比前面幾種收集器更復雜,可以分爲4個步驟:
(A)、初始標記(CMS initial mark)
僅標記一下GC Roots能直接關聯到的對象;
速度很快;
但需要"Stop The World";
(B)、併發標記(CMS concurrent mark)
進行GC Roots Tracing的過程;
剛纔產生的集合中標記出存活對象;
應用程序也在運行;
並不能保證可以標記出所有的存活對象;
(C)、重新標記(CMS remark)
爲了修正併發標記期間因用戶程序繼續運作而導致標記變動的那一部分對象的標記記錄;
需要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;
採用多線程並行執行來提升效率;
(D)、併發清除(CMS concurrent sweep)
回收所有的垃圾對象;
整個過程中耗時最長的併發標記和併發清除都可以與用戶線程一起工作;
所以總體上說,CMS收集器的內存回收過程與用戶線程一起併發執行;
CMS收集器運行示意圖如下:
5、CMS收集器3個明顯的缺點
(A)、對CPU資源非常敏感:併發收集雖然不會暫停用戶線程,但因爲佔用一部分CPU資源,還是會導致應用程序變慢,總吞吐量降低。當CPU數量多於4個,收集線程佔用的CPU資源多於25%,對用戶程序影響可能較大;不足4個時,影響更大,可能無法接受。
(B)、無法處理浮動垃圾,可能出現"Concurrent Mode Failure"失敗
(C)、產生大量內存碎片
總體來看,與Parallel Old垃圾收集器相比,CMS減少了執行老年代垃圾收集時應用暫停的時間;但卻增加了新生代垃圾收集時應用暫停的時間、降低了吞吐量而且需要佔用更大的堆空間;
G1收集器:
G1(Garbage-First)是JDK7-u4才推出商用的收集器;
1、特點
(A)、並行與併發
能充分利用多CPU、多核環境下的硬件優勢;
可以並行來縮短"Stop The World"停頓時間;
也可以併發讓垃圾收集與用戶程序同時進行;
(B)、分代收集,收集範圍包括新生代和老年代
能獨立管理整個GC堆(新生代和老年代),而不需要與其他收集器搭配;
能夠採用不同方式處理不同時期的對象;
2、應用場景
(1)、超過50%的Java堆被活動數據佔用;
(2)、對象分配頻率或年代提升頻率變化很大;
(3)、GC停頓時間過長(長於0.5至1秒)。