前言
“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調用長度
-
- 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對象,這個叫做 **空間分配擔保**
-
-
動態對象年齡判斷
- 這是什麼意思呢?當對象的年齡還沒到進入老年代的閥值(默認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。
後記
在今後的不斷學習中,我會不斷的更新這篇文章。