JVM深度解析

JVM內存管理深度剖析

JVM-java虛擬機

JVM --- Java Virtual Machine --- java虛擬機
所謂的java虛擬機,其實就是翻譯軟件,將.class.jar等文件,翻譯成各個操做系統所能識別的機器語言,JVM是跨平臺的,甚至是跨語言的,因此這就是Java的魅力。java

image.png

JVM、JRE、JDK

JDK:Java工具包
JRE:(Java Runtime Environment)Java 的運行時環境,也就是JVM+一堆基礎類庫,JRE提供的不少類庫
JVM:翻譯,把Class翻譯成機器語言
因此 JDK>JRE>JVM面試

JVM 的結構

JVM運行過程
image.png算法

運行時數據區是重點!!!
運行時數據區又分紅兩部分:線程之間共享的(方法區、堆),線程私有的(虛擬機棧、程序計數器、本地方法棧)
image.png數組

程序計數器

用來記錄當前線程的每一行代碼所執行的字節碼地址、操做等。是字節碼的行號指示器。(本行對應的操做,內存物理地址,線程恢復等)
緣由:cpu的時間片輪轉,由於時間片輪轉,執行到一半停了,切其餘線程了,因此得有一個記錄當前執行到哪行,啥操做的記錄器。
程序計數器很小,因此不會OOM緩存

虛擬機棧

常說的堆棧,其中的棧指的就是JVM的虛擬機棧。
棧是先進後出的結構(LIFO),類比於彈夾。安全

一個棧(虛擬機棧)相似於彈夾,都是先進後出的,每一個棧幀就是子彈(1個方法=1個棧幀),棧幀是一個一個壓入棧的(子彈壓入彈夾),而後一個一個射出。
image.pngmarkdown

棧幀的結構
image.png多線程

大小限制 -Xss
棧的大小缺省爲1M,可用參數 –Xss調整大小,例如-Xss256k,若是超過限制就會OOM。棧放的棧幀是有大小的(彈夾有大小,放的子彈有限制)併發

局部變量表
存儲局部變量,存儲八大基本類型和對象的引用,Object對象會存放堆裏,引用會存這。一個32位,64位的類型會一個佔兩個位置。工具

操做數棧
存放咱們方法執行的操做數的。剛開始棧是空的,有操做的時候會頻繁入棧出棧進行計算。

動態鏈接
用來記錄多態具體執行哪一個方法的,如 People wang = new Man(); wang.wc(); 在運行時動態鏈接到 wang.男廁所

返回地址
返回返回值的地址(程序計數器記錄的物理地址),異常的時候不走這個,走異常處理器表

執行下面操做的流程:
image.png

  1. 局部變量表(第一個通常都是this,除了靜態這樣的)會聲明兩變量,x和y用1,2存儲
  2. x=1壓入操做數棧
  3. y=2壓入操做數棧(由於是棧 先進後出,因此x=1原本在棧頂1號位,會被壓到下面位置(相似子彈))
  4. 將xy都移出棧,去cpu執行 1+2操做,返回3放入操做數棧
  5. 將操做數棧裏的3返回值取出來,賦值給局部變量表中的y=2

本地方法棧

功能相似於虛擬機棧,是native 方法,交給C來執行。

方法區

(永久代,在HotSpot 虛擬機中使用永久代來實現方法區,可是其餘虛擬機並非。因此方法區≠永久代)

方法區主要是用來存放已被虛擬機加載的類相關信息,包括類信息、靜態變量、常量、運行時常量池、字符串常量池 (String會存到堆裏面,引用會存在常量池裏)

JVM 在加載類時,會先加載.class文件。
.class文件包含類的版本、字段、方法和接口等描述+常量池
常量池用於存放編譯期間生成的各類字面量和符號引用
字面量=String+final常量。符號引用=類、方法、字段的全名和描述符。

而當類加載到內存中後,JVM 就會將 class 文件常量池中的內容存放到運行時的常量池中;在解析階段,JVM 會把符號引用替換爲直接引用(對象的索引值)。 例如,類中的一個字符串常量在 class 文件中時,存放在 class 文件常量池中的;在 JVM 加載完類以後,JVM 會將這個字符串常量放到運行時常量池中,並在解析階段,指定該字符串對象的索引值。運行時常量池是全局共享的,多個類共用一個運行時常量池,class 文件中常量池多個相同的字符串在運行時常量池只會存在一份。

方法區與堆空間相似,也是一個共享內存區,因此方法區是線程共享的。 假如兩個線程都試圖訪問方法區中的同一個類信息,而這個類尚未裝入 JVM,那麼此時就只容許一個線程去加載它,另外一個線程必須等待。在 HotSpot 虛擬機、Java7 版本中已經將永久代的靜態變量和運行時常量池轉移到了堆中,其他部分則存儲在 JVM 的非堆內存中,而 Java8 版本已經將方法區中實現的永久代去掉了,並用元空間(class metadata)代替了以前的永久代,而且元空間的存儲位置是本地

配置元空間大小參數:
dk1.7及之前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
jdk1.8之後(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize
jdk1.8之後大小就只受本機總內存的限制(若是不設置參數的話)

Java8 爲何使用元空間替代永久代,這樣作有什麼好處呢?
1.兩公司合併,爲了融合
2.永久代內存常常不夠用或發生內存溢出

堆是 JVM 上最大的內存區域,這裏存儲了 幾乎全部對象+數組。垃圾回收,操做的對象就是堆。

程序啓動就會申請堆空間,隨着堆增大,會進行GC(垃圾回收)。
對象new出來就是在堆上建立出來的。基本數據類型:方法體內聲明的(局部變量)存在棧裏面,否則就是存在堆上。

配置堆的大小:
-Xmx:堆的最大值;
-Xms:堆的最小值(初始值);

堆分爲新生代(容易被GC回收)和老年代(不容易被GC回收),物理地址是連續的。一個對象建立出來的時候在新生代,若是通過好幾回GC都還活着,那就會移到老年代。 image.png

直接內存

不屬於JVM運行時數據區的。可是使用NIO就能夠在內存裏申請一塊區域供JVM使用。也會發生OOM
配置的大小:
-XX:MaxDirectMemorySize

HSDB工具

可使用HSDB工具來查看JVM運行時的狀況,用線程id去那個工具搜索。

內存溢出

JVM各個部分除了程序計數器之外全部部分均可能發生內存溢出。

  • 棧溢出

1.每一個棧都有最小固定大小的,1M。無限建線程,就會無限建棧,而後機器內存不足就會OOM。
2.棧的棧幀是有大小上限的(彈夾子彈有上限)。棧幀=方法,因此無限執行方法,會java.lang.StackOverflowError。

image.png image.png

  • 堆溢出

堆上對象太多太大,致使內存溢出,Android大部分都是堆溢出。
1.代碼正常能夠經過 調大堆大小。 -Xms,-Xmx參數。
2.大部分都是內存泄漏致使的,就應該檢查代碼了(異常持有引用等)。
3.檢查調整對象,是否不合理的設計,持有時間太長,對象太大手動清理一部分,對象生命週期太長。

  • 方法區溢出

(1) 運行時常量池溢出
(2)方法區中保存的Class對象沒有被及時回收掉或者Class信息佔用的內存超過了咱們配置。

  • 本機直接內存溢出

申請更大空間,或者檢查代碼。

虛擬機優化技術

1. 方法內聯:將簡單的方法直接不調用,放調用的地方複製一份執行(調用方法會在棧裏多一個棧幀,而後頻繁的入棧出棧)

image.png

2. 棧幀之間數據的共享(虛擬機已經優化好的)
棧幀之間會共用一些數據,這時不會建立多分,會共用一份。如a(10); 10這個變量傳遞在兩方法(棧幀)上,只有一份。

image.png

對象與垃圾回收機制

對象的建立

對象建立過程圖,很是重要
別的步驟看圖,分配內存的時候會根據內存是否工整採用不一樣的方式分配內存,分別是指針碰撞和空閒列表。
可是由於堆是多線程共享的,因此會引起線程安全問題,因此有兩種解決方案CAS機制和本地線程分配緩衝TLABimage.png

指針碰撞
image.png

空閒列表
image.png

CAS機制
CAS機制compare and swap比較而且交換,先找到空閒的區域,而後利用cpu的CAS指令,若是這塊內存無人佔據,那我就寫入,若是在執行CPU CAS指令時,已經被別人佔據了(比較compare之後跟我想要的不一樣)那就繼續循環找下一塊區域
詳情見線程 juejin.cn/post/695650…

本地線程分配緩衝TLAB
由於線程很少,並且所需的內存較小通常,因此 去 堆裏的新生代Eden區直接給每一個線程劃一塊內存,供其使用。
這塊區域很小,通常佔用Eden區的1%,2% image.png

對象的構成

對象分爲:對象頭、實例數據、對齊填充
image.png

對象的訪問定位

訪問對象主流的有兩種:句柄(堆裏劃出一個句柄池,用句柄池再來管理)和直接指針 image.png

判斷對象的存活

判斷對象是否存活,是否須要被GC回收,如今通常有兩種方式:引用計數算法和可達性分析(根可達)
image.png

可達性分析
GC Roots的對象(系統定義好的,堆之外的指針):
● 虛擬機棧(棧幀中的本地變量表)中引用的對象。
● 方法區中類靜態屬性引用的對象。
● 方法區中常量引用的對象。
● 本地方法棧中JNI(即通常說的Native方法)引用的對象。
● VM的內部引用(class對象、異常對象NullPointException、OutofMemoryError,系統類加載器)。
● 全部被同步鎖(synchronized關鍵)持有的對象。
● JVM內部的JMXBean、JVMTI中註冊的回調、本地代碼緩存等
● JVM實現中的「臨時性」對象,跨代引用的對象(在使用分代模型回收只回收部分代時)

各類引用

強>軟>弱>虛 image.png

對象的分配策略-對象建立的完整流程

  1. 有些對象是在棧上分配的,只要知足逃逸分析的小對象,就能在棧上分配(也就是方法裏的局部變量,別的方法、線程沒引用到)

image.png

好處:沒必要在堆上分配,速度快。棧是方法執行完了,裏面的內存就釋放了 不用GC
image.png

  1. 本地線程分配緩衝(TLAB)

image.png

  1. 是不是大對象,大對象直接分配到老年代(大的String,數組)。緣由是老年代不用頻繁GC、移動。並且老年代空間大。新生代:老年代=1/3 : 2/3

image.png

4.通常對象new出來都是在新生代的Eden(伊甸園)區。而後被頻繁GC活下來之後晉級到老年代。
1.對象出生在Eden區,對象頭上存儲的GC年齡age爲空(0歲)
2.第一次GC之後,會有90%的對象都被GC回收掉,剩下的10%會晉級到from區,這時age=1
3.第二次GC若是仍是活下來會晉級到To區,age=2,第三次第四次...會反覆在from區和to區跳而後年齡age++,直到age到達一個臨界值15(或者本身設置或者根據不一樣算法得出)
4.年齡age到達臨界值15,或者由於空間分配擔保會晉級到老年代Tenured區
image.png

image.png

image.png

image.png

對象的回收

對象的生講完了,如今講對象的死。也就是GC。

分代收集理論

GC垃圾回收器,在新生代和老年代的回收算法是不同的。新生代裏用的是複製算法。老年代用的是標記清除算法和標記整理算法
空間大小:新生代:老年代 = 1:2 。新生代裏 Eden:from:to = 8:1:1 image.png

複製算法(Copying)

image.png

標記-清除算法(Mark-Sweep)

優勢:不要整理,快,對象不須要移動
缺點:內存碎片 image.png

標記-整理算法(Mark-Compact)

image.png

JVM中常見的垃圾收集器

一代目:單線程。Serial--Serial Old---複製算法、標記整理算法
二代目:多線程並行。Parallel Scavenge--Parallel Old--複製算法、標記整理算法。就從單線程改爲多線程沒啥區別
三代目:多線程併發。ParNew--CMS---複製算法、標記清除算法

image.png

單線程與多線程並行

一代目、二代目 image.png

CMS垃圾回收器

Android採用的垃圾回收器
CMS將GC分紅好幾個階段:
1.先單獨執行初始標記(標記可達性分析的第一層)---執行速度快
2.併發標記 可達性分析根之外的葉子節點 --- 時間長,因此跟用戶線程併發執行
3.從新標記 在執行期間新new出來的對象
4.併發清理並重置線程 iii.png
image.png

垃圾回收器暫停用戶線程,而後再去執行GC的現象叫Stop The World

G1垃圾回收器

image.png

總結與面試

常量池與String

image.png

JVM內存結構說一下!

image.png image.png

什麼狀況下內存棧溢出?

java.lang.StackOverflowError 若是出現了可能會是無限遞歸。
OutOfMemoryError:不斷創建線程,JVM申請棧內存,機器沒有足夠的內存。

描述new一個對象的流程!

image.png

Java對象會不會分配在棧中?

能夠,若是這個對象不知足逃逸分析,那麼虛擬機在特定的狀況下會走棧上分配。

若是判斷一個對象是否被回收,有哪些算法,實際虛擬機使用得最多的是什麼?

引用計數法和根可達性分析兩種,用得最可能是根可達性分析。
image.png

GC收集算法有哪些?他們的特色是什麼?

複製、標記清除、標記整理。複製速度快,可是要浪費空間,不會內存碎片。標記清除空間利用率高,可是有內存碎片。標記整理算法沒有內存碎片,可是要移動對象,性能較低。三種算法各有所長,各有所短。

JVM中一次完整的GC流程是怎樣的?對象如何晉級到老年代?

對象優先在新生代區中分配,若沒有足夠空間,Minor GC;
大對象(須要大量連續內存空間)直接進入老年態;長期存活的對象進入老年態。
若是對象在新生代出生並通過第一次MGC後仍然存活,年齡+1,若年齡超過必定限制(15),則被晉升到老年態。

Java中的幾種引用關係,他們的區別是什麼?

image.png

final、finally、finalize的區別

在java中,final能夠用來修飾類,方法和變量(成員變量或局部變量)
當用final修飾類的時,代表該類不能被其餘類所繼承。當咱們須要讓一個類永遠不被繼承,此時就能夠用final修飾,但要注意:
final類中全部的成員方法都會隱式的定義爲final方法。
使用final方法的緣由主要有兩個:
(1) 把方法鎖定,以防止繼承類對其進行更改。
(2) 效率,在早期的java版本中,會將final方法轉爲內嵌調用。但若方法過於龐大,可能在性能上不會有多大提高。所以在最近版本中,不須要final方法進行這些優化了。
final成員變量表示常量,只能被賦值一次,賦值後其值再也不改變。

finally做爲異常處理的一部分,它只能用在try/catch語句中,而且附帶一個語句塊,表示這段語句最終必定會被執行(無論有沒有拋出異常),常常被用在須要釋放資源的狀況下

Object中的Finalize方法 即便經過可達性分析判斷不可達的對象,也不是「非死不可」,它還會處於「緩刑」階段,真正要宣告一個對象死亡,須要通過兩次標記過程,一次是沒有找到與GCRoots的引用鏈,它將被第一次標記。隨後進行一次篩選(若是對象覆蓋了finalize),咱們能夠在finalize中去拯救。 因此建議你們儘可能不要使用finalize,由於這個方法太不可靠。在生產中你很難控制方法的執行或者對象的調用順序,建議你們忘了finalize方法!由於在finalize方法能作的工做,java中有更好的,好比try-finally或者其餘方式能夠作得更好

相關文章
相關標籤/搜索