前言
“Write Once Run anywhere” 是得益于JVM,工作了将近一年的時間也明白了,最重要的還是思想結構和底層的實作,因為就算新技術層出不窮,它們也隻不過是在錦上添花而已。
本文是我是從《深入了解Java虛拟機》總結而來,如果有什麼說的不對的地方,還請各位看官指出,我還進行改正
正文
JDK,JRE,JVM三者之間的關系
JDK包含JRE,JRE包含JVM
記憶體溢出診斷
通過一個 VM argument進行設定 -xx: +HeapDumpOutOfMemoryError
這個指令會導出一個分析檔案,需要下載下傳一些工具對這個檔案加以分析。
還可以通過JDK自帶的可視化工具 console.exe 進行監控。
JVM分類
-
Sun Classical VM(已淘汰,第一台商用的java虛拟機)
解釋器和編譯器不能一同執行。 隻能使用純解釋器的方式來執行java代碼
-
Exact VM
編譯器和解釋器混合工作即兩級及時編譯器
-
Hot Spot
就是我們現在最普遍使用的虛拟機。
JAVA虛拟機記憶體管理
java虛拟機在執行程式的時候會把它所管理的區域劃分成不同的資料區。
記憶體區域可以分為兩個部分:
1.線程共享區
- 方法區
- Java堆
- 新生代
- Eden(伊甸園)
- Survivor(存活區)
- 老年代
- Tenured Gen
- 新生代
2.線程獨占區
- 虛拟機棧
- 本地方法棧
- 程式計數器
記憶體區域之程式計數器
是一塊較小的記憶體空間,是一個目前線程所執行的位元組碼的行号訓示器
位元組碼解釋器就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令
如果執行的是線程的java方法,計時器記錄的是虛拟機位元組碼的指令位址,
如果執行的是native方法,那麼這個計數器的值是空的(Undefined)
那麼另一個問題:為什麼需要用程式計數器儲存執行的行号呢,是為了線程上下文切換,保證了線程不會錯亂,線程隻是執行操作,而并不會儲存資料。
記憶體區域之虛拟機棧
描述的Java方法運作的記憶體模型
每一個方法的調用到完成都對應着棧幀在虛拟機棧中入棧和出棧的過程。
- 棧幀:每個方法執行,都會建立一個棧幀,伴随着這個方法的産生與完成,用于存儲局部變量表(定長為32),操作數棧,動态連結(面向對象的多态性),方法出口(兩種出棧:1)return 2)exception)等。
- 局部變量表:存放的是編譯器,資料類型,引用類型,returnAddress類型
(Tips:這個局部變量表,指的就是我們平常說的棧)
棧的區域是固定的,當我們不斷調用方法,就會不斷産生棧幀進入棧中,如果超出了棧的大小,就會出現stackOverflow的異常,想象平常最容易出現的場景就是遞歸,如果沒寫好的話,無止境的遞歸。
記憶體區域之本地方法棧
本地方法棧為虛拟機執行native方法服務
而虛拟機棧為虛拟機執行java方法服務
這就是二者唯一差別的差別,在*hot spot VM中這兩個區域并沒有明顯的區分。
之是以開辟這個區域,是為了友善和系統互動,使用java和作業系統互動,有不便之處,是以最頂層的ClassLoader采用的C++編寫。
記憶體區域之堆
記憶體中最大的一塊。
存放對象的執行個體。
垃圾收集器管理的主要區域,是以很多人稱之為GC堆
如果堆記憶體溢出,會産生OutOfMemeory的Error
-Xmx -Xms, 這兩個VM參數,可以修改堆大小
記憶體區域之方法區
很多人稱之為永生代。
垃圾收集在這個區域比較少見。
存儲虛拟機加載的類資訊(類的版本,字段,方法,接口),常量,靜态變量,即時編譯器(JIT)編譯後的代碼等資料。
可能出現OutOfMemory
運作時常量池
屬于方法區的一部分
存放編譯時生成的字面量,以及符号引用
小例子:
String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
在這裡 a與b的位址是相同的,這收益于常量池,而c與a,b是不相同的因為new是直接在堆中開辟了一條記憶體空間,不受常量池影響的
直接記憶體大多時候也被稱為堆外記憶體,自從 JDK 引入 NIO 後,直接記憶體的使用也越來越普遍。通過 native 方法可以配置設定堆外記憶體,通過 DirectByteBuffer 對象來操作。
對象建立
給對象配置設定記憶體的方式:
- 指針碰撞
- 空閑清單
具體使用哪一種方式,是由堆記憶體是否規整決定的,是否規整是由垃圾回收機制決定的,如果垃圾回收會把區域變得相對完整則使用指針碰撞,如果是零散的,則使用空閑清單。
對象的結構
對象的大小必須是八的整數倍。
對象結構:
- Header (自身運作時資料,類型指針)
- markword(自身運作時資料)
第一部分markword,用于存儲對象自身的運作時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分資料的長度在32位和64位的虛拟機(未開啟壓縮指針)中分别為32bit和64bit,官方稱它為“MarkWord”。
- klass(類型指針)
這個區域存放的是klass類型的指針,這個指針說明了目前這個對象是哪個類的執行個體。
- 數組長度(隻有數組對象有)
記錄了數組的長度,是以我們才可以通過length調用長度
- markword(自身運作時資料)
-
instanceData
這個區域是真實存儲資訊的地方,不管是從父類繼承來的還是自己所有的,都需要進行記錄。
- padding 填充記憶體的作用,因為對象配置設定的記憶體必須是八的整數倍,是以如果在instanceData并沒有對齊的情況下,便會填充
對象的通路定位
- 使用句柄:有點像是一種間接尋址的感覺在堆記憶體中,存在一個句柄池,引用指向了句柄池中的一塊位址,然後句柄池再指向堆記憶體,使用這種方法最大的好處就是存儲的是穩定的句柄位址。
- 使用指針:引用直接指向堆記憶體。最大的好處就是速度快,節省了一次指針定位的開銷
虛拟機參數
1.-Xms 設定堆的最小值
2.-Xmx 設定堆的最大值
3.-Xss 設定棧容量
垃圾回收
列印GC的詳細資訊 需要在VM 參數中加入下面兩個參數:-verbose:gc -xx:PrintGCDetail
如何判定對象為垃圾對象?
1.引用計數法
在對象中添加一個引用計數器,當有地方引用時+1,當引用失效-1。
但是一般并不使用,因為如果存在引用的互相依賴那麼引用計數法将會失效。
2.可達性分析
從GC root 節點,向下搜尋,如果一個位址對于GC root對象(包括 虛拟機棧中的局部常量表中的引用的對象,方法區中類靜态屬性引用的對象,方法區中常量所引用的的對象,本地方法棧中引用的對象),再也沒有任何可走的路徑,那麼将會把在堆中的整個記憶體都給回首掉。
這裡說的最多的字眼就是引用,Java 中一共包括四種引用方法:
-
強引用
垃圾回收器永遠不會回收掉強引用的對象
-
軟引用
有用但是非必須,在記憶體即将溢出之前如果有軟引用會調用第二次GC,如果還是溢出,才會曝出異常。使用SoftReference來使用軟引用
-
弱引用
非必須的引用,隻能存活到下一次GC之前,無論記憶體是否足夠,使用WeakReference 來使用弱引用。
-
虛引用
這種引用存在的目的是,當這個引用的對象被回收的時候,我們會得到一個通知,提供PhantomReference來實作虛引用。
大多數情況下,回收的都是堆區,很少收集方法區,那是因為這樣做成本效益很低。
如果回收方法區的話,主要回收兩種:廢棄常量,無用的類。
回收政策:
- 标記-清除算法
通過可達性分析算法,首先标記有哪些是需要清理的,然後再将它們進行清除。這個算法簡單,但是存在的問題就是效率問題和空間問題。被标記可能十分分散,清理後,在記憶體裡就會出現特别零散的空間,不利于日後開辟空間使用。
- 複制算法
複制算法,将堆記憶體劃分成兩份,然後操作其中的一份,當需要進行垃圾回收的時候,複制算法會講沒有被回收的執行個體複制到另一份中去然後,将原來的所有的(不管有沒有被回收的都删除掉)删除掉,然後在另一份中進行繼續操作,下次在GC的時候,就和剛剛的操作一樣,簡單的說就是兩個區域交替的工作。這樣有效的解決了标記-清除算法的效率問題。但是這個問題,造成了堆記憶體中有空間浪費的情況
現在大多數虛拟機新生代都是采用了這種回收政策。
多說一點,在hotspot中,新生代會有一個eden區,兩個survior區,比例為8:1,每次一個eden區和一個survior區被占用,也就是說隻會有一個survior區被浪費掉。
- 标記-整理算法
一般應用與老年代,因為複制算法消耗空間,可能需要記憶體擔保。
這個算法是将不需要GC的對象移向記憶體的一段,然後将除了一端區域界線外的對象全部清除掉
- 分代收集算法
根據不同的記憶體區域(新生代,或者老年代),選擇不同的GC算法。
新生代使用複制算法,而老年代時候标記-清理,或标記-整理,可以做到每一塊都因地制宜。
垃圾回收器:
不同的垃圾回收器對應不同的使用場景。
垃圾收集器的不斷推塵出新,其實就是一個不斷縮短垃圾回收時間的過程。
- Serial
- 最基本
- 單線程
這就導緻了一個問題,多線程并發運作,但是需要垃圾回收了,那麼所有線程都被阻塞,隻有垃圾回收線程在運作,直到回收完畢,其他線程才繼續運作。
這個回收器對于運作在client端是一個好的選擇。
- Parnew
- 多線程
- 複制算法(新生代收集器),可以與Cms(老年代收集器)共同使用
- Parallel scavenge
- 複制算法(新生代收集器),不可以與Cms共同使用
- 多線程收集器
- 達到可控制的吞吐量(吞吐量:cpu用于運作使用者代碼的時間與cpu消耗的總時間的比值)
- Cms(Concurrent Mark sweep)
可以邊扔垃圾邊打掃。
- 使用标記-清理算法
- 工作過程
- 初始标記
- 并發标記
- 重新标記
- 并發清理
- 優點
- 并發收集
- 低停頓
- 缺點
- 占用大量CPU
- 無法清理浮動垃圾
- G1
- 使用标記-整理算法
-
- 并行和并發
- 分代收集
- 空間整合
-
- 最終标記
- 篩選回收
- 工作原理
- G1與其他的收集器在記憶體布局上有很大的差别,它是将記憶體劃分成了一塊一塊可以不連續的region,雖然保留新生代,老年代,但是已經不在實體隔離。在背景會維護一個優先清單,每次根據允許的收集時間,回收掉價值最大的region區,是以這個收集器叫 Garbage first。
上面有一個概念,容易讓人混淆,那就是并發和并行,舉個例子,并行就是你去看病,醫院有多個看病的醫生,而并發就是有多個病人找了同一個醫生。
記憶體配置設定
記憶體配置設定原則:
- 優先配置設定到Eden區,當Eden區記憶體不足的時候,會發生一次 Minor GC。
- 大對象直接配置設定到老年代,因為Eden區經常出現 Minor GC,而且采用的是複制算法,如果把大對象放在其中不友善移動,是以放在了GC不經常發生的老年代。發生在老年代的GC,我們稱之為 Full GC/Major GC,而且Full GC的速度要比 Minor GC 慢上10倍以上。
那什麼是大對象呢?大對象指的是在記憶體中需要大量的連續記憶體,例如說 長的字元串或者大的數組。
- 長期存活的對象配置設定到老年代
- 每個對象都會有一個年齡對象計數器,這個計數器會因為每一次逃過了GC就會增加1,等到長到15(預設值)的時候,便會晉升到老年代。
- 空間配置設定擔保
- 例子:假如現在分别有2M,2M,2M,4M的對象,然後我們的Eden區設定為8M。
那麼前三個會首先進入到Eden區域中,但是卻發現4M對象放不進去,那麼會将之前的6M移到别的空閑區域中,然後在eden中放入4M對象,這個叫做 **空間配置設定擔保**
- 例子:假如現在分别有2M,2M,2M,4M的對象,然後我們的Eden區設定為8M。
- 動态對象年齡判斷
- 這是什麼意思呢?當對象的年齡還沒到進入老年代的閥值(預設15)的情況下,也是有可能進入老年代的。那就是當survivor區中同一年齡的所有對象的大小大于survivor記憶體大小的一半的時候,大于或等于這個年齡的對象将至今進入老年組。
- 逃逸分析與棧上配置設定
- 對象的作用域僅在方法中有效,沒有發生逃逸,則把對象放到棧記憶體中
換句話說 **能使用局部變量,盡量使用局部變量**
- 對象的作用域僅在方法中有效,沒有發生逃逸,則把對象放到棧記憶體中
第四十九節:虛拟機工具
虛拟機工具:
- JPS:java process status
- -m 運作時傳入的參數
- -v 虛拟機傳入的參數
- -l 詳細的類資訊,或者jar包資訊
- Jstate:監控虛拟機的各種運作狀态的,例如類裝載,記憶體,垃圾回收,JIT編譯等資料的
- Jinfo:實時檢視和調整虛拟機各項參數
- -v可以檢視在啟動的時候,指定的參數清單
- Jmap:用于生成堆轉儲快照,一般稱為heapdump或dump
- Jhat:結合jmap生成的檔案進行分析,形成可視化潔面
- Jstack:生成目前時刻線程快照
- HSDIS:生成JIT的反編譯代碼
- Jconsole:代替了JPS,并且可以檢視遠端程序的狀态。
- VisualVM:多合一故障處理工具
性能調優例子
問題為将使用者績效考核資訊處理為一個Excel,但是時不時的不定時間會出現卡頓。
解決思路:
- 優化sql
- 監控CPU
- 監控記憶體
- 經常Full GC
根本原因為把一台tomcat的堆設定的太大,而且使用者生成excel的時間比較集中,導緻大對象不斷的生成,導緻老年代告急,是以經常Full GC,産生full gc後所有其他的工作線程被阻塞,是以導緻會有時間空檔期。
解決方案:在一台伺服器上部署多個伺服器構成叢集,每個叢集的堆配置設定4G。
後記
在今後的不斷學習中,我會不斷的更新這篇文章。