文章目錄
- 一、前言
- 1.1 計算機==>作業系統==>JVM
- 1.1.1 虛擬與物體(對上圖的結構層次分析)
- 1.1.2 Java程式執行(對上圖的箭頭流程分析)
- 二、JVM記憶體空間與引數設定
- 2.1 運行時資料區
- 2.2 關于StackOverflowError和OutOfMemoryError
- 2.2.1 StackOverflowError
- 2.2.2 OutOfMemoryError
- 2.3 JVM堆記憶體和非堆記憶體
- 2.3.1 堆記憶體和非堆記憶體
- 2.3.2 JVM堆內部構型(新生代和老年代)
- 2.4 JVM堆引數設定
- 2.4.1 JVM重要引數
- 2.4.2 JVM其他引數
- 2.5 從日志看JVM
- 三、HotSpot VM
- 3.1 HotSpot VM相關知識
- 3.2 HotSpot VM的兩個實作與查看本機HotSpot
- 四、JVM記憶體回收
- 4.1 垃圾收集演算法
- 4.1.1 標記-清除演算法
- 4.1.2 復制演算法
- 4.1.3 標志-整理演算法(復制演算法變更后在老年代的應用)
- 4.1.4 分代收集演算法
- 4.2 垃圾收集器
- 4.2.1 Serial + serial old 新生代和老年代都是單執行緒
- 4.2.2 ParNew+ serial old 新生代多執行緒,老年代單執行緒
- 4.2.3 Parallel scavenge + Parallel old 新生代和老年代都是多執行緒,該組合完成吞吐量優先虛擬機,適用于后臺計算
- 4.2.4 cms收集器 多執行緒,完成回應時間短虛擬機,適用于用戶互動
- 4.2.5 G1收集器 多執行緒,面向服務端的垃圾回收器
- 4.3 垃圾收集器常用引數
- 五、JVM記憶體分配
- 5.1 新物件優先在Eden上分配
- 5.1.1 設定VM Options
- 5.1.2 程式輸出
- 5.2 大物件直接進入老年代(使用-XX:PretenureSizeThreshold引數設定)
- 5.2.1 設定VM Options
- 5.2.2 程式輸出
- 5.3 長期存活的物件應該進入老年代(使用-XX:MaxTenuringThreshold引數設定)
- 5.3.1 設定VM Options
- 5.3.2 程式輸出
- 六、尾聲
一、前言
對于Java虛擬機在記憶體分配與回收的學習,可以類比物理機,很多東西可以觸類旁通,
1.1 計算機==>作業系統==>JVM
JVM全稱為Java Virtual Machine,譯為Java虛擬機,讀者會問,虛擬機虛擬的是誰呢?即虛擬是對什么東西的虛擬,即物體是什么,是如何虛擬的?下面讓我們來看看“虛擬與物體”,
?關于計算機、作業系統、JVM三者關系,如下圖:?

1.1.1 虛擬與物體(對上圖的結構層次分析)
JVM之所以稱為之虛擬機,是因為它是實作了計算機的虛擬化,下表展示JVM位于作業系統堆記憶體中,分別實作的了對作業系統和計算機的虛擬化,
| 物體 | 在JVM上虛擬 | |
|---|---|---|
| 堆疊 | 作業系統堆疊,一般由程式員分配釋放,若程式員不釋放,程式結束時可能由OS回收,分配方式類似于鏈表, | JVM堆疊,用于存放基本資料型別變數,存放物件參考(即參考型別變數的參考),本質上是對作業系統堆疊的虛擬 |
| 堆 | 作業系統堆,由作業系統自動分配釋放,存放函式的引數值,區域變數值等,操作方式與資料結構中的堆疊相類似, | |
| JVM堆,用于存放參考型別變數,本質上是對作業系統堆的虛擬 | ||
| 磁盤 | 計算機磁盤,用于存放計算機上的程式 | JVM方法區,本質上是對計算機磁盤的虛擬 |
| 程式計數器(暫存器方面) | 計算機CPU控制器中的PC暫存器,用于存放當前正在執行指令的地址(注意存放的是指令地址,不是指令本身) | JVM程式計數器,本質上用于對計算機CPU控制器中的PC程式計數器進行虛擬 |
| 記憶體 | 整個計算機(記憶體(作業系統堆疊+作業系統堆)+磁盤+PC計數器) JVM占用的整個記憶體(JVM堆疊+JVM堆+JVM方法區+JVM程式計數器),本質上是對整個計算機的虛擬 |
作業系統堆疊對應JVM堆疊,作業系統堆對應JVM堆,計算機磁盤對應JVM方法區,存放位元組碼物件,計算機PC暫存器對應JVM程式計數器(注意:計算機PC暫存器是下一條指令地址,JVM程式計數器是當前指令的地址),唯一不同的是,整個計算機(記憶體(作業系統堆疊+作業系統堆)+磁盤+PC計數器)對應JVM占用的整個記憶體(JVM堆疊+JVM堆+JVM方法區+JVM程式計數器),
1.1.2 Java程式執行(對上圖的箭頭流程分析)
上圖中不僅是結構圖,展示JVM的虛擬和物體的關系,也是一個流程圖,上圖中的箭頭展示JVM對一個物件的編譯執行,
程式員寫好的類加載到虛擬機執行的程序是:當一個classLoder啟動的時候,classLoader的生存地點在JVM中的堆,首先它會去主機硬碟上將Test.class裝載到JVM的方法區,方法區中的這個位元組檔案會被虛擬機拿來new Test位元組碼(),然后在堆記憶體生成了一個Test位元組碼的物件,最后Test位元組碼這個記憶體檔案有兩個參考一個指向Test的class物件,一個指向加載自己的classLoader,整個程序上圖用箭頭表示,這里做說明,
就像本文開始時說過的,有了計算機組成原理和作業系統兩門課的底子,學起JVM的時候會容易許多,因為JVM本質上就是對計算機和作業系統的虛擬,就是一個虛擬機,
Java正是有了這一套虛擬機的支持,才成就了跨平臺(一次編譯,永久運行)的優勢,
這樣一來,前言部分我們成功引入JVM,接下來,本文要講述的重點是JVM自動記憶體管理,先給出總述:
JVM自動記憶體管理=分配記憶體(指給物件分配記憶體)+回收記憶體(回收分配給物件的記憶體)
上面公式告訴我們,JVM自動記憶體管理分為兩塊,分配記憶體和回收記憶體
二、JVM記憶體空間與引數設定
2.1 運行時資料區
JVM在執行Java程式的程序中會把它所管理的記憶體劃分為若干個不同的運行時資料區域,這些運行時資料區包括方法區、堆、虛擬堆疊、本地方法堆疊、程式計數器,如圖:

讓我們一步步介紹,對于運行時資料區,很多博客都是使用順序介紹的方式,不利于讀者對比比較學習,這里筆者以表格的方式呈現:
| 程式計數器 | Java虛擬機堆疊 | 本地方法堆疊 | Java 堆 | 方法區 | |
|---|---|---|---|---|---|
| 存放內容 | JVM位元組碼指令的地址或Undefined(如果執行緒正在執行一個 Java 方法,這個計數器記錄的是正在執行的虛擬機位元組碼指令的地址;如果正在執行的是 Native 方法,這個計數器的值則為 (Undefined)) | 區域變數表、運算元堆疊、動態鏈接、方法出口 | Native方法(本地方法) | 物件實體、陣列 | 類資訊、常量、靜態變數、即時編譯器編譯后的代碼 |
| 作用 | (1) 位元組碼解釋器通過改變程式計數器來依次讀取指令,從而實作代碼的流程控制,如:順序執行、選擇、回圈、例外處理,(2) 在多執行緒的情況下,程式計數器用于記錄當前執行緒執行的位置,從而當執行緒被切換回來的時候能夠知道該執行緒上次運行到哪兒了, | 每個方法在執行時都會創建一個堆疊幀(Stack Frame)用于存盤區域變數表、運算元堆疊、動態鏈接、方法出口等資訊,每一個方法從呼叫直至執行結束,就對應著一個堆疊幀從虛擬機堆疊中入堆疊到出堆疊的程序, | 每一個本地方法的呼叫執行程序,就對應著一個堆疊幀從本地方法堆疊中入堆疊到出堆疊的程序, | 用于存放物件實體,被物件參考所指向 | 存盤一個型別所使用到的所有型別,域和方法的符號參考,在java程式的動態鏈接中起核心作用 特點:(1)不需要連續的記憶體,可以選擇固定大小或者可擴展,可以選擇不實作垃圾收集; (2)方法區這個概念僅限于HotSpot虛擬機; (3)String作為final修飾的常量:jdk1.6運行時常量池存在于方法區,jdk1.7移到了堆區,而jdk1.8運行時常量池其實是存在于與方法區和堆區相對獨立的元空間. |
| 執行緒共享還是私有 | 執行緒私有 | 執行緒私有 | 執行緒私有 | 執行緒間共享 | 執行緒間共享 |
| StackOverflowError堆疊溢位 | 執行緒請求的堆疊深度大于虛擬機所允許的深度, 報錯資訊:java.lang.StackOverflowError | 無 | |||
| OutOfMemoryError記憶體泄露 | 無 | 如果虛擬機堆疊可以動態擴展,而擴展時無法申請到足夠的記憶體, 報錯資訊:java.lang.OutOfMemoryError:unable to create new native thread | 如果堆中沒有記憶體完成實體分配,并且堆也無法再擴展時,拋出該例外, 報錯資訊:java.lang.OutOfMemoryError: Java heap space | 當方法區無法滿足記憶體分配需求,拋出該例外,報錯資訊:java.lang.OutOfMemoryError: PermGen space | |
| 特點 | 是五個區域中唯一一個沒有OutOfMemoryError | Java虛擬機堆疊和本地方法堆疊都是方法呼叫堆疊,不同之處在于是一個是程式員撰寫的Java方法,一個是自帶Native方法. | 1、可以位于物理上不連續的空間,但是邏輯上要連續, 2、Java堆又稱為CG堆,分為新生區和老年區,新生區又分為Eden區、From Survivor區和To Survivor | 又稱為Non-Heap,非堆,與Java堆區分開來 | |
讓我們對上表繼續深入,講述上表中的StackOverflowError和OutOfMemoryError,
2.2 關于StackOverflowError和OutOfMemoryError
2.2.1 StackOverflowError
運行時資料區中,拋出堆疊溢位的就是虛擬機堆疊和本地方法堆疊,
產生原因:執行緒請求的堆疊深度大于虛擬機所允許的深度,因為JVM堆疊深度是有限的而不是無限的,但是一般的方法呼叫都不會超過JVM的堆疊深度,如果出現堆疊溢位,基本上都是代碼層面的原因,如遞回呼叫沒有設定出口或者無限回圈呼叫,
解決方法:程式員檢查代碼是否有無限回圈即可,
2.2.2 OutOfMemoryError
容易發生OutOfMemoryError記憶體溢位問題的記憶體空間包括:Permanent Generation space和Heap space,
第一種java.lang.OutOfMemoryError: PermGen space(方法區拋出)
產生原因:發生這種問題的原意是程式中使用了大量的jar或class,使java虛擬機裝載類的空間不夠,與Permanent Generation space有關,所以,根本原因在于jar或class太多,方法區堆溢位,則解決方法有兩個種,要么增大方法區,要么減少jar、class檔案,且看解決方法,
解決方法:
(1) 從增大方法區方面入手:增加java虛擬機中的XX:PermSize和XX:MaxPermSize引數的大小,其中XX:PermSize是初始永久保存區域大小,XX:MaxPermSize是最大永久保存區域大小,如web應用中,針對tomcat應用服務器,在catalina.sh 或catalina.bat檔案中一系列環境變數名說明結束處增加一行:
JAVA_OPTS=" -XX:PermSize=64M -XX:MaxPermSize=128m",可有效解決web專案的tomcat服務器經常宕機的問題,
(2) 從減少jar、class檔案入手:清理應用程式中web-inf/lib下的jar,如果tomcat部署了多個應用,很多應用都使用了相同的jar,可以將共同的jar移到tomcat共同的lib下,減少類的重復加載,
第二種OutOfMemoryError: Java heap space(堆拋出)
產生原因:發生這種問題的原因是java虛擬機創建的物件太多,在進行垃圾回收之間,虛擬機分配的到堆記憶體空間已經用滿了,與Heap space有關,所以,根本原因在于物件實體太多,Java堆溢位,則解決方法有兩個種,要么增大堆記憶體,要么減少物件示例,且看解決方法,
解決方法:
(1) 從增大堆記憶體方面入手:增加Java虛擬機中Xms(初始堆大小)和Xmx(最大堆大小)引數的大小,如:set JAVA_OPTS= -Xms256m -Xmx1024m
(2) 從減少物件實體入手:一般來說,正常程式的物件,堆記憶體時絕對夠用的,出現堆記憶體溢位一般是死回圈中創建大量物件,檢查程式,看是否有死回圈或不必要地重復創建大量物件,找到原因后,修改程式和演算法,
第三種OutOfMemoryError:unable to create new native thread(Java虛擬機堆疊、本地方法堆疊拋出)
產生原因:這個例外問題本質原因是我們創建了太多的執行緒,而能創建的執行緒數是有限制的,導致了例外的發生,能創建的執行緒數的具體計算公式如下:
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
注意:MaxProcessMemory 表示一個行程的最大記憶體,JVMMemory 表示JVM記憶體,ReservedOsMemory 表示保留的作業系統記憶體,ThreadStackSize 表示執行緒堆疊的大小,
在java語言里, 當你創建一個執行緒的時候,虛擬機會在JVM記憶體創建一個Thread物件同時創建一個作業系統執行緒,而這個系統執行緒的記憶體用的不是JVMMemory,而是系統中剩下的記憶體(MaxProcessMemory - JVMMemory - ReservedOsMemory),由公式得出結論:你給JVM記憶體越多,那么你能創建的執行緒越少,越容易發生 java.lang.OutOfMemoryError: unable to create new native thread
解決方法:
(1) 如果程式中有bug,導致創建大量不需要的執行緒或者執行緒沒有及時回收,那么必須解決這個bug,修改引數是不能解決問題的,
(2) 如果程式確實需要大量的執行緒,現有的設定不能達到要求,那么可以通過修改MaxProcessMemory,JVMMemory,ThreadStackSize這三個因素,來增加能創建的執行緒數:MaxProcessMemory 表示使用64位作業系統,JVMMemory 表示減少 JVMMemory 的分配,ThreadStackSize 表示減小單個執行緒的堆疊大小,
2.3 JVM堆記憶體和非堆記憶體
2.3.1 堆記憶體和非堆記憶體
JVM記憶體劃分為堆記憶體和非堆記憶體,堆記憶體分為年輕代(Young Generation)、老年代(Old Generation),非堆記憶體就一個永久代(Permanent Generation),
年輕代又分為Eden和Survivor區,Survivor區由FromSpace和ToSpace組成,Eden區占大容量,Survivor兩個區占小容量,默認比例是8:1:1,
官方推薦新生代占堆的的3/8,Survivor占新生代的1/10,
堆記憶體用途:存放的是物件,垃圾收集器就是收集這些物件,然后根據GC演算法回收,
非堆記憶體用途:永久代,也稱為方法區,存盤程式運行時長期存活的物件,比如類的元資料、方法、常量、屬性等,
在JDK1.8版本廢棄了永久代,替代的是元空間(MetaSpace),元空間與永久代上類似,都是方法區的實作,他們最大區別是:永久代使用的是JVM的堆記憶體空間,而元空間使用的是物理記憶體,直接受到本機的物理記憶體限制,在后面的實踐中,因為筆者使用的是JDK8,所以列印出的GC日志里面就有MetaSpace,
2.3.2 JVM堆內部構型(新生代和老年代)
Jdk8中已經去掉永久區,這里為了與時俱進,不再贅余,

上圖演示Java堆記憶體空間,分為新生代和老年代,分別占Java堆1/3和2/3的空間,新生代中又分為Eden區、Survivor0區、Survivor1區,分別占新生代8/10、1/10、1/10空間,
問題1:什么是Java堆?
回答1:JVM規范中說到:”所有的物件實體以及陣列都要在堆上分配”,Java堆是垃圾回收器管理的主要區域,百分之九十九的垃圾回收發生在Java堆,另外百分之一發生在方法區,因此又稱之為”GC堆”,根據JVM規范規定的內容,Java堆可以處于物理上不連續的記憶體空間中,但是邏輯上要求是連續的,
問題2:為什么Java堆要分為新生代和老年代?
回答2:當前JVM對于堆的垃圾回收,采用分代收集的策略,根據堆中物件的存活周期將堆記憶體分為新生代和老年代,在新生代中,每次垃圾回收都有大批物件死去,只有少量存活,而老年代中存放的物件存活率高,這樣劃分的目的是為了使 JVM 能夠更好的管理堆記憶體中的物件,包括記憶體的分配以及回收,
問題3:為什么新生代要分為Eden區、Survivor0區、Survivor1區?
回答3:這是結構與策略相適應的原則,新生代垃圾收集使用的是復制演算法(一種垃圾收集演算法,Serial收集器、ParNew收集器、Parallel scavenge收集器都是用這種演算法),復制演算法可以很好的解決垃圾收集的記憶體碎片問題,但是有一個天然的缺陷,就是要犧牲一半的記憶體(即任意時刻只有一半記憶體用于作業),這對于寶貴的記憶體資源來說是極度奢侈的,新生代在使用復制演算法作為其垃圾收集演算法的時候,對其做了優化,拿出2/10的新生代的記憶體作為交換區,稱為Survivor0區和Survivor1區(注意:有的博客上稱為From Survivor Space和To Survivor Space,這樣闡述也是對的,但是容易對初學者形成誤導,因為在復制演算法中,復制是雙向的,沒有固定的From和To,這一次是由這一邊到另一邊,下次就是從另一邊到這一邊,使用From Survivor Space和To Survivor Space容易讓后來學習者誤以為復制只能從一邊到另一邊,當然有的博客中會附加不管從哪邊到哪邊,起始就是From,終點就是To,即From Survivor Space和To Survivor Space所對應的區回圈對調,但是讀者不一定想的明白,所以筆者這里使用Survivor0、Survivor1,減少誤解)
所以說,新生代在結構上分為Eden區、Survivor0區、Survivor1區,是與其使用的垃圾收集演算法(復制演算法)相適應的結果,
問題4:關于永久區Permanent Space?
回答4:由于Jdk8中取消了永久區Permanent Space,本文為與時俱進,不再講述Permanent Space,
2.4 JVM堆引數設定
這些都是和堆記憶體分配有關的引數,所以我們放在第二部分了,和垃圾收集器有關的引數放在第四部分,
舉例:java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m
2.4.1 JVM重要引數
因為整個堆大小=年輕代大小(新生代大小) + 年老代大小 + 持久代大小,
-Xmn2g:表示年輕代大小為2G,持久代一般固定大小為64m,所以增大年輕代后,將會減小年老代大小,此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8,
-XX:NewRatio=4:設定年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代),這里設定為4,表示年輕代與年老代所占比值為1:4,又因為上面設定年輕代為2G,則老年代大小為8G
-XX:SurvivorRatio=8:設定年輕代中Eden區與Survivor區的大小比值,這里設定為8,則兩個Survivor區與一個Eden區的比值為2:8,一個Survivor區占整個年輕代的1/10,則Survivor0:Survivor1:Eden=1:1:8
-XX:MaxPermSize=16m:設定持久代大小為16m,
所有整個堆大小=年輕代大小 + 年老代大小 + 持久代大小= 2G+ 8G+ 16M=10G+6M=10246MB
2.4.2 JVM其他引數
-Xmx3550m:設定JVM最大可用記憶體為3550M,
-Xms3550m:設定JVM促使記憶體為3550m,此值可以設定與-Xmx相同,
-Xss128k:設定每個執行緒的堆疊大小,JDK5.0以后每個執行緒堆疊大小為1M,以前每個執行緒堆疊大小為256K,更具應用的執行緒所需記憶體大小進行調整,在相同物理記憶體下,減小這個值能生成更多的執行緒,但是作業系統對一個行程內的執行緒數還是有限制的,不能無限生成,經驗值在3000~5000左右,
問題:關于為什么-xmx與-xms的大小設定為一樣的?
回答:首先,在Java堆記憶體分配中,-xmx用于指定JVM最大分配的記憶體,-xms用于指定JVM初始分配的記憶體,所以,-xmx與-xms相等表示JVM初次分配的記憶體的時候就把所有可以分配的最大記憶體分配給它(指JVM),這樣的做的好處是:
(1) 避免JVM在運行程序中、每次垃圾回收完成后向OS申請記憶體:因為所有的可以分配的最大記憶體第一個就給它(JVM)了,
(2) 延后啟動后首次GC的發生時機、減少啟動初期的GC次數:因為第一次給它分配了最大的;
(3) 盡可能避免使用swap space:swap space為交換空間,當web專案部署到linux上時,有一條調優原則就是“盡可能使用記憶體而不是交換空間”
(4) 設定堆記憶體為不可擴展和收縮,避免在每次GC 后調整堆的大小
影響堆記憶體擴展與收縮的兩個引數
MaxHeapFreeRadio 默認值為70 當xmx值比xms值大,堆可以動態收縮與擴展,這個引數控制當堆空間大于指定比例時會自動收縮,默認表示堆空間大于70%會自動收縮
MinHeapFreeRadio 默認值為40 當xmx值比xms值大,堆可以動態收縮與擴展,這個引數控制當堆空間小于指定比例時會自動擴展,默認表示堆空間小于40%會自動擴展
所以,堆記憶體默認是自動擴展和收縮的,但是有一個前提條件,就是到xmx比xms大的時候,當我們將xms設定為和xmx一樣大,堆記憶體就不可擴展和收縮了,即整個堆記憶體被設定為一個固定值,避免在每次GC 后調整堆的大小,
在Java非堆記憶體分配中,一般是用永久區記憶體分配,JVM 使用 -XX:PermSize 設定非堆記憶體初始值,由 -XX:MaxPermSize 設定最大非堆記憶體的大小,
2.5 從日志看JVM
如圖,這里了設定GC日志關聯的類和將GC日志列印


如程式所述,申請了10MB的空間,allocation1 2MB+allocation2 2MB+allocation3 2MB+allocation4 4MB=10MB,接下來我們開始閱讀GC日志,這里筆者以自己電腦上列印的GC日志為例,講述閱讀GC日志的方法:
heap表示堆,即下面的日志是對JVM堆記憶體的列印;
因為使用的是jdk8,所以默認使用ParallelGC收集器,也就是在新生代使用Parallel Scavenge收集器,老年代使用ParallelOld收集器
PSYoungGen 表示使用Parallel scavenge收集器作為年輕代收集器,ParOldGen表示使用Parallel old收集器作為老年代收集器,即筆者電腦上默認是使用Parallel scavenge+Parallel old收集器組合,
其中,PSYoungGen總共38400K(37.5MB),被使用了13568K(13.25MB),PSYoungGen又分為Eden Space 33280K(32.5MB) 被使用了40% 13MB,from space 5120K(5MB)和to space 5120K(5MB),這就是一個eden區和兩個survivor區,
此處注意,因為使用的是jdk8,所以沒有永久區了,只有MetaSpace,見上圖,
三、HotSpot VM
3.1 HotSpot VM相關知識
問題一:什么是HotSpot虛擬機?HotSpot VM的前世今生?
回答一:HotSpot VM是由一家名為“Longview Technologies”的公司設計的一款虛擬機,Sun公司收購Longview Technologies公司后,HotSpot VM成為Sun主要支持的VM產品,Oracle公司收購Sun公司后,即在HotSpot的基礎上,移植JRockit的優秀特性,將HotSpot VM與JRockit VM整合到一起,
問題二:HotSpot VM有何優點?
回答二:HotSpot VM的熱點代碼探測能力可以通過執行計數器找出最具有編譯價值的代碼,然后通知JIT編譯器以方法為單位進行編譯,如果一個方法被頻繁呼叫,或方法中有效回圈次數很多,將會分別觸發標準編譯和OSR(堆疊上替換)編譯動作, 通過編譯器與解釋器恰當地協同作業,可以在最優化的程式回應時間與最佳執行性能中取得平衡,而且無須等待本地代碼輸出才能執行程式,即時編譯的時間壓力也相對減小,這樣有助于引入更多的代碼優化技術,輸出質量更高的本地代碼,
問題三:HotSpot VM與JVM是什么關系?
回答三:JVM是理論標準,有很多種實作方式,HotSpot VM是其中的一種實作方式,
今天的HotSpot VM,是Sun JDK和OpenJDK中所帶的虛擬機,也是目前使用范圍最廣的Java虛擬機,
3.2 HotSpot VM的兩個實作與查看本機HotSpot
HotSpot VM包括兩個實作,不同的實作適合不同的場景:
Java HotSpot Client VM:通過減少應用程式啟動時間和記憶體占用,在客戶端環境中運行應用程式時可以獲得最佳性能,此經過專門調整,可縮短應用程式啟動時間和記憶體占用,使其特別適合客戶端環境,此jvm實作比較適合我們平時用作本地開發,平時的開發不需要很大的記憶體,
Java HotSpot Server VM:旨在最大程度地提高服務器環境中運行的應用程式的執行速度,此jvm實作經過專門調整,可能是特別調整堆大小、垃圾回收器、編譯器那些,用于長時間運行的服務器程式,這些服務器程式需要盡可能快的運行速度,而不是快速啟動時間,
只要電腦上安裝jdk,我們就可以看到hotspot的具體實作:

四、JVM記憶體回收
我們知道,Java中是沒有解構式的,既然沒有解構式,那么如何回收物件呢,答案是自動垃圾回收,Java語言的自動回識訓制可以使程式員不用再操心物件回收問題,一切都交給JVM就好了,那么JVM又是如何做到自動回收垃圾的呢,且看本節,本節分為兩個部分——垃圾收集演算法和垃圾收集器,其中,收集演算法是記憶體回收的理論,而垃圾回收器是記憶體回收的實踐,
4.1 垃圾收集演算法
4.1.1 標記-清除演算法
標記-清除演算法分為兩個階段,“標記”和“清除”,
標記:首先標記出所有需要回收的物件;
清除:在標記完成后統一回收所有被標記的物件,
“標記-清除”演算法的不足:第一,效率問題,標記和清除兩個程序的效率都不會太高;第二,空間問題,標記清除后產生大量不連續的記憶體碎片,這些記憶體空間碎片可能會導致以后程式運行程序中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發一次垃圾收集動作,如果很容易出現這樣的空間碎片多、無法找到大的連續空間的情況,垃圾收集就會較為頻繁,
4.1.2 復制演算法
為了解決“標記-清除演算法”的效率問題,一種復制演算法產生了,它將當前可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中一塊,當一塊的記憶體用完了,就將還活著的物件復制到另一塊上面,然后再把已使用的記憶體空間一次清除掉,這樣使得每次都對整個半區進行記憶體回收,記憶體分配時就不用考慮記憶體碎片等復雜情況,只要移動堆頂指標,按順序分配記憶體即可,這種演算法處理記憶體碎片的核心在于將整個半塊中活的的物件復制到另一整個半塊上面去,所以稱為復制演算法,
復制演算法合理的解決了記憶體碎片問題,但是卻要以犧牲一半的寶貴記憶體為代價,這是非常讓人心疼的,令人愉快地是,現代虛擬機中,早就有了關于復制演算法的改進:
對于Java堆中新生代中的物件來說,99%的物件都是“朝升夕死”的,就是說很多的物件在創建出來后不久就會死掉了,所有我們可以大膽一點,不需要按照1:1的比例來劃分記憶體空間,而是將新生代的記憶體劃分為一塊較大的Eden區(一般占新生代8/10的大小)和兩塊較小的Survivor區(用于復制,一般每塊占新生代1/10的大小,兩塊占新生代2/10的大小),當回收時,將Eden區和Survivor里面當前還活著的物件全部都復制到另一塊Survivor中(關于另一個塊Survivor是否會溢位的問題,答案是不會,這里將新生代90%的容量里的物件復制到10%的容量里面,確實是有風險的,但是JVM有一種記憶體的分配擔保機制,即當目的Survivor空間不夠,會將多出來的物件放到老年代中,因為老年代是足夠大的),最后清理Eden區和源Survivor區的空間,這樣一來,每次新生代可用記憶體空間為整個新生代90%,只有10%的記憶體被浪費掉,
正是因為這一特性,現代虛擬機中采用復制演算法來回收新生代,如Serial收集器、ParNew收集器、Parallel scavenge收集器均是如此,
4.1.3 標志-整理演算法(復制演算法變更后在老年代的應用)
對于新生代來說,由于具有“99%的物件都是朝生夕死的”這一特點,所以我們可以大膽的使用10%的記憶體去存放90%的記憶體中活著的物件,即使是目的Survivor的容量不夠,也可以將多余的存放到老年代中(擔保機制),所有對于新生代,我們使用復制演算法是比較好的(Serial收集器、ParNew收集器、Parallel scavenge收集器),
但是對于老年代,沒有大多數物件朝生夕死這一特點,如果使用復制演算法就要浪費一半的寶貴記憶體,所有我們用另一種辦法來處理它(指老年代)——標志-整理演算法,標記-整理演算法分為兩個階段,“標記”和“整理”,
標記:首先標記出所有需要回收的物件(和標記-清除演算法一樣);
整理:在標記完成后讓所有存活的物件都向一端移動,然后直接清理掉端邊界以外的記憶體(向一端移動類似復制演算法),
4.1.4 分代收集演算法
當前商業虛擬機都是的垃圾收集都使用“分代收集”演算法,這種演算法并沒有什么新的思想,只是根據物件存活周期的不同將記憶體劃分為幾塊,一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采取最適當的收集演算法,在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量物件存活,就是使用復制演算法,這樣只需要付出少量存活物件的復制成本就可以完成收集,而老年代中因為物件的存活率高、沒有額外空間對其分配擔保(新生代復制演算法如果目的Survivor容量不夠會將多余物件放到老年代中,這就是老年代對新生代的分配擔保),必須使用“標記-清除演算法”或“標記-整理演算法”來回收,
四種常用演算法優缺點比較、用途比較
| 優點 | 缺點 | 用途/具體收集器實作 | |
|---|---|---|---|
| 標記-清除演算法 | 實作簡單 | 1、標記和清除效率不高;2、因為清除帶來記憶體碎片問題,導致后面的大記憶體塊越來越少,垃圾收集提前 | cms收集器 |
| 復制演算法 | 解決了記憶體碎片問題,解決了低效率問題 | 每次只能使用一半的記憶體,意味著需要犧牲一半的寶貴記憶體 | Serial收集器、ParNew收集器、Parallel scavenge收集器 |
| 標志-整理演算法 | 充分使用記憶體空間,解決記憶體碎片問題 | 標記和整理效率不高 | serial old收集器、Parallel old收集器 |
| 分代收集演算法 | 結合實際(新生代物件存活時間短,老年代物件存活時間長),使用不同的收集演算法 | 無 | 這是一種綜合收集策略,一般來說,新生代使用復制演算法,老年代是用“標記-清除演算法”或“標志-整理演算法” |
4.2 垃圾收集器
有了上面的垃圾回收演算法,就有了很多的垃圾回收器,對于垃圾回收器,很少有表格對比,筆者以表格對比的方式呈現:
| 單執行緒or多執行緒 | 新生代or老年代 | 基于的收集演算法 | 備注 | |
|---|---|---|---|---|
| Serial收集器 | 單執行緒 | 新生代 | 復制演算法 | 優點:簡單 缺點:Stop the world,垃圾收集時要停掉所有其他執行緒 常用組合:Serial + serial old 新生代和老年代都是單執行緒,簡單 |
| ParNew收集器(是Serial收集器的多執行緒版本) | 多執行緒 | 新生代 | 復制演算法 | 優點:相對于Serial收集器,使用了多執行緒 缺點:除了多執行緒,其他所有和Serial收集器一樣 常用組合:ParNew+ serial old 新生代多執行緒,老年代單執行緒,簡單(新生代ParNew收集器僅僅是Serial收集器的多執行緒版本,所有該組合相對于Serial + serial old 只是新生代是多執行緒而已,其余不變) |
| Parallel scavenge收集器(吞吐量優先收集器) | 多執行緒 | 新生代 | 復制演算法 | 設計目標:盡可能達到一個可控制的吞吐量 吞吐量=運行用戶代碼時間/(運行用戶代碼時間+來及收集時間) 優點:吞吐量高,可以高效率地利用CPU時間,盡快完成程式的計算任務,適合后臺運算 缺點:沒有太大缺陷 常用組合:Parallel scavenge + Parallel old 該組合完成吞吐量優先虛擬機,適用于后臺計算 |
| serial old收集器(是Serial收集器的老年代版本) | 單執行緒 | 老年代 | 標記-整理演算法 | 優點:簡單 缺點:Stop the world,垃圾收集時要停掉所有其他執行緒 常用組合:Serial + serial old 新生代和老年代都是單執行緒,簡單 |
| Parallel old收集器(是Parallel scavenge收集器的老年代版本) | 多執行緒 | 老年代 | 標記-整理演算法 | 優點:吞吐量高,可以高效率地利用CPU時間,盡快完成程式的計算任務,適合后臺運算 缺點:沒有太大缺陷 常用組合:Parallel scavenge + Parallel old 該組合完成吞吐量優先虛擬機,適用于后臺計算 |
| cms收集器(并發低停頓收集器) | 多執行緒 | 老年代 | 標記-清除演算法 | 優點:停頓時間短,適合與用戶互動的程式 四個步驟:初始標記 CMS initial mark、并發標記 CMS concurrent mark、重新標記 CMS remark、并發清除 CMS concurrent sweep 常用組合:cms收集器 完成回應時間短虛擬機,適用于用戶互動 |
| G1收集器 | 多執行緒 | 新生代+老年代 | 標記-整理演算法 | 面向服務端的垃圾回收器, 特點:并行與并發、分代收集、空間整合、可預測的停頓 四個步驟:初始標記 Initial Marking、并發標記 Concurrent Marking、最終篩選 Final Marking、篩選回收 Live Data Counting and Evacuation 常用組合:G1收集器 面向服務端的垃圾回收器注意:G1收集器的收集演算法加粗了,這里做出說明,G1收集器從整體上來看是基于“標記-整理”演算法實作的收集器,從區域(兩個region之間)上看來是基于“復制”演算法實作的, |
注意:G1收集器的收集演算法加粗了,這里做出說明,G1收集器從整體上來看是基于“標記-整理”演算法實作的收集器,從區域(兩個region之間)上看來是基于“復制”演算法實作的,
從上表可以得到的收集常用組合包括:
常用組合1:Serial + serial old 新生代和老年代都是單執行緒,簡單
常用組合2:ParNew+ serial old 新生代多執行緒,老年代單執行緒,簡單
常用組合3:Parallel scavenge + Parallel old 該組合完成吞吐量優先虛擬機,適用于后臺計算
常用組合4:cms收集器 完成回應時間短虛擬機,適用于用戶互動
常用組合5:G1收集器 面向服務端的垃圾回收器
4.2.1 Serial + serial old 新生代和老年代都是單執行緒

附:圖上有一個safepoint,譯為安全點(有的博客上寫成了savepoint,是錯誤的,至少是不準確的),這個safepoint干什么的呢?如何確定這個safepoint的位置?
這個safepoint是干什么的?
safepoint的定義是“A point in program where the state of execution is known by the VM”,譯為程式中一個點就是虛擬機所知道的一個執行狀態,
JVM中safepoint有兩種,分別為GC safepoint、Deoptimization safepoint:
GC safepoint:用在垃圾收集操作中,如果要執行一次GC,那么JVM里所有需要執行GC的Java執行緒都要在到達GC safepoint之后才可以開始GC;
Deoptimization safepoint:如果要執行一次deoptimization,那么JVM里所有需要執行deoptimization的Java執行緒都要在到達deoptimization safepoint之后才可以開始deoptimize
我們上圖中的safepoint自然是GC safepoint,所以上圖中的兩個safepoint都是指執行GC執行緒前的狀態,
對于上圖的理解是(很多博客上都有這種運行示意圖,但是沒有加上解釋,筆者這里加上):
1、多個用戶執行緒(圖中是四個)要開始執行新生代GC操作,所以都要達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
2、四個執行緒都執行新生代的GC操作,因為使用的是Serial收集器,所以是基于復制演算法的單執行緒GC,而且要Stop the world,所以只有GC執行緒在執行,四個用戶執行緒都停止了,
3、新生代GC操作完成,四個執行緒繼續執行,過了一會兒,要開始執行老年代的GC操作了,所以四個執行緒都要再次達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
4、四個執行緒都執行老年代的GC操作,因為使用的是Serial Old收集器,所以是基于標志-整理演算法的單執行緒GC,而且要Stop the world,所以只有GC執行緒在執行,四個用戶執行緒都停止了,
5、老年代GC操作完成,四個執行緒繼續執行,
4.2.2 ParNew+ serial old 新生代多執行緒,老年代單執行緒

該組合中新生代ParNew收集器僅僅是Serial收集器的多執行緒版本,所有該組合相對于Serial + serial old 只是新生代是多執行緒而已,其余不變
對于上圖的理解是(很多博客上都有這種運行示意圖,但是沒有加上解釋,筆者這里加上):
1、多個用戶執行緒(圖中是四個)要開始執行新生代GC操作,所以都要達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
2、四個執行緒都執行新生代的GC操作,因為使用的是Parnew收集器,所以是基于復制演算法的多執行緒GC(注意,這里的多執行緒GC,是指多個GC執行緒并發,用戶執行緒還是要停止的)所以還是要Stop the world,所以只有GC執行緒在執行,四個用戶執行緒都停止了,
3、新生代GC操作完成,四個執行緒繼續執行,過了一會兒,要開始執行老年代的GC操作了,所以四個執行緒都要再次達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
4、四個執行緒都執行老年代的GC操作,因為使用的是Serial Old收集器,所以是基于標志-整理演算法的單執行緒GC,而且要Stop the world,所以只有GC執行緒在執行,四個用戶執行緒都停止了,
5、老年代GC操作完成,四個執行緒繼續執行,
4.2.3 Parallel scavenge + Parallel old 新生代和老年代都是多執行緒,該組合完成吞吐量優先虛擬機,適用于后臺計算

對于上圖的理解是:
1、多個用戶執行緒(圖中是四個)要開始執行新生代GC操作,所以都要達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
2、四個執行緒都執行新生代的GC操作,因為使用的是Parallel scavenge收集器,所以是基于復制演算法的多執行緒GC(注意,這里的多執行緒GC,是指多個GC執行緒并發,用戶執行緒還是要停止的)所以只有GC執行緒在執行,四個用戶執行緒都停止了,
3、新生代GC操作完成,四個執行緒繼續執行,過了一會兒,要開始執行老年代的GC操作了,所以四個執行緒都要再次達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
4、四個執行緒都執行老年代的GC操作,因為使用的是Parallel Old收集器,所以是基于標志-整理演算法的多執行緒GC,(注意,這里的多執行緒GC,是指多個GC執行緒并發,用戶執行緒還是要停止的)所以只有GC執行緒在執行,四個用戶執行緒都停止了,
5、老年代GC操作完成,四個執行緒繼續執行,
4.2.4 cms收集器 多執行緒,完成回應時間短虛擬機,適用于用戶互動

對于上圖的理解是:
CMS收集包括四個步驟:初始標記、并發標記、重新標記、并發清除(CMS作為標記-清除收集器,三個標記一個清除)
| 是否需要stop the world,停止用戶執行緒 | 單個GC執行緒運行or多個GC執行緒運行 | |
|---|---|---|
| 初始標記 | 需要 | 單個GC執行緒運行 |
| 并發標記 | 不需要 | 多個GC執行緒運行 |
| 重新標記 | 需要 | 多個GC執行緒運行 |
| 并發清除 | 不需要 | 多個GC執行緒運行 |
1、多個用戶執行緒(圖中是四個)要開始執行新生代GC操作,所以都要達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
2、四個執行緒都執行GC操作,因為使用的是CMS收集器,第一步驟是初始標記,初始標記僅僅只是標記一下GC Roots能直接關聯到的物件,GC的標記階段需要stop the world,讓所有Java執行緒掛起,這樣JVM才可以安全地來標記物件,所以只有“初始標記”在執行,四個用戶執行緒都停止了,初始標記完成后,達到第二個GC safepoint,圖中達到了;
3、開始執行并發標記,并發標記是GCRoot開始對堆中的物件進行可達性分析,找出存活的物件,并發標記可以與用戶執行緒一起執行,并發標記完成后,所有執行緒達到下一個GC safepoint,圖中達到了;
4、開始執行重新標記,重新標記是為了修正在并發標記期間因用戶程式繼續運作而導致標記產生變動的那部分標記記錄,重新標記完成后,所有執行緒達到下一個GC safepoint,圖中達到了;
5、開始執行并發清理,并發清理可以與用戶執行緒一起執行,并發清理完成后,所有執行緒達到下一個GC safepoint,圖中達到了;
6、開始重置執行緒,就是對剛才并發標記操作的物件,圖中是執行緒3(注意:重置執行緒針對的是并發標記的執行緒,沒有被并發標記的執行緒不需要重置執行緒操作),重置操作執行緒3的時候,與其他三個用戶執行緒無關,它們可以一起執行,
問題:CMS為什么是多執行緒收集器?
回答:因為CMS收集器整個程序中耗時最長的第二并發標記和第四并發清除程序中,GC執行緒都可以與用戶執行緒一起作業,初始標記和重新標記時間忽略不計,所以,從總體上來說,cms收集器的記憶體回收程序與用戶執行緒是并發執行的,所以上表中CMS為多執行緒收集器,
4.2.5 G1收集器 多執行緒,面向服務端的垃圾回收器
1、什么是G1?
G1就是Gabage-First,它將整個Java堆劃分為多個大小相等的獨立區域,即Region,雖然還保留新生代和老年代的概念,但新生代和老年代已不再物理隔離,它們都是一部分Region的集合,
G1收集器的底層原理:G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先串列,每次依據允許的收集時間,優先收集回收價值最大的Region,正是這種使用Region劃分記憶體空間以及有優先級的區域回收方式,保證了G1收集器在有限時間內可以獲取盡可能高的效率,
G1收集器運行示意圖如下:

對于上圖的理解是:
G1收集包括四個步驟:初始標記、并發標記、最終篩選、篩選回收
1、多個用戶執行緒(圖中是四個)要開始執行新生代GC操作,所以都要達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
2、開始執行初始標記,初始標記僅僅只是標記一下GC Roots能直接關聯到的物件,并且修改TAMS(Next Top at Mark Start)的值,讓下一個階段用戶程式并發標記時,能在正確可用的Region上創建新物件,整個標記階段需要stop the world,讓所有Java執行緒掛起,這樣JVM才可以安全地來標記物件,所以只有“初始標記”在執行,四個用戶執行緒都停止了,初始標記完成后,達到第二個GC safepoint,圖中達到了;
3、開始執行并發標記,并發標記是GCRoot開始對堆中的物件進行可達性分析,找出存活的物件,并發標記可以與用戶執行緒一起執行,并發標記完成后,所有執行緒(GC執行緒、用戶執行緒)達到下一個GC safepoint,圖中達到了;
4、開始執行最終標記,最終標記是為了修正在并發標記期間因用戶程式繼續運作而導致標記產生變動的那部分標記記錄,最終標記完成后,所有執行緒達到下一個GC safepoint,圖中達到了;
5、開始執行篩選回收,篩選回歸首先對各個Region的回收價值和成本排序, 根據用戶期待的GC停頓時間來制定回收計劃,篩選回收程序中,因為停頓用戶執行緒將大幅提高收集效率,所以一般篩選回歸是停止用戶執行緒的,篩選回歸完成后,所有執行緒達到下一個GC safepoint,圖中達到了;
6、G1收集器收集結束,繼續并發執行用戶執行緒,
4.3 垃圾收集器常用引數
| 引數 | idea中使用方式 | 描述 |
|---|---|---|
| UseSerialGC | VM Options:-XX:+UseSerialGC | 虛擬機運行在Client模式下的默認值,打開此開關之后,使用Serial+Serial Old的收集器組合進行記憶體回收 |
| UseParNewGC | VM Options: -XX:+UseParNewGC | 打開此開關之后,使用ParNew+ Serial Old的收集器組合進行記憶體回收 |
| UseConcMarkSweepGC | VM Options: -XX:+UseConcMarkSweepGC | 打開此開關之后,使用ParNew + CMS+ Serial Old的收集器組合進行記憶體回收,Serial Old收集器將作為CMS收集器出現Concurrent Mode Failure失敗后的后備收集器使用 |
| UseParallelGC | VM Options: -XX:+UseParallelGC | 虛擬機運行在Server模式下的默認值,打開此開關之后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器組合進行記憶體回收 |
| UseParallelOldGC | VM Options: -XX:UseParallelOldGC | 打開此開關后,使用Parallel Scavenge + Parallel Old 的收集器組合進行記憶體回收 |
| SurvivorRatio | VM Options: -XX:SurvivorRatio=8 | 新生代中Eden區域與Survivor區域的容量比值,默認為8,代表Eden:Survivor=8:1 |
| PretenureSizeThreshold | VM Options: -XX:PretenureSizeThreshold=3145728,表示大于3MB都到老年代中去 | 直接晉升到老年代的物件大小,設定這個引數后,這個引數以位元組B為單位大于這個引數的物件將直接在老年代中分配 |
| MaxTenuringThreshold | VM Options: -XX:MaxTenuringThreshold=2,表示經歷兩次Minor GC,就到老年代中去 | 晉升到老年代的物件年齡,每個物件在堅持過一次Minor GC之后,年齡就增加1,當超過這個引數值就進入到老年代 |
| UseAdaptiveSizePolicy | VM Options: -XX:+UseAdaptiveSizePolicy | 動態調整Java堆中各個區域的大小以及進入老年代的年齡 |
| HandlePromotionFailure | jdk1.8下,HandlePromotionFailure會報錯,Unrecongnized VM option | 是否允許分配擔保失敗,即老年代的剩余空間不足應應對新生代的整個Eden區和Survivor區的所有物件存活的極端情況 |
| ParallelGCThreads | VM Options: -XX:ParallelGCThreads=10 | 設定并行GC時進入記憶體回收執行緒數 |
| GCTimeRadio | VM Options: -XX:GCTimeRadio=99 | GC占總時間的比率,默認值是99,即允許1%的GC時間,僅在使用Parallel Scavenge收集器時生效 |
| MaxGCPauseMillis | VM Options:-XX:MaxGCPauseMillis=100 | 設定GC的最大停頓時間,僅在使用Parallel Scavenge收集器時生效 |
| CMSInitiatingOccupanyFraction | VM Options:-XX:CMSInitiatingOccupanyFraction=68 | 設定CMS收集器在老年代空間被使用多少后觸發垃圾收集,默認值68%,僅在使用CMS收集器時生效 |
| UseCMSCompactAtFullCollection | VM Options: -XX:+UseCMSCompactAtFullCollection | 設定CMS收集器在完成垃圾收集后是否要進行一次記憶體碎片的整理,僅在使用CMS收集器時生效 |
| CMSFullGCsBeforeCompaction | VM Options:-XX:CCMSFullGCsBeforeCompaction=10 | 設定CMS收集在進行若干次垃圾收集后再啟動一次記憶體碎片整理,僅在使用CMS收集器時生效 |
五、JVM記憶體分配
問題:What is minorGC? What is Major GC(Full GC)?
回答:
新生代GC(Minor GC):發生在新生代的垃圾收集動作,因為Java物件大多具有朝生夕滅的特性,所有Minor GC非常頻繁,一般回收速度較快,
老年代GC(Major GC/Full GC):發生在老年代的GC,出現了major GC,經常會伴隨一個MinorGC(但是不絕對),Major GC速度一般比Minor GC慢10倍,
5.1 新物件優先在Eden上分配
5.1.1 設定VM Options
-XX:+PrintGCDetails //列印GC日志
-Xms20M //初始堆大小為20M
-Xmx20M //最大堆大小為20M
-Xmn10M //年輕代大小為10M,則老年代大小=堆大小20M-年輕代大小10M=10M
-XX:SurvivorRatio=8 //年輕代 Eden:Survivor=8 則Eden為8M Survivor0為1M Survivor1為1M
-XX:+UseSerialGC //筆者使用的jdk8默認為Parallel scavenge+Parallel old收集器組合,書上使用Serial+Serial Old的收集器組合,這里設定好
5.1.2 程式輸出
第一步:可以看到,當分配6M記憶體時,全部都在Eden區,沒有任何問題,說明JVM優先在Eden區上分配物件

第二步:因為年輕代只有9M,剩下1M是給To Survivor用的,已經使用了6M,現在申請4M, 就會觸發Minor GC,將6M的存活的物件放到目的survivor中去,但是放不下,因為目的survivor只有1M空間,所以分配擔保到老年代中去,然后將4M物件放到Eden區中,所以,最后的結果是 Eden區域使用了4096KB 4M 老年代中使用了6M 這里form space占用57%可以忽略不計,

5.2 大物件直接進入老年代(使用-XX:PretenureSizeThreshold引數設定)
5.2.1 設定VM Options
-XX:+PrintGCDetails //列印GC日志
-Xms20M //初始堆大小為20M
-Xmx20M //最大堆大小為20M
-Xmn10M //年輕代大小為10M,則老年代大小=堆大小20M-年輕代大小10M=10M
-XX:SurvivorRatio=8 //年輕代 Eden:Survivor=8 則Eden為8M Survivor0為1M Survivor1為1M
-XX:+UseSerialGC //筆者使用的jdk8默認為Parallel scavenge+Parallel old收集器組合,書上使用Serial+Serial Old的收集器組合,這里設定好
-XX:PretenureSizeThreshold=3145728 // 單位是位元組 3145728/1024/1024=3MB 大于3M的物件直接進入老年代
5.2.2 程式輸出

5.3 長期存活的物件應該進入老年代(使用-XX:MaxTenuringThreshold引數設定)
5.3.1 設定VM Options
-XX:+PrintGCDetails //列印GC日志
-Xms20M //初始堆大小為20M
-Xmx20M //最大堆大小為20M
-Xmn10M //年輕代大小為10M,則老年代大小=堆大小20M-年輕代大小10M=10M
-XX:SurvivorRatio=8 //年輕代 Eden:Survivor=8 則Eden為8M Survivor0為1M Survivor1為1M
-XX:+UseSerialGC //筆者使用的jdk8默認為Parallel scavenge+Parallel old收集器組合,書上使用Serial+Serial Old的收集器組合,這里設定好
-XX:MaxTenuringThreshold=1 //表示經歷一次Minor GC,就到老年代中去
5.3.2 程式輸出
第一步驟:只分配allocation1 allocation2,不會產生任何Minor GC,物件都在Eden區中

第二步驟:分配allocation3,產生Minor GC,allocation2移入老年區

第三步驟:allocation3再次分配,allocation1也被送入老年區,老年區里有allocation1 allocation2

六、尾聲
本文講述JVM自動記憶體管理(包括記憶體回收和記憶體),前言部分從作業系統引入JVM,第二部分介紹JVM空間結構(運行時資料區、堆記憶體和非堆記憶體),第三部分介紹HotSpot虛擬機,第四部分和第五部分分別介紹自動記憶體回收和自動記憶體分配的原理實作,
天天打碼,天天進步!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/340611.html
標籤:其他
