文章目錄
- 一、JVM組成
- 1.什么是程序計數器
- 2.什么是Java堆?
- 3.能不能介紹一下方法區(元空間)
- 4.你聽過直接內存嗎
- 5.什么是虛擬機棧
- 6.垃圾回收是否涉及棧內存?
- 7.棧內存分配越大越好嗎?
- 8.方法內的局部變量是否線程安全?
- 9.什么情況下會導致棧內存溢出?
- 10.堆棧的區別是什么
- 二、類加載器
- 11.什么是類加載器,類加載器有哪些
- 12.什么是雙親委派模型?
- 13.JVM為什么采用雙親委派機制?
- 14.說一下類裝載的執行過程
- 三、垃圾回收
- 15.強引用、軟引用、弱引用、虛引用區別:
- 15.1 對象什么時候可以被垃圾器回收
- 16.JVM 垃圾回收算法有哪些?
- 16.1 標記清除算法
- 16.2 標記整理算法( 標記清除算法的升級---多了一個整理階段)
- 16.3 復制算法
- 17.說一下JVM中的分代回收
- 17.1 MinorGC、 Mixed GC 、 FullGC的區別是什么
- 18.說一下JVM有哪些垃圾回收器?
- 19.詳細聊一下G1垃圾回收器
- 四、JVM實踐
- 20.JVM 調優的參數可以在哪里設置
- 21.用的 JVM 調優的參數都有哪些?
- 22.說一下 JVM 調優的工具?
- 23.Java內存泄露的排查思路?
- 24.CPU飆高排查方案與思路?
JVM是什么:
JVM(Java虛擬機)是Java程序的
運行環境
,它是Java平臺的核心組成部分之一。JVM提供了一個
運行Java字節碼的虛擬機
,負責將
Java程序解釋和執行。
Java程序員可以在JVM上編寫和運行Java程序,而不用考慮底層操作系統的差異性
。JVM的特性使得Java具備了跨平臺性
,同一份Java代碼可以在不同
的操作系統上運行
。
組成部分如下:
一、JVM組成
1.什么是程序計數器
程序計數器:線程私有的
,內部保存的字節碼的行號
。用于記錄正在執行的字節碼指令的地址。(通俗的來說就是記錄當前線程的程序執行的字節碼指令的行號)
2.什么是Java堆?
線程共享的區域:主要用來保存對象實例
,數組
等,當堆中沒有內存空間可分配給實例,也無法再擴展時,則拋出OutOfMemoryError異常。
Java1.8-JVM內存結構
其中:堆分為兩部分
年輕代
包括三部分,Eden區
和兩個大小嚴格相同的Survivor區
,根據JVM的策略,在經過
幾次垃圾收集
后,任然存活于Survivor的對象將被移動到老年代區間。
老年代
主要保存生命周期長
的對象,一般是一些老的對象
元空間
保存的類信息
、靜態變量
、常量
、編譯后的代碼
Java1.7-JVM內存結構
唯一的不同就是在java8的JVM中,元空間是在本地內存中的
,而java7當中元空間是作為方法區/永久代
存在于堆空間中的
因而在1.8之后做出改動,主要是為了防止OMM內存泄漏,因為元空間(方法區/永久代)保存的是類信息
、靜態變量
、常量
、編譯后的代碼
,隨著程序不斷龐大,實現很難預估元空間的大小,久而久之就會產生OMM。
在舊的Java版本(如Java 7及更早版本)中,方法區和永久代(PermGen)都是
堆
的一部分,用于存儲類的結構信息、常量池、靜態變量等。但永久代容易導致內存溢出的問題,因為它的大小是固定
的,并且無法在運行時動態調整。
為了解決這個問題,并改進類的元數據的存儲方式,Java 8 引入了元空間(Metaspace)。元空間通過使用本地內存
來存儲類的元數據,有效地解決了永久代的限制和內存溢出問題。與永久代不同,元空間的大小并不受堆內存大小的限制
,而是受系統的物理內存限制
。
元空間還具有自動調整大小的能力
,可以根據需要動態地分配和釋放內存
。這使得元空間更具靈活
性和可靠
性,并能更好地適應各種應用程序的需求。
3.能不能介紹一下方法區(元空間)
在不同的JVM實現中,方法區存在位置是不一樣的。
這里舉例在jdk1.7時的jvm
方法區(Method Area)是各個線程共
享的內存區域
主要存儲類的信息
、運行時常量池
虛擬機啟動
的時候創建
,關閉
虛擬機時釋放
如果方法區域中的內存無法滿足分配請求,則會拋出OutOfMemoryError: Metaspace
普通常量池
可以看作是一張表,虛擬機指令
根據這張常量表找到要執行的類名、方法名、參數類型、字面量等信息
通俗的說,就是記錄了虛擬機指令和執行的類名、方法名等信息的
映射關系
。
編譯階段:常量池的符號引用為這種
#+數字
,因為只是編譯階段,沒到運行階段,只需要直到機器指令和方法、類名等等的映射關系,能找到對應上就算編譯通過
運行時常量池
在類加載階段,JVM會將符號引用
解析為直接引用
(將符號替換為真實的地址值),并將其存儲在常量池中,以供運行
時使用。
*常量池是 .class 文件中的,當該類被加載,它的常量池信息就會放入運行時常量池,并把里面的
符號地址
變為真實地址
4.你聽過直接內存嗎
直接內存:并不屬于JVM中的內存結構
,不由JVM進行管理
。是虛擬機的系統內存
,常見于NIO
(非阻塞io)操作時,用于數據緩沖區,它分配回收成本較高,但讀寫性能高
直接內存最直接的體現就是常規IO和NIO
的區別:
我們都知道NIO是非阻塞的,IO是傳統阻塞式
IO傳統阻塞式
相當于java程序不能直接去讀系統內存給的數據,需要一個媒介就是堆內存java緩沖區
NIO非阻塞式
相比較IO是直接到直接內存讀數據,所以只要將
磁盤文件讀到直接內存
,數據準備就緒,程序切換到內核態就直接從直接內存讀數據
。
5.什么是虛擬機棧
通俗來說就是線程的方法棧
Java Virtual machine Stacks (java 虛擬機棧)
每個線程運行時所需要的內存
,稱為虛擬機棧,先進后出
每個棧由多個棧幀
(frame)組成(一個棧幀代表一個方法占用的內存),對應著每次方法調用時所占用的內存
每個線程
只能有一個活動棧幀
,對應著當前正在執行的那個方法
也就是一個方法(主方法)可以調用其他很多別的方法(子方法)
6.垃圾回收是否涉及棧內存?
垃圾回收主要指就是堆內存
,當棧幀彈棧
以后,內存就會釋放
7.棧內存分配越大越好嗎?
未必,默認的棧內存通常為1024k
棧幀過大會導致線程數變少
,例如,機器總內存為512m,目前能活動的線程數則為512個,如果把棧內存改為2048k,那么能活動的棧幀就會減半
很好理解,機器內存就那么多,單獨把
棧內存調大了
,那么棧幀的個數自然的得變少
,所以對應的線程數就會變少
8.方法內的局部變量是否線程安全?
如果方法內局部變量沒有逃離方法的作用范圍
,它是線程安全
的
如果是局部變量引用了對象,并逃離方法的作用范圍
,需要考慮線程安全
通俗來說,就是看
方法內的局部變量是不是完全包圍在方法內
(像如果是作為方法參數
傳過來的、作為返回值返回
的都可以算是逃離了方法
的作用范圍),這樣別的線程可以肆意修改方法內的局部變量
9.什么情況下會導致棧內存溢出?
棧幀過多導致棧內存溢出,典型問題:遞歸調用
棧幀過大導致棧內存溢出(基本上不存在)
棧的容量較小:相對于堆內存而言,棧內存的容量通常比較有限。
每個線程都有自己的棧空間
,而每個棧的大小通常只有幾MB到幾十MB
。這限制了棧幀的大小
,使得棧溢出的概率較低。
10.堆棧的區別是什么
分配對象和數據類型:堆內存用于分配Java對象
和數組
。所有通過關鍵字new創建的對象以及通過反射或JNI創建的對象都存儲在堆中。而棧內存主要用于存儲基本數據類型的變量
和方法調用時的局部變量。
存儲位置:堆內存位于JVM的堆區
,是一個共享的內存區域
。棧內存位于JVM的棧區
,每個線程
都有自己的獨立棧
。
內存管理方式:堆
內存的分配和回收由Java虛擬機的垃圾回收器負責管理
。垃圾回收器會自動回收不再使用的對象,并釋放對應的內存空間。而棧
內存的分配和回收是由編譯器自動管理的
,當方法執行結束
或變量超出作用域時,棧上的內存會自動被釋放
。
內存分配速度:堆
內存的分配速度相對較慢,因為需要進行復雜的垃圾回收算法
和對象定位操作。而棧
內存的分配速度較快,僅僅是簡單地進行指針移動。
內存空間大小限制:堆內存的大小一般比棧內存大得多
。堆內存的大小可以通過JVM的配置參數進行調整
。而棧
內存的大小是由線程的啟動參數
決定的,每個線程的棧大小通常是固定的。
對象的生命周期:堆內存中的對象生命周期可以很長
,可以在程序的任意位置被引用和訪問。而棧
內存中的局部變量生命周期與方法調用密切相關,當方法執行結束時,棧上的局部變量會自動銷毀。
總結來說,堆內存主要用于存儲Java對象和數組,由垃圾回收器管理,分配和回收速度相對較慢;而棧內存主要用于存儲基本數據類型的變量和方法調用時的局部變量,由編譯器自動管理,分配和回收速度相對較快。兩者在內存管理方式、存儲位置、分配速度和大小等方面有較大的區別。
二、類加載器
11.什么是類加載器,類加載器有哪些
類加載器
JVM只會運行二進制文件,類加載器
的作用就是將字節碼文件
加載到JVM中,從而讓Java程序能夠啟動起來。
類加載器包括四種
啟動類加載器(加載像庫里自帶的類string integer等等
)(BootStrap ClassLoader):加載JAVA_HOME/jre/lib目錄下的庫
擴展類加載器(ExtClassLoader):主要加載JAVA_HOME/jre/lib/ext目錄中的類
應用類加載器(加載自己java程序寫的類
)(AppClassLoader):用于加載classPath下的類
自定義類加載器(很少自己寫加載器
)(CustomizeClassLoader):自定義類繼承ClassLoader,實現自定義類加載規則。
12.什么是雙親委派模型?
加載某一個類,先委托上一級的加載器進行加載
,如果上級加載器也有上級,則會繼續向上委托
,如果該類委托上級沒有被加載
,子加載器嘗試加載該類
舉例:
假設有一個Student類
,需要加載
這個時候就會定位到AppclassLoader(應用類加載器),發現應用記載器有上級加載器,就先不加載,看看上級加載器里面有沒有加載過的Student類,一直往上找,發現都沒有加載過的Student類,那么此時AppclassLoader才會去加載Student類
假設有一個String類
,需要加載,也是定位到AppclassLoader(應用類加載器)往上委托直到找到啟動類加載器
,找到了加載好的String類,直接加載。
13.JVM為什么采用雙親委派機制?
JVM采用雙親委派機制(Parent Delegation Model)是為了解決兩個主要問題:
安全性
和避免類的重復加載
。
安全性體現在:
確保核心Java庫的安全性,避免惡意代碼通過自定義的類偽裝成核心類庫
,從而提高系統的安全性。
例如下面我們自己創建一個String包裝類,此時核心類庫是本身就會加載這個類,這個時候就會報錯,不允許加載自定義的核心庫的類。
雙親委派機制可以確保核心Java庫的安全性。當一個類需要被加載時,首先會
委派給父類加載器進行查找和加載
,只有在父類加載器無法找到該類時
,才會由當前類加載器自己去加載。
避免類的重復加載體現在
通過雙親委派機制,當一個類需要被加載時,首先由父類加載器嘗試加載,如果父類加載器已經加載了該類,就直接返回;否則,再由子類加載器嘗試加載。這種機制可以確保在整個類加載器層次結構中
,每個類只被加載一次
,避免了類的重復加載,提高了運行效率
。
14.說一下類裝載的執行過程
類在裝載的時候會經歷7個過程:
- 加載:查找和導入class文件
- 驗證:保證加載類的準確性
- 準備:為類變量分配內存并設置類變量初始值
- 解析:把類中的符號引用轉換為直接引用
- 初始化:對類的靜態變量,靜態代碼塊執行初始化操作
- 使用:JVM 開始從入口方法開始執行用戶的程序代碼
- 卸載:當用戶程序代碼執行完畢后,JVM便開始銷毀創建的Class對象。
詳細參考鏈接:【JVM】類裝載的執行過程
三、垃圾回收
15.強引用、軟引用、弱引用、虛引用區別:
強引用:只有所有 GC Roots 對象都不通過【強引用】引用該對象
,該對象才能被垃圾回收
向以下這種情況就不會被回收
軟引用:僅有軟引用引用該對象時,在垃圾回收后
,內存仍不足時會再次出發垃圾回收
弱引用:僅有弱引用引用該對象時,在垃圾回收時,無論內存是否充足
,都會回收弱引用對象
虛引用:必須配合引用隊列使用
,被引用對象回收時
,會將虛引用入隊
,由 Reference Handler 線程調用虛引用相關方法釋放
直接內存
總結:
- 強引用:
只要所有 GC Roots 能找到,就不會被回收
- 軟引用:需要
配合SoftReference使用
,當垃圾多次回收,內存依然不夠的時候會回收軟引用對象
- 弱引用:需要
配合WeakReference使用
,只要進行了垃圾回收,就會把弱引用對象回收
- 虛引用:必須配合
引用隊列使用
,被引用對象回收時,會將虛引用入隊
,由Reference Handler線程
調用虛引用相關方法釋放直接內存
15.1 對象什么時候可以被垃圾器回收
垃圾回收,回收的是針對于堆的
簡單一句就是:如果一個或多個對象沒有任何的引用指向它
了,那么這個對象現在就是垃圾,如果定位了垃圾
,則有可能會被垃圾回收器回收。
如果要定位什么是垃圾,有兩種方式來確定,第一個是引用計數法,第二個是可達性分析算法
引用計數法(不常用)
一個對象被引用了一次,在當前的對象頭上遞增一次引用次數(ref=1)
,如果這個對象的引用次數為0(ref=0)
,代表這個對象可回收
但是當對象間出現了循環引用
的話,則引用計數法就會失效
可達性分析算法
現在的虛擬機采用的都是通過可達性分析算法來確定哪些內容是垃圾。
比如下面的例子
X,Y這兩個節點是可回收的
Java 虛擬機中的垃圾回收器采用可達性分析來探索所有存活的對象
掃描堆中的對象,看是否能夠沿著 GC Root 對象
為起點的引用鏈
找到該對象,找不到,表示可以回收
哪些對象可以作為 GC Root ?
-
虛擬機棧(棧幀中的本地變量表)中引用的對象
-
方法區中類靜態屬性引用的對象
-
方法區中常量引用的對象
-
本地方法棧中 JNI(即一般說的 Native 方法)引用的對象
16.JVM 垃圾回收算法有哪些?
參考鏈接:GC詳解、GC四大算法和GC Root
總共分為三種:
標記清除算法
:垃圾回收分為2個階段,分別是標記和清除,效率高,有磁盤碎片,內存不連續
標記整理算法
:標記清除算法一樣,將存活對象都向內存另一端移動,然后清理邊界以外的垃圾,無碎片,對象需要移動,效率低
復制算法:
將原有的內存空間一分為二,每次只用其中的一塊,正在使用的對象復制到另一個內存空間中,然后將該內存空間清空,交換兩個內存的角色,完成垃圾的回收;無碎片,內存使用率低
16.1 標記清除算法
標記清除算法,是將垃圾回收分為2個階段,分別是標記和清除
。
1.根據可達性分析算法
得出的垃圾進行標記
2.對這些標記為可回收的內容
進行垃圾回收
注意:標記GC Root引用鏈的對象,回收的就是沒有被標記的
缺點:
- 需要兩次掃描,耗時嚴重。
先掃描一次,對
存活的對象進行標記。
再次掃描,回收沒有被標記的對象。
- 會產生內存碎片,導致內存空間不連續。
當需要分配一個較大的內存塊時,由于
沒有足夠的連續內存空間
,可能會導致分配失敗即內存溢出。
16.2 標記整理算法( 標記清除算法的升級—多了一個整理階段)
優缺點同標記清除算法,解決了標記清除算法的碎片化的問題
,同時,標記壓縮算法多了一步,對象移動內存位置的步驟,其效率也有有一定的影響。
相比較標記清除算法在垃圾回收后,會將活著的對象滑動到一側,這樣就能讓空出的內存空間是連續的。
16.3 復制算法
相當于把一塊堆內存分成兩半來用,一般拿來存對象,另一半拿來做垃圾回收后的整理收納倉(必須為空閑空間)
復制算法需要將存活的對象
從一個區域
復制到另一個區域
(空白區域),然后直接清空存活對象和待回收對象的那一片區域
注意:要求堆內存的
使用比例不超過50%
,因為每次垃圾回收時,需要保證目標區域有足夠的空間來存放從源區域復制過來的對象。
優點:
在垃圾對象多的情況下,效率較高
清理后,內存無碎片
缺點:
分配的2塊內存空間,在同一個時刻,只能使用一半,內存使用率較低
17.說一下JVM中的分代回收
一、堆的區域劃分
堆被分為了兩份:新生代和老年代
【1:2】
對于新生代,內部又被分為了三個區域。Eden區
,幸存者區survivor(分成from和to)
【8:1:1】
二、對象回收分代回收策略
- 新創建的對象,都會先分配到eden區
- 當伊甸園內存不足,標記伊甸園與 from(現階段沒有)的存活對象
- 將存活對象采用復制算法復制到to中,復制完畢后,伊甸園和 from 內存都得到釋放
- 經過一段時間后伊甸園的內存又出現不足,標記eden區域to區存活的對象,將其復制到from區
- 當幸存區對象熬過幾次回收(最多15次),晉升到老年代(幸存區內存不足或大對象會提前晉升)
參考鏈接:【JVM】JVM中的分代回收
17.1 MinorGC、 Mixed GC 、 FullGC的區別是什么
STW(Stop-The-World):暫停所有應用程序線程,等待垃圾回收的完成
MinorGC、 Mixed GC 、 FullGC 相當于垃圾回收的等級:
MinorGC:【young GC】發生在新生代
的垃圾回收,暫停時間短(STW)
Mixed GC: 新生代 + 老年代部分區域
的垃圾回收,G1 收集器特有
FullGC: 新生代 + 老年代完整
垃圾回收,暫停時間長(STW),應盡力避免
18.說一下JVM有哪些垃圾回收器?
在jvm中,實現了多種垃圾收集器,包括:
-
串行垃圾收集器
Serial 作用于新生代,采用復制算法
Serial Old 作用于老年代,采用標記-整理算法 -
并行垃圾收集器(
JDK8默認使用此垃圾回收器
)
Parallel New作用于新生代,采用復制算法
Parallel Old作用于老年代,采用標記-整理算法 -
CMS(并發)垃圾收集器
針對老年代垃圾回收的 -
G1垃圾收集器(
在JDK9之后默認使用
)
應用于新生代和老年代
詳細參考鏈接:【JVM】JVM垃圾收集器
19.詳細聊一下G1垃圾回收器
詳細參考鏈接:【JVM】JVM垃圾收集器
G1垃圾收集器的設計目標是
在可控的停頓時間
內實現高吞吐量的垃圾回收。
- 應用于
新生代
和老年代
- 劃分成
多個區域
,每個區域
都可以充當 eden,survivor,old, humongous,其中humongous 專為大對象準備
- 采用
標記整理算法
因為基本上G1主要針對大型堆內存進行垃圾回收,而復制算法在大型堆內存上的應用存在一些挑戰和限制。(必須考慮內存空間使用率)
-
響應時間與吞吐量兼顧
-
分成三個階段:新生代回收、并發標記、混合收集(在不同的條件下被觸發)
-
如果并發失敗(即回收速度趕不上創建新對象速度),會觸發 Full GC(盡量避免)
如果對象內存分配速度過快,mixed gc來不及回收,導致老年代被填滿,就會觸發一次full gc,G1的full gc算法就是單線程執行的serial old gc,會導致異常長時間的暫停時間,需要進行不斷的調優,盡可能的避免full gc.
四、JVM實踐
20.JVM 調優的參數可以在哪里設置
war包部署在tomcat中設置
jar包部署在啟動參數設置
21.用的 JVM 調優的參數都有哪些?
- 設置堆空間大小
設置堆的初始大小和最大大小,為了防止垃圾收集器在初始大小、最大大小之間收縮堆而產生額外的時間,通常把最大、初始大小設置為相同的值
。
堆空間設置多少合適?
最大大小的默認值是物理內存的1/4,初始大小是物理內存的1/64
堆太小,可能會頻繁的導致年輕代和老年代的垃圾回收,會產生stw,暫停用戶線程
堆內存大肯定是好的,存在風險,假如發生了fullgc,它會掃描整個堆空間,暫停用戶線程的時間長
設置參考推薦:盡量大,也要考察一下當前計算機其他程序的內存使用情況
- 虛擬機棧的設置
虛擬機棧的設置:每個線程默認會開啟1M的內存
,用于存放棧幀、調用參數、局部變量等,但一般256K就夠用
。通常減少每個線程的堆棧,可以產生更多的線程,但這實際上還受限于操作系統。
- 年輕代中Eden區和兩個Survivor區的大小比例
設置年輕代中Eden區和兩個Survivor區的大小比例。該值如果不設置,則默認比例為8:1:1
。通過增大Eden區的大小,來減少YGC發生的次數
,但有時我們發現,雖然次數減少了,但Eden區滿的時候,由于占用的空間較大,導致釋放緩慢,此時STW的時間較長,因此需要按照程序情況去調優。
-
年輕代晉升老年代閾值
-
設置垃圾回收收集器
22.說一下 JVM 調優的工具?
命令工具
-
jps 進程狀態信息
-
jstack 查看java進程內線程的堆棧信息
-
jmap 查看堆轉信息
-
jhat 堆轉儲快照分析工具
-
jstat JVM統計監測工具
可視化工具
-
jconsole 用于對jvm的內存,線程,類 的監控
-
VisualVM 能夠監控線程,內存情況(只有jdk8才有)
23.Java內存泄露的排查思路?
內存泄漏通常是指堆內存
,通常是指一些大對象不被回收的情況
1、通過jmap或設置jvm參數獲取堆內存快照dump
2、通過工具, VisualVM去分析dump文件
,VisualVM可以加載離線的dump文件
3、通過查看堆信息的情況,可以大概定位內存溢出是哪行代碼出了問題
4、找到對應的代碼,通過閱讀上下文的情況,進行修復即可
參考鏈接:【JVM】Java內存泄露的排查思路?
24.CPU飆高排查方案與思路?
1.使用top命令查看占用cpu的情況
2.通過top命令查看后,可以查看是哪一個進程占用cpu較高
3.使用ps命令查看進程中的線程信息
4.使用jstack命令查看進程中哪些線程出現了問題,最終定位問題
參考鏈接:【JVM】CPU飆高排查方案與思路