在C++中我們需要手動申請內存然后釋放內存,否則就會出現對象已經不再使用內存卻仍被占用的情況。在Java中JVM內置了垃圾回收的機制,幫助開發者承擔對象的創建和釋放的工作,極大的減輕了開發的負擔。那是不是我們就不需要了解JVM了,顯然在做一些優化或者深入研究應用性能的時候,JVM還是起了很關鍵的作用的。
Java內存模型結構分為線程共享區和線程私有區
• 線程共享區:堆、方法區
• 線程私有區:虛擬機棧、本地方法棧、程序 計數器
堆:用於存放對象實例和數組,由於堆是用來存放對象實例,因此堆也是垃圾收集器管理的主要區域,故也稱為GC堆。由於現在的垃圾收集器基本都采用分代收集算法,所以堆的內部結構只包含新生代和老年代。
方法區:用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。
方法區通常和永久區(Perm)關聯在一起,但永久代與方法區不是一個概念,只是有的虛擬機用永久代來實現方法區,這樣就可以用永久代GC來管理方法區,省去專門內存管理的工作
根據Java虛擬機規范的規定,當方法區無法滿足內存分配的需求時,將拋出 OutOfMemoryError 異常
虛擬機棧
每個方法在執行的時候都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息
每個方法從調用直至完成的過程,對應一個棧幀在虛擬機棧中入棧到出棧的過程
局部變量表主要存放一些基本類型的變量和對象句柄,它們可以是方法參數,也可以是方法的局部變量
程序計數器
為什么需要程序計數器?
在多線程情況下,當線程數超過CPU數量或CPU內核數量時,線程之間就要根據時間片輪詢搶奪CPU時間資源。也就是說,在任何一個確定的時刻,一個處理器都只會執行一條線程中的指令。
因此,為了線程切換后能夠恢復到正確的執行位置,每條線程都需要一個獨立的程序計數器去記錄其正在執行的字節碼指令地址
程序計數器是線程私有的一塊較小的內存空間,可以看做是當前線程所執行的字節碼的行號指示器
如果線程正在執行的是一個 Java 方法,計數器記錄的是正在執行的字節碼指令的地址
如果正在執行的是 Native 方法,則計數器的值為空
程序計數器是唯一一個沒有規定任何 OutOfMemoryError 的區域
---------------------------------JVM 內存模型---------------------------------
JAVA中的垃圾回收機制
程序計數器、虛擬機棧、本地方法棧、堆區、方法區。其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生、隨線程而滅,因此這幾個區域的內存分配和回收都具備確定性,就不需要過多考慮回收的問題,因為方法結束或者線程結束時,內存自然就跟隨着回收了。而Java堆區和方法區則與之不同,這部分內存的分配和回收是動態的,正是垃圾收集器所需關注的部分。
垃圾定位分析:
有兩種方式,一種是引用計數(但是無法解決循環引用的問題);另一種就是可達性分析。
判斷對象可以回收的情況:
顯示的把某個引用置位NULL或者指向別的對象
局部引用指向的對象
弱引用關聯的對象
引用計數算法
首先來談談什么是引用:JAVA中當一個對象被創建的時候會給該對象分配一個變量,這個變量便稱為對象的引用。當任何其它變量被賦值為這個對象的引用時,計數加1。但當一個對象實例的某個引用超過了生命周期或者被設置為一個新值時,對象實例的引用計數器減1。任何引用計數器為0的對象實例可以被當作垃圾收集。 此種處理方式是最快速的。但是有bug,相互引用的變量永遠無法為0
public class ReferenceFindTest { public static void main(String[] args) { MyObject object1 = new MyObject(); MyObject object2 = new MyObject(); object1.object = object2; object2.object = object1; object1 = null; object2 = null; } }
這段代碼是用來驗證引用計數算法不能檢測出循環引用。最后面兩句將object1和object2賦值為null,也就是說object1和object2指向的對象已經不可能再被訪問,但是由於它們互相引用對方,導致它們的引用計數器都不為0,那么垃圾收集器就永遠不會回收它們。
可達性分析算法
可達性分析算法是從離散數學中的圖論引入的,程序把所有的引用關系看作一張圖,從一個節點GC ROOT開始,尋找對應的引用節點,找到這個節點以后,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之后,剩余的節點則被認為是沒有被引用到的節點,即無用的節點,無用的節點將會被判定為是可回收的對象。例如如中EFG 對象在圖中不可達 ,但是相互引用,他們便是GC處理的對象。
在Java語言中,可作為GC Roots的對象包括下面幾種:
a) 虛擬機棧中引用的對象(棧幀中的本地變量表);
b) 方法區中類靜態屬性引用的對象;
c) 方法區中常量引用的對象;
d) 本地方法棧中JNI(Native方法)引用的對象。
分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據對象存活的生命周期將內存划分為若干個不同的區域。一般情況下將堆區划分為老年代(Tenured Generation)和新生代(Young Generation),在堆區之外還有一個代就是永久代(Permanet Generation)。老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那么就可以根據不同代的特點采取最適合的收集算法。
年輕代(Young Generation)的回收算法
a) 所有新生成的對象首先都是放在年輕代的。年輕代的目標就是盡可能快速的收集掉那些生命周期短的對象。
b) 新生代內存按照8:1:1的比例分為一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(一般而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象復制到一個survivor0區,然后清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象復制到另一個survivor1區,然后清空eden和這個survivor0區,此時survivor0區是空的,然后將survivor0區和survivor1區交換,即保持survivor1區為空, 如此往復。
c) 當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收。
d) 新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)。
年老代(Old Generation)的回收算法
a) 在年輕代中經歷了N次垃圾回收后仍然存活的對象,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命周期較長的對象。
b) 內存比新生代也大很多(大概比例是1:2),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。
持久代(Permanent Generation)的回收算法
用於存放靜態文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如Hibernate 等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代也稱方法區,具體的回收可參見上文2.5節。
常見的垃圾收集器
下面一張圖是HotSpot虛擬機包含的所有收集器,圖是借用過來滴:
• Serial收集器(復制算法)
新生代單線程收集器,標記和清理都是單線程,優點是簡單高效。是client級別默認的GC方式,可以通過-XX:+UseSerialGC來強制指定。
• Serial Old收集器(標記-整理算法)
老年代單線程收集器,Serial收集器的老年代版本。
• ParNew收集器(停止-復制算法)
新生代收集器,可以認為是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現。
• Parallel Scavenge收集器(停止-復制算法)
並行收集器,追求高吞吐量,高效利用CPU。吞吐量一般為99%, 吞吐量= 用戶線程時間/(用戶線程時間+GC線程時間)。適合后台應用等對交互相應要求不高的場景。是server級別默認采用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=4來指定線程數。
• Parallel Old收集器(停止-復制算法)
Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先。
• CMS(Concurrent Mark Sweep)收集器(標記-清理算法)
高並發、低停頓,追求最短GC回收停頓時間,cpu占用比較高,響應時間快,停頓時間短,多核cpu 追求高響應時間的選擇。
五、GC是什么時候觸發的(面試最常見的問題之一)
由於對象進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有兩種類型:Scavenge GC和Full GC。
5.1 Scavenge GC
一般情況下,當新對象生成,並且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,並且把尚且存活的對象移動到Survivor區。然后整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這里需要使用速度快、效率高的算法,使Eden去能盡快空閑出來。
5.2 Full GC
對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個堆進行回收,所以比Scavenge GC要慢,因此應該盡可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於Full GC的調節。有如下原因可能導致Full GC:
a) 年老代(Tenured)被寫滿;
b) 持久代(Perm)被寫滿;
c) System.gc()被顯示調用;
d) 上一次GC之后Heap的各域分配策略動態變化;