jvm
jvm整體
jvm運作過程
不僅跨平台還跨語言
運作時資料區
定義:java虛拟機在執行java程式的過程中會把它所管理的記憶體劃分為若幹個不同的資料區域
類型:程式計數器,虛拟機棧,本地方法區,java堆,方法區(運作時常量池),直接記憶體
線程
線程私有 紅色圈起來
線程共享 紅線
方法區:class 靜态周遊 常量
堆:幾乎所有對象都在堆中進行配置設定
線程私有區域
程式計數器
指向目前線程正在執行的位元組碼指令的位址(時間片輪轉的時候再切回來直接運作上次的末尾) 唯一不會 out of memory的區域
虛拟機棧
存儲目前線程運作方法所需的 資料,指令,傳回位址
棧幀
局部變量表
方法裡面的 局部變量(八大基礎資料類型) + 引用
操作數棧
方法的操作,執行。
動态連接配接
完成出口
大小限制 -xss 1m 不同的虛拟機大小不一樣
局部變量表和操作數棧配合使用
java 解釋執行是棧(操作數棧),c語言是寄存器運算
優點 缺點?
c語言快 基于硬體 移植性差(make install)編譯成機器碼
java 相容性好,效率偏低
再代碼執行的時候,棧幀裡面的動态連接配接決定你應該怎麼工作(符号引用 到 具體引用)
多态
靜态分派
動态分派
傳回位址
方法執行完了就出棧
正常傳回,調用程式計數器中的位址
異常傳回, 異常處理表
-xss
預設值取決于平台
本地方法棧
本地方法棧用來 儲存native方法的資訊
當一個jvm建立的線程調用native方法後,jvm不再為其虛拟機棧中建立棧幀,jvm隻是簡單的動态連接配接并直接調用native方法
虛拟機規範無強制規定,各版本虛拟機自由實作hotspot直接把本地方法棧和虛拟機棧合二為一
線程共享區域
方法區
不同的版本有不同的實作。
<=jdk1.7 永久帶 先劃分堆中 新生帶 和 老年帶
=jdk1.8 元空間 機器記憶體---不受限制 不被清理
好處:友善擴充
壞處: 擠壓堆空間
類資訊
常量
靜态變量
即時編譯期編譯後的代碼
java堆
對象執行個體(幾乎所有)
數組
劃分 連續的記憶體區域
eden 新生待
from
to
terured 老年帶
java堆的大小參數設定
—xmx 堆區記憶體可被配置設定的最大上限
—xms 堆區記憶體初始記憶體配置設定的大小
面試題:為什麼不用一份,2個區(堆,方法區)分?
動靜态分離
堆:對象,數組。頻繁回收。
方法區:回收難度大。
直接記憶體
不是虛拟機運作時資料區的一部分,也不是java虛拟機規範定義的記憶體區域
使用了nio,這塊區域會被頻繁使用,在java堆内可以用directbytebuffer對象直接引用并操作
這塊記憶體不受java堆大小限制,但受本機總記憶體的限制,可以通過maxdirectmemorysize來設定(預設與堆記憶體最大值一樣),是以也會出現oom異常
從底層深入了解運作時資料區
申請記憶體 把棧 方法區 堆 記憶體大小申請出來
類加載器 —class進入方法區
常量和靜态變量放入方法區
虛拟機棧工作 main方法入虛拟機棧 main方法棧幀
棧幀方法執行 堆中new出對象 引用加入局部變量表
垃圾收集器 被調用,有的對象被回收到老年代
深入辨析堆和棧
功能
以棧幀的方式存儲方法調用的過程,并存儲方法調用過程中基本資料類型的變量,以及對象的引用變量,其記憶體配置設定在棧上,變量出了作用域就會自動釋放
而堆記憶體用來存儲java中的對象,無論是成員變量,局部變量,還是類變量,它們指向的對象都存儲在堆記憶體中。
線程獨享和線程共享
棧記憶體歸屬 于單個線程,每個線程都會有一個棧記憶體,其存儲的變量隻能在其所屬線程中可見,即棧記憶體可以了解成線程的私有記憶體。
堆記憶體中的對象對所有線程可見。堆記憶體中的對象可以被所有線程通路
空間大小
棧記憶體遠遠小于堆,棧的深度是有限的,可能發生outofmemory
記憶體溢出
棧溢出 遞歸調用
堆溢出
方法區溢出
本機直接記憶體溢出
虛拟機優化技術
編輯優化技術
方法内聯
棧的優化技術
棧幀之間的資料共享
常量池在方法區還是堆?
jdk1.8 運作時常量池(字元串部分 -- 放入堆)。靜态(class) ---方法區
類成員變量(屬性)放在堆中,屬于執行個體裡面的東西
jvm一開始就加載了異常類
類的解除安裝
類 所有執行個體,都被回收
加載的該類的classload已經被回收
該類java.lone.class對象,沒有任何地方被引用,無法通過反射通路該類的方法
虛拟機中對象建立的過程
對象的配置設定
虛拟機遇到一條new指令時,首先檢查是否被類加載器加載,如果沒有,那必須先執行相應的類加載過程。
類加載就是把class加載到jvm的運作時資料區的過程(類加載後面有專門的專題講)。
1)檢查加載
首先檢查這個指令的參數是否能在常量池中定位到一個類的符号引用(符号引用 :符号引用以一組符号來描述所引用的目标),并且檢查類是否已經被加載、解析和初始化過。
2)配置設定記憶體
接下來虛拟機将為新生對象配置設定記憶體。為對象配置設定空間的任務等同于把一塊确定大小的記憶體從java堆中劃分出來。
指針碰撞
如果java堆中記憶體是絕對規整的,所有用過的記憶體都放在一邊,空閑的記憶體放在另一邊,中間放着一個指針作為分界點的訓示器,那所配置設定記憶體就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種配置設定方式稱為“指針碰撞”。
空閑清單
如果java堆中的記憶體并不是規整的,已使用的記憶體和空閑的記憶體互相交錯,那就沒有辦法簡單地進行指針碰撞了,虛拟機就必須維護一個清單,記錄上哪些記憶體塊是可用的,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的記錄,這種配置設定方式稱為“空閑清單”。
對象占的記憶體時連續的
選擇哪種配置設定方式由java堆是否規整決定,而java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
如果是serial、parnew等帶有壓縮的整理的垃圾回收器的話,系統采用的是指針碰撞,既簡單又高效。
如果是使用cms這種不帶壓縮(整理)的垃圾回收器的話,理論上隻能采用較複雜的空閑清單。
并發安全
除如何劃分可用空間之外,還有另外一個需要考慮的問題是對象建立在虛拟機中是非常頻繁的行為,即使是僅僅修改一個指針所指向的位置,在并發情況下也并不是線程安全的,可能出現正在給對象a配置設定記憶體,指針還沒來得及修改,對象b又同時使用了原來的指針來配置設定記憶體的情況。
cas機制
解決這個問題有兩種方案,一種是對配置設定記憶體空間的動作進行同步處理——實際上虛拟機采用cas配上失敗重試的方式保證更新操作的原子性;
配置設定緩沖
另一種是把記憶體配置設定的動作按照線程劃分在不同的空間之中進行,即每個線程在java堆中預先配置設定一小塊私有記憶體,也就是本地線程配置設定緩沖(thread local allocation buffer,tlab),jvm線上程初始化時,同時也會申請一塊指定大小的記憶體,隻給目前線程使用,這樣每個線程都單獨擁有一個buffer,如果需要配置設定記憶體,就在自己的buffer上配置設定,這樣就不存在競争的情況,可以大大提升配置設定效率,當buffer容量不夠的時候,再重新從eden區域申請一塊繼續使用。
tlab的目的是在為新對象配置設定記憶體空間時,讓每個java應用線程能在使用自己專屬的配置設定指針來配置設定空間,減少同步開銷。
tlab隻是讓每個線程有私有的配置設定指針,但底下存對象的記憶體空間還是給所有線程通路的,隻是其它線程無法在這個區域配置設定而已。當一個tlab用滿(配置設定指針top撞上配置設定極限end了),就新申請一個tlab。
參數:
-xx:+usetlab
允許在年輕代空間中使用線程本地配置設定塊(tlab)。預設情況下啟用此選項。要禁用tlab,請指定-xx:-usetlab。
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
img
3)記憶體空間初始化
(注意不是構造方法)記憶體配置設定完成後,虛拟機需要将配置設定到的記憶體空間都初始化為零值(如int值為0,boolean值為false等等)。這一步操作保證了對象的執行個體字段在java代碼中可以不賦初始值就直接使用,程式能通路到這些字段的資料類型所對應的零值。
4)設定
接下來,虛拟機要對對象進行必要的設定,例如這個對象是哪個類的執行個體、如何才能找到類的中繼資料資訊(java classes在java hotspot vm内部表示為類中繼資料)、對象的哈希碼、對象的gc分代年齡等資訊。這些資訊存放在對象的對象頭之中。
5)對象初始化
在上面工作都完成之後,從虛拟機的視角來看,一個新的對象已經産生了,但從java程式的視角來看,對象建立才剛剛開始,所有的字段都還為零值。是以,一般來說,執行new指令之後會接着把對象按照程式員的意願進行初始化(構造方法),這樣一個真正可用的對象才算完全産生出來。
對象的記憶體布局
在hotspot虛拟機中,對象在記憶體中存儲的布局可以分為3塊區域:
對象頭(header)、
執行個體資料(instance data)
對齊填充(padding)。
對象頭包括兩部分資訊,
第一部分用于存儲對象自身的運作時資料,如哈希碼(hashcode)、gc分代年齡、鎖狀态标志、線程持有的鎖、偏向線程id、偏向時間戳等。
對象頭的另外一部分是類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體。
如果對象是一個java數組,那麼在對象頭中還有一塊用于記錄數組長度的資料。
第三部分對齊填充并不是必然存在的,也沒有特别的含義,它僅僅起着占位符的作用。由于hotspot vm的自動記憶體管理系統要求對對象的大小必須是8位元組的整數倍。當對象其他資料部分沒有對齊時,就需要通過對齊填充來補全。
對象的通路定位
建立對象是為了使用對象,我們的java程式需要通過棧上的reference資料來操作堆上的具體對象。目前主流的通路方式有使用句柄和直接指針兩種。
句柄
如果使用句柄通路的話,那麼java堆中将會劃分出一塊記憶體來作為句柄池,reference中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊。
直接指針
如果使用直接指針通路, reference中存儲的直接就是對象位址。
這兩種對象通路方式各有優勢,使用句柄來通路的最大好處就是reference中存儲的是穩定的句柄位址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時隻會改變句柄中的執行個體資料指針,而reference本身不需要修改。
使用直接指針通路方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象的通路在java中非常頻繁,是以這類開銷積少成多後也是一項非常可觀的執行成本。
對sun hotspot而言,它是使用直接指針通路方式進行對象通路的。
判斷對象的存活
在堆裡面存放着幾乎所有的對象執行個體,垃圾回收器在對對進行回收前,要做的事情就是确定這些對象中哪些還是“存活”着,哪些已經“死去”(死去代表着不可能再被任何途徑使用得對象了)
引用計數法
在對象中添加一個引用計數器,每當有一個地方引用它,計數器就加1,當引用失效時,計數器減1.
python在用,但主流虛拟機沒有使用,因為存在對象互相引用的情況,這個時候需要引入額外的機制來處理,這樣做影響效率,
在代碼中看到,隻保留互相引用的對象還是被回收掉了,說明jvm中采用的不是引用計數法。
可達性分析
(面試時重要的知識點,牢記)
來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為“gc roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊(reference chain),當一個對象到gc roots沒有任何引用鍊相連時,則證明此對象是不可用的。
作為gc roots的對象包括下面幾種:
l 虛拟機棧(棧幀中的本地變量表)中引用的對象。
l 方法區中類靜态屬性引用的對象。
l 方法區中常量引用的對象。
l 本地方法棧中jni(即一般說的native方法)引用的對象。
l jvm的内部引用(class對象、異常對象nullpointexception、outofmemoryerror,系統類加載器)。
l 所有被同步鎖(synchronized關鍵)持有的對象。
l jvm内部的jmxbean、jvmti中注冊的回調、本地代碼緩存等
l jvm實作中的“臨時性”對象,跨代引用的對象(在使用分代模型回收隻回收部分代時)
以上的回收都是對象,類的回收條件:
注意 class要被回收,條件比較苛刻,必須同時滿足以下的條件(僅僅是可以,不代表必然,因為還有一些參數可以進行控制):
1、 該類所有的執行個體都已經被回收,也就是堆中不存在該類的任何執行個體。
2、 加載該類的classloader已經被回收。
3、 該類對應的java.lang.class對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的方法。
4、 參數控制:
還有一個廢棄的常量,這個是對象的回收非常相似,比如:假如有一個字元串“king”進入常量池。
finalize方法
即使通過可達性分析判斷不可達的對象,也不是“非死不可”,它還會處于“緩刑”階段,真正要宣告一個對象死亡,需要經過兩次标記過程,一次是沒有找到與gcroots的引用鍊,它将被第一次标記。随後進行一次篩選(如果對象覆寫了finalize),我們可以在finalize中去拯救。
代碼示範:
運作結果:
可以看到,對象可以被拯救一次 (finalize執行第一次,但是不會執行第二次)
代碼改一下,再來一次。
對象沒有被拯救,這個就是finalize方法執行緩慢,還沒有完成拯救,垃圾回收器就已經回收掉了。
是以建議大家盡量不要使用finalize,因為這個方法太不可靠。在生産中你很難控制方法的執行或者對象的調用順序,建議大家忘了finalize方法!因為在finalize方法能做的工作,java中有更好的,比如try-finally或者其他方式可以做得更好**
各種引用
強引用
一般的object obj = new object() ,就屬于強引用。在任何情況下,隻有有強引用關聯(與根可達)還在,垃圾回收器就永遠不會回收掉被引用的對象。
軟引用 softreference
一些有用但是并非必需,用軟引用關聯的對象,系統将要發生記憶體溢出( ,這些對象就會被回收(如果這次回收後還是沒有足夠的空間,才會抛出記憶體溢出)。參見代碼:
vm參數 -xms10m -xmx10m -xx:+printgc
運作結果
例如,一個程式用來處理使用者提供的圖檔。如果将所有圖檔讀入記憶體,這樣雖然可以很快的打開圖檔,但記憶體空間使用巨大,一些使用較少的圖檔浪費記憶體空間,需要手動從記憶體中移除。如果每次打開圖檔都從磁盤檔案中讀取到記憶體再顯示出來,雖然記憶體占用較少,但一些經常使用的圖檔每次打開都要通路磁盤,代價巨大。這個時候就可以用軟引用建構緩存。
弱引用 weakreference
一些有用(程度比軟引用更低)但是并非必需,用弱引用關聯的對象,隻能生存到下一次垃圾回收之前,gc發生時,不管記憶體夠不夠,都會被回收。
參看代碼:
注意:軟引用 softreference和弱引用 weakreference,可以用在記憶體資源緊張的情況下以及建立不是很重要的資料緩存。當系統記憶體不足的時候,緩存中的内容是可以被釋放的。
實際運用(weakhashmap、threadlocal)
虛引用 phantomreference
幽靈引用,最弱(随時會被回收掉)
垃圾回收的時候收到一個通知,就是為了監控垃圾回收器是否正常工作。
14.學習垃圾回收的意義
java與c++等語言最大的技術差別:自動化的垃圾回收機制(gc)
為什麼要了解gc和記憶體配置設定政策
1、面試需要
2、gc對應用的性能是有影響的;
3、寫代碼有好處
棧:棧中的生命周期是跟随線程,是以一般不需要關注
堆:堆中的對象是垃圾回收的重點
方法區/元空間:這一塊也會發生垃圾回收,不過這塊的效率比較低,一般不是我們關注的重點
對象的配置設定政策
棧上配置設定
跟随線程
沒有逃逸
即方法中的對象沒有發生逃逸。
逃逸分析的原理:分析對象動态作用域,當一個對象在方法中定義後,它可能被外部方法所引用,比如:調用參數傳遞到其他方法中,這種稱之為方法逃逸,甚至還有可能被外部線程通路到,例如:指派給其他線程中通路的變量,這個稱之為線程逃逸。
從不逃逸到方法逃逸到線程逃逸,稱之為對象由低到高的不同逃逸程度。
如果确定一個對象不會逃逸出線程之外,那麼讓對象在棧上配置設定記憶體可以提高jvm的效率。
逃逸分析代碼
public class escapeanalysistest {
public static void main(string[] args) throws exception {
long start = system.currenttimemillis();
for (int i = 0; i < 50000000; i++) {
allocate();
}
system.out.println((system.currenttimemillis() - start) + " ms");
thread.sleep(600000);
static void allocate() {
myobject myobject = new myobject(2020, 2020.6);
static class myobject {
int a;
double b;
myobject(int a, double b) {
this.a = a;
this.b = b;
這段代碼在調用的過程中 myboject這個對象屬于全局逃逸,jvm可以做棧上配置設定
然後通過開啟和關閉doescapeanalysis開關觀察不同。
開啟逃逸分析(jvm預設開啟)
檢視執行速度
關閉逃逸分析
測試結果可見,開啟逃逸分析對代碼的執行性能有很大的影響!那為什麼有這個影響?
逃逸分析
如果是逃逸分析出來的對象可以在棧上配置設定的話,那麼該對象的生命周期就跟随線程了,就不需要垃圾回收,如果是頻繁的調用此方法則可以得到很大的性能提高。
采用了逃逸分析後,滿足逃逸的對象在棧上配置設定
沒有開啟逃逸分析,對象都在堆上配置設定,會頻繁觸發垃圾回收(垃圾回收會影響系統性能),導緻代碼運作慢
代碼驗證
開啟gc列印日志
-xx:+printgc
開啟逃逸分析
可以看到沒有gc日志
可以看到關閉了逃逸分析,jvm在頻繁的進行垃圾回收(gc),正是這一塊的操作導緻性能有較大的差别。
對象優先在eden區配置設定
虛拟機參數:
-xms20m
-xmx20m
-xmn10m
-xx:+printgcdetails
-xx:+printgcdetails 列印垃圾回收日志,程式退出時輸出目前記憶體的配置設定情況
注意:新生代初始時就有大小
大多數情況下,對象在新生代eden區中配置設定。當eden區沒有足夠空間配置設定時,虛拟機将發起一次minor gc。
大對象直接進入老年代
-xx:pretenuresizethreshold=4m
-xx:+useserialgc
pretenuresizethreshold參數隻對serial和parnew兩款收集器有效。
最典型的大對象是那種很長的字元串以及數組。這樣做的目的:
1.避免大量記憶體複制
2.避免提前進行垃圾回收,明明記憶體有空間進行配置設定。
長期存活對象進入老年區
如果對象在eden出生并經過第一次minor gc後仍然存活,并且能被survivor容納的話,将被移動到survivor空間中,并将對象年齡設為1,對象在survivor區中每熬過一次 minor gc,年齡(對象頭 age)就增加1,當它的年齡增加到一定程度( ),cms是6時,就會被晉升到老年代中。
-xx:maxtenuringthreshold調整
對象年齡動态判定
為了能更好地适應不同程式的記憶體狀況,虛拟機并不是永遠地要求對象的年齡必須達到了maxtenuringthreshold才能晉升老年代,如果在survivor空間中相同年齡所有對象大小的總和大于survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到maxtenuringthreshold中要求的年齡
空間配置設定擔保
在發生minor gc之前,虛拟機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那麼minor gc可以確定是安全的。如果不成立,則虛拟機會檢視handlepromotionfailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大于曆次晉升到老年代對象的平均大小,如果大于,将嘗試着進行一次minor gc,盡管這次minor gc是有風險的,如果擔保失敗則會進行一次full gc;如果小于,或者handlepromotionfailure設定不允許冒險,那這時也要改為進行一次full gc。
垃圾回收算法
複制算法(copying)
将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體配置設定時也就不用考慮記憶體碎片等複雜情況,隻要按順序配置設定記憶體即可,實作簡單,運作高效。隻是這種算法的代價是将記憶體縮小為了原來的一半。
注意:記憶體移動是必須實打實的移動(複制),不能使用指針玩。
專門研究表明,新生代中的對象98%是“朝生夕死”的,是以并不需要按照1:1的比例來劃分記憶體空間,而是将記憶體分為一塊較大的eden空間和兩塊較小的survivor空間,每次使用eden和其中一塊survivor[1]。當回收時,将eden和survivor中還存活着的對象一次性地複制到另外一塊survivor空間上,最後清理掉eden和剛才用過的survivor空間。
hotspot虛拟機預設eden和survivor的大小比例是8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的90%(80%+10%),隻有10%的記憶體會被“浪費”。當然,98%的對象可回收隻是一般場景下的資料,我們沒有辦法保證每次回收都隻有不多于10%的對象存活,當survivor空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行配置設定擔保(handle promotion)
标記-清除算法(mark-sweep)
算法分為“标記”和“清除”兩個階段:首先标記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象。
它的主要不足空間問題,标記清除之後會産生大量不連續的記憶體碎片,空間碎片太多可能會導緻以後在程式運作過程中需要配置設定較大對象時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
标記-整理算法(mark-compact)
首先标記出所有需要回收的對象,在标記完成後,後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體。标記整理算法雖然沒有記憶體碎片,但是效率偏低。
jvm中常見的垃圾收集器
分代收集的思想
在新生代中,每次垃圾收集時都發現有大批對象死去,隻有少量存活,那就選用複制算法,隻需要付出少量存活對象的複制成本就可以完成收集。
而老年代中因為對象存活率高、沒有額外空間對它進行配置設定擔保,就必須使用“标記—清理”或者“标記—整理”算法來進行回收。
請記住下圖的垃圾收集器和之間的連線關系。
收集器 收集對象和算法 收集器類型 說明 适用場景
serial 新生代,複制算法 單線程 簡單高效; 适合記憶體不大的情況;
parnew 新生代,複制算法 并行的多線程收集器 parnew垃圾收集器是serial收集器的多線程版本 搭配cms垃圾回收器的首選
parallel scavenge 吞吐量優先收集器 新生代,複制算法 并行的多線程收集器 類似parnew,更加關注吞吐量,達到一個可控制的吞吐量; 本身是server級别多cpu機器上的預設gc方式,主要适合背景運算不需要太多互動的任務;
serial old 老年代,标記整理算法 單線程 client模式下虛拟機使用
parallel old 老年代,标記整理算法 并行的多線程收集器 parallel scavenge收集器的老年代版本,為了配合parallel scavenge的面向吞吐量的特性而開發的對應組合; 在注重吞吐量以及cpu資源敏感的場合采用
cms 老年代,标記清除算法 并行與并發收集器 盡可能的縮短垃圾收集時使用者線程停止時間;缺點在于: 1.記憶體碎片 2.需要更多cpu資源 3.浮動垃圾問題,需要更大的堆空間 重視服務的響應速度、系統停頓時間和使用者體驗的網際網路網站或者b/s系統。網際網路後端目前cms是主流的垃圾回收器;
g1 跨新生代和老年代;标記整理 + 化整為零 并行與并發收集器 jdk1.7才正式引入,采用分區回收的思維,基本不犧牲吞吐量的前提下完成低停頓的記憶體回收;可預測的停頓是其最大的優勢; 面向服務端應用的垃圾回收器,目标為取代cms
并行:垃圾收集的多線程的同時進行。
并發:垃圾收集的多線程和應用的多線程同時進行。
注:吞吐量=運作使用者代碼時間/(運作使用者代碼時間+ 垃圾收集時間)
垃圾收集時間= 垃圾回收頻率 * 單次垃圾回收時間
垃圾回收器工作示意圖
serial/serial old
最古老的,單線程,獨占式,成熟,适合單cpu 伺服器
-xx:+useserialgc 新生代和老年代都用串行收集器
-xx:+useparnewgc 新生代使用parnew,老年代使用serial old
-xx:+useparallelgc 新生代使用parallergc,老年代使用serial old
parnew
和serial基本沒差別,唯一的差別:多線程,多cpu的,停頓時間比serial少
parallel scavenge(parallergc)/parallel old
關注吞吐量的垃圾收集器,高吞吐量則可以高效率地利用cpu時間,盡快完成程式的運算任務,主要适合在背景運算而不需要太多互動的任務。
所謂吞吐量就是cpu用于運作使用者代碼的時間與cpu總消耗時間的比值,即吞吐量=運作使用者代碼時間/(運作使用者代碼時間+垃圾收集時間),虛拟機總共運作了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
concurrent mark sweep (cms)
收集器是一種以擷取最短回收停頓時間為目标的收集器。目前很大一部分的java應用集中在網際網路站或者b/s系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。cms收集器就非常符合這類應用的需求。
從名字(包含“mark sweep”)上就可以看出,cms收集器是基于“标記—清除”算法實作的,它的運作過程相對于前面幾種收集器來說更複雜一些,整個過程分為4個步驟,包括:
l 初始标記-短暫,僅僅隻是标記一下gc roots能直接關聯到的對象,速度很快。
l 并發标記-和使用者的應用程式同時進行,進行gc roots追蹤的過程
l 重新标記-短暫,為了修正并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,這個階段的停頓時間一般會比初始标記階段稍長一些,但遠比并發标記的時間短。
l 并發清除
由于整個過程中耗時最長的并發标記和并發清除過程收集器線程都可以與使用者線程一起工作,是以,從總體上來說,cms收集器的記憶體回收過程是與使用者線程一起并發執行的。
-xx:+useconcmarksweepgc ,表示新生代使用parnew,老年代的用cms
浮動垃圾:由于cms并發清理階段使用者線程還在運作着,伴随程式運作自然就還會有新的垃圾不斷産生,這一部分垃圾出現在标記過程之後,cms無法在當次收集中處理掉它們,隻好留待下一次gc時再清理掉。這一部分垃圾就稱為“浮動垃圾”。
g1
-xx:+useg1gc
并行與并發:g1能充分利用多cpu、多核環境下的硬體優勢,使用多個cpu(cpu或者cpu核心)來縮短stop-the-world停頓的時間,部分其他收集器原本需要停頓java線程執行的gc動作,g1收集器仍然可以通過并發的方式讓java程式繼續執行。
分代收集:與其他收集器一樣,分代概念在g1中依然得以保留。雖然g1可以不需要其他收集器配合就能獨立管理整個gc堆,但它能夠采用不同的方式去處理新建立的對象和已經存活了一段時間、熬過多次gc的舊對象以擷取更好的收集效果。
空間整合:與cms的“标記—清理”算法不同,g1從整體來看是基于“标記—整理”算法實作的收集器,從局部(兩個region之間)上來看是基于“複制”算法實作的,但無論如何,這兩種算法都意味着g1運作期間不會産生記憶體空間碎片,收集後能提供規整的可用記憶體。這種特性有利于程式長時間運作,配置設定大對象時不會因為無法找到連續記憶體空間而提前觸發下一次gc。
記憶體布局:在g1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而g1不再是這樣。使用g1收集器時,java堆的記憶體布局就與其他收集器有很大差别,它将整個java堆劃分為多個大小相等的獨立區域(region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是實體隔離的了,它們都是一部分region(不需要連續)的集合。
n 新生代gc
回收eden區和survivor區,回收後,所有eden區被清空,存在一個survivor區儲存了部分資料。老年代區域會增多,因為部分新生代的對象會晉升到老年代。
n 并發标記周期
初始标記:短暫,僅僅隻是标記一下gc roots能直接關聯到的對象,速度很快,産生一個全局停頓,都伴随有一次新生代的gc。
根區域掃描:掃描survivor區可以直接到達的老年代區域。
并發标記階段:掃描和查找整個堆的存活對象,并标記。
重新标記:會産生全局停頓,對并發标記階段的結果進行修正。
獨占清理:會産生全局停頓,對gc回收比例進行排序,供混合收集階段使用
并發清理:識别并清理完全空閑的區域,并發進行
n 混合收集
對含有垃圾比例較高的region進行回收。
g1當出現記憶體不足的的情況,也可能進行的fullgc回收。
g1中重要的參數:
-xx:maxgcpausemillis 指定目标的最大停頓時間,g1嘗試調整新生代和老年代的比例,堆大小,晉升年齡來達到這個目标時間。
-xx:parallergcthreads:設定gc的工作線程數量
stop the world現象
gc收集器和我們gc調優的目标就是盡可能的減少stw的時間和次數。
記憶體洩漏和記憶體溢出辨析
記憶體溢出:實實在在的記憶體空間不足導緻;
記憶體洩漏:該釋放的對象沒有釋放,多見于自己使用容器儲存元素的情況下。
未來的垃圾回收
zgc通過技術手段把stw的情況控制在僅有一次,就是第一次的初始标記才會發生,這樣也就不難了解為什麼gc停頓時間不随着堆增大而上升了,再大我也是通過并發的時間去回收了
關鍵技術
有色指針(colored pointers)
加載屏障(load barrier)
常量池與string
常量池有很多概念,包括運作時常量池、class常量池、字元串常量池。
虛拟機規範隻規定以上區域屬于方法區,并沒有規定虛拟機廠商的實作。
嚴格來說是靜态常量池和運作時常量池,靜态常量池是存放字元串字面量、符号引用以及類和方法的資訊,而運作時常量池存放的是運作時一些直接引用。
運作時常量池是在類加載完成之後,将靜态常量池中的符号引用值轉存到運作時常量池中,類在解析之後,将符号引用替換成直接引用。
這兩個常量池在jdk1.7版本之後,就移到堆記憶體中了,這裡指的是實體空間,而邏輯上還是屬于方法區(方法區是邏輯分區)。
字面量:
給基本類型變量指派的方式就叫做字面量或者字面值
比如:int i=120; long j=10l;
符号引用:包括類和方法的全限定名(例如 string 這個類,它的全限定名就是 java/lang/string)、字段的名稱和描述符以及方法的名稱和描述符。
直接引用:具體對象的索引值。
重要其實還是字元串。
string 對象是如何實作的?