面試常見:
- 請你談談你對JVM的理解?
- java8虛擬機和之前的變化更新?
- 什么是OOM,什么是棧溢出StackOverFlowError? 怎么分析?
- JVM的常用調優參數有哪些?
- 內存快照如何抓取?怎么分析Dump文件?
- 談談JVM中,類加載器你的認識?
請你談談你對JVM的理解? JVM(Java虛擬機)是Java程序的運行環境,它允許Java程序在不同的平臺上運行。JVM負責將Java源代碼編譯成字節碼,并在運行時解釋或編譯執行這些字節碼。JVM還負責內存管理、垃圾回收、安全性等任務。它的主要優點是跨平臺性和自動內存管理。
java8虛擬機和之前的變化更新? Java 8的虛擬機相比之前的版本有一些變化和更新,其中包括:
- Lambda表達式和函數式接口的支持,使得在JVM中能更方便地使用函數式編程風格。
- 元空間(Metaspace)的引入,取代了永久代(Permanent Generation),用于存儲類的元數據信息。
- 默認方法(Default Methods)的支持,允許接口中有具體的方法實現。
- 增強的垃圾回收器性能和功能。
- 新的時間和日期API(java.time包)的引入。
- 什么是OOM,什么是棧溢出StackOverFlowError? 怎么分析?
- OOM(Out of Memory)是指JVM內存不足,無法分配更多的內存給應用程序使用,導致應用程序無法繼續執行的情況。這可能是由于堆內存溢出、方法區(元空間)溢出或者線程棧空間不足等原因引起的。
- 棧溢出(StackOverflowError)是指線程調用棧的深度超過了JVM所允許的最大深度,導致棧空間耗盡而拋出的錯誤。通常是由于遞歸調用或者方法調用層級過深引起的。
- 要分析這些問題,可以通過查看JVM的日志、堆棧跟蹤信息以及內存使用情況來確定問題的根源,并可能通過調整JVM參數或者修改代碼來解決問題。
- JVM的常用調優參數有哪些? 常用的JVM調優參數包括:
- -Xms:設置初始堆大小
- -Xmx:設置最大堆大小
- -Xss:設置線程棧大小
- -XX:PermSize和-XX:MaxPermSize:設置永久代(Java 8之前)或元空間(Java 8及之后)大小
- -XX:+UseG1GC:啟用G1垃圾回收器
- -XX:+UseConcMarkSweepGC:啟用CMS垃圾回收器
- -XX:MaxGCPauseMillis:設置最大垃圾回收停頓時間
- -XX:NewRatio:設置新生代與老年代的大小比例
內存快照如何抓取?怎么分析Dump文件? 內存快照可以通過JVM工具(如jmap、jcmd)或者第三方工具(如VisualVM、MAT)來抓取。分析Dump文件通常可以使用MAT(Memory Analyzer Tool)等工具,這些工具能夠解析Dump文件并生成分析報告,幫助定位內存泄漏、對象占用過多內存等問題。
談談JVM中,類加載器你的認識? 類加載器負責將類文件加載到JVM中,并生成對應的Class對象。JVM中的類加載器主要分為三種:啟動類加載器(Bootstrap ClassLoader)、擴展類加載器(Extension ClassLoader)和應用程序類加載器(Application ClassLoader)。類加載器采用雙親委派模型,即當一個類加載器收到加載類的請求時,它會先將請求委派給父類加載器處理,只有在父類加載器無法完成加載時,才會自己嘗試加載。這種機制保證了類的唯一性和安全性。自定義類加載器可以實現一些特殊的類加載行為,例如從網絡或者其他非標準的地方加載類。
1.JVM的位置
-
JVM(Java虛擬機, Java Virtual Machine)是Java程序的運行環境,它負責解釋Java字節碼并將其轉換為機器碼以在特定平臺上執行。JVM通常安裝在操作系統之上,并提供了一個虛擬的運行時環境來執行Java應用程序。
-
JVM的位置通常取決于安裝的Java開發工具包(JDK)。在安裝了JDK的系統中,JVM的二進制文件通常存儲在JDK安裝目錄的子目錄中,例如在Windows系統中,它可能位于
C:\Program Files\Java\jdk<version>\bin
目錄下。
三種JVM:
- Sun公司:HotSpot 用的最多
- BEA:JRockit
- IBM:J9VM
我們學習都是:HotSpot
2.JVM的體系結構
紅色的為多個線程共享,灰色的為單個線程私有的
線程間共享:堆,方法區.
線程私有:程序計數器,棧,本地方法棧.
程序在執行之前先要把 java 代碼轉換成字節碼(class 文件),jvm 首先需要把字節碼通過一定的方式 類加載器(ClassLoader) 把文件加載到內存中的運行時數據區(Runtime Data Area)的方法區(Method Area) ,而字節碼文件是 jvm 的一套指令集規范,并不能直接交個底層操作系統去執行,因此需要特定的命令解析器 執行引擎(Execution Engine) 將字節碼翻譯成底層系統指令再交由CPU 去執行,而這個過程中需要調用其他語言的接口 本地庫接口(Native Interface) 來實現整個程序的功能,這就是這 4 個主要組成部分的職責與功能。
而我們通常所說的 JVM 組成指的是 運行時數據區(Runtime Data Area) ,因為通常需要程序員調試分析的區域就是“運行時數據區”,或者更具體的來說就是“運行時數據區”里面的 Heap(堆)模塊。
-
jvm調優:99%都是在方法區和堆,大部分時間調堆。 JNI(java native interface)本地方法接口。
-
為什么棧和 P C 沒有垃圾回收? \red {為什么棧和PC沒有垃圾回收?} 為什么棧和PC沒有垃圾回收?
棧(Stack)和 PC(Program Counter,程序計數器)通常不需要進行垃圾回收,因為它們所占用的內存空間具有確定的生命周期,并且由編譯器或虛擬機進行管理。
-
棧: 棧是一種數據結構,用于存儲函數調用時的局部變量、函數參數、返回地址等數據。棧上的數據存儲遵循后進先出(LIFO)的原則,每當函數調用時,都會為該函數分配一塊棧幀(Stack Frame)用于存儲相關數據。當函數執行完畢時,其對應的棧幀會被彈出,從而釋放其占用的內存空間。這種自動的分配和釋放機制使得棧上的內存管理成本較低,并且不需要進行垃圾回收。
-
程序計數器: 程序計數器是一種特殊的寄存器,用于存儲當前線程正在執行的指令地址。在程序執行過程中,程序計數器會隨著指令的執行而不斷更新,指向下一條將要執行的指令。程序計數器的生命周期與線程的生命周期密切相關,當線程結束時,程序計數器的內容也會被銷毀。由于程序計數器的內容是由硬件直接管理的,因此不需要進行垃圾回收。
-
3.類加載器
- 作用:加載Class文件——如果new Student();(具體實例在堆里,引用變量名放棧里) 。
- 先來看看一個類加載到 JVM 的一個基本結構:
- 類是模板,對象是具體的,通過new來實例化對象。car1,car2,car3,名字在棧里面,真正的實例,具體的數據在堆里面,棧只是引用地址。
類加載器分類:
JVM角度
引導類加載器(啟動類加載器 Bootstrap ClassLoader).
其他所有類加載器,這些類加載器由 java 語言實現,獨立存在于虛擬機外部,并 且全部繼承自抽象類 java.lang.ClassLoader.
開發人員角度
啟動類加載器/根加載器/啟動類加載器 Bootstrap ClassLoader
加載
JAVA_HOME\jre\lib\rt.jar
中的類(runtime, Java 運行環境的核心庫目錄)啟動類加載器通常只加載指定的核心類庫rt.jar,并不會加載
lib
目錄下的其他 JAR 文件。這個類加載器使用 C/C++語言實現,嵌套在 JVM 內部.它用來加載 java 核心類庫.并不繼承于 java.lang.ClassLoader 沒有父加載器,java 程序獲取不到
負責加載擴展類加載器和應用類加載器,并為他們指定父類加載器.
出于安全考慮,引用類加載器只加載存放在
JAVA_HOME\jre\lib\rt.jar
目錄,或者被-Xbootclasspath 參數鎖指定的路徑中存儲放的類擴展類加載器 Extension ClassLoader
Java 語言編寫的,由 sun.misc.Launcher$ExtClassLoader 實現.
派生于 ClassLoader 類.
系統類加載器/應用程序類加載器 System/App ClassLoader
從 java.ext.dirs 系統屬性所指定的目錄中加載類庫,或從 JDK 系統安裝目錄的
JAVA_HOME\jre\lib\ext
子目錄(擴展目錄)下加載類庫.如果用戶創建的 jar 放在此目錄下,也會自動由擴展類加載器加載Java 語言編寫的,由 sun.misc.Launcher$AppClassLoader 實現.
派生于 ClassLoader 類.
加載我們自己定義的類,用于加載用戶類路徑(classpath)上所有的類.
該類加載器是程序中默認的類加載器.
ClassLoader 類 , 它 是 一 個 抽 象 類 , 其 后 所 有 的 類 加 載 器 都 繼 承 自 ClassLoader(不包括啟動類加載器)
package github.JVM.Demo01;/** * @author subeiLY * @create 2021-06-08 07:42 */
public class Test01 {public static void main(String[] args) {Test01 test01 = new Test01();Test01 test02 = new Test01();Test01 test03 = new Test01();System.out.println(test01.hashCode());System.out.println(test02.hashCode());System.out.println(test03.hashCode());
/*18360192403250408041173230247 */Class<? extends Test01> aClass1 = test01.getClass();ClassLoader classLoader = aClass1.getClassLoader();System.out.println(classLoader);System.out.println(classLoader.getParent());System.out.println(classLoader.getParent().getParent());
/*sun.misc.Launcher$AppClassLoader@18b4aac2sun.misc.Launcher$ExtClassLoader@330bedb4null */Class<? extends Test01> aClass2 = test02.getClass();Class<? extends Test01> aClass3 = test03.getClass();System.out.println(aClass1.hashCode());System.out.println(aClass2.hashCode());System.out.println(aClass3.hashCode());/* 2133927002 2133927002 2133927002 */}
}
類加載器的分類
- Bootstrap ClassLoader 啟動類加載器
- Extention ClassLoader 標準擴展類加載器
- Application ClassLoader 應用類加載器
- User ClassLoader 用戶自定義類加載器
類加載過程:
2、2、1 加載:
- 通過類名(地址)獲取此類的二進制字節流。
- 將這個字節流所代表的靜態存儲結構轉換為方法區(元空間)的運行時結構。
- 在內存中生成一個代表這個類的 java.lang.Class 對象,作為這個類的各種數據的訪問入口。
2、2、2 鏈接:
- 驗證:
- 文件格式驗證:檢查 class 文件是否以 CA FE BA BE 開頭。
- 主、次版本號驗證:確認版本號在當前 Java 虛擬機接收范圍內。對于 Java 17.0.1,其主版本號是 17,次版本號是 0, 1 表示補丁或更新版本。
- 元數據驗證:對字節碼描述的信息進行語義分析,確保符合 Java 語言規范的要求。
- 準備:
- 為類的靜態屬性分配內存,并設置默認初始值。
- 不包含用
final
修飾的 static 常量,在編譯時進行初始化。
- 解析:
- 將類的二進制數據中的符號引用替換成直接引用。(符號引用是 Class 文件的邏輯符號,直接引用指向的方法區中某一個地址)
2、2、3 初始化:
- 為類的靜態變量賦予正確的初始值,執行類構造器方法
<clinit>()
的過程。該方法是編譯器自動收集類中所有類變量的賦值動作和靜態代碼塊中的語句合并而來的。
實例化過程:
類加載完成 + 以下過程
- 創建對象:一旦類加載和初始化完成,就可以創建該類的對象實例了。對象實例化的過程包括:
分配內存空間:在堆內存中為對象分配內存空間,內存大小取決于對象的大小和結構。
初始化對象:將對象的實例變量(即非靜態變量)設置為默認值,如果有顯式的初始化操作,則會執行對應的初始化代碼。
調用構造方法:在內存中分配了足夠的空間后,會調用對象的構造方法進行初始化。構造方法負責對對象進行初始化操作,可以設置對象的初始狀態和執行其他必要的操作。
- 返回引用:對象實例化完成后,會返回對象的引用,以便能夠在程序中使用該對象。
類什么時候初始化:
- 每個類或接口被首次主動使用時才對其進行初始化,主動使用包括:
- 通過
new
關鍵字創建對象。 - 訪問類的靜態變量,包括讀取和更新。
- 訪問類的靜態方法。
- 對某個類進行反射操作。
- 初始化子類會導致父類的初始化。
- 執行該類的
main
函數。
- 除了以上幾種主動使用,以下情況被動使用,不會加載類:
-
引用該類的靜態常量,但注意只有已經指定字面量的常量不會導致初始化,對于需要計算才能得出結果的常量會導致類加載。
//不會導致類初始化,被動使用 public final static int NUMBER = 5 ; //會導致類加載 public final static int RANDOM = new Random().nextInt() ;
-
構造某個類的數組時不會導致該類的初始化。
Student[] students = new Student[10] ;
4.雙親委派機制
package java.lang;/** * @author subeiLY * @create 2021-06-08 08:06 */
public class String {/* 雙親委派機制:安全
1.APP-->EXC-->BOOT(最終執行) BOOT EXC APP */public String toString() {return "Hello";}public static void main(String[] args) {String s = new String();System.out.println(s.getClass());s.toString();}/*
1.類加載器收到類加載的請求
2.將這個請求向上委托給父類加載器去完成,一直向上委托,知道啟動類加載
3.啟動加載器檢查是否能夠加載當前這個類,能加載就結束,使用當前的加載器,否則,拋出異常,適知子加載器進行加載
4.重復步驟3 */
}
- idea報了一個錯誤:
雙親委派機制
如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請 求委托給父類的加載器去執行.
如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終 將到達頂層的啟動類加載器.
如果父類加載器可以完成類的加載任務,就成功返回,倘若父類加載器無法完 成加載任務,子加載器才會嘗試自己去加載,這就是雙親委派機制.
如果均加載失敗,就會拋出 ClassNotFoundException 異常。
優點:
安全:可避免用戶自己編寫的類替換 Java 的核心類,如 java.lang.String.
避免類重復加載:當父親已經加載了該類時,就沒有必要子 ClassLoader 再加載一次
如何打破雙親委派機制
Java 虛擬機的類加載器本身可以滿足加載的要求,但是也允許開發者自定義類加載器。
在 ClassLoader 類中涉及類加載的方法有兩個,loadClass(String name), findClass(String name),這兩個方法并沒有被 final 修飾,也就表示其他子類可以重寫.
重寫 loadClass 方法(是實現雙親委派邏輯的地方,修改他會破壞雙親委派機制, 不推薦)
重寫 findClass 方法 (推薦)
我們可以通過自定義類加載重寫方法打破雙親委派機制, 再例如 tomcat 等都有自己定義的類加載器.
-
關于雙親委派機制的博客:
你確定你真的理解“雙親委派“了嗎?!
雙親委派模型中,類加載器之間的父子關系一般不會以繼承(Inheritance)的關系來實現,而是都使用組合(Composition)關系來復用父加載器的代碼的。
實現雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()方法之中, 主要就是以下幾個步驟:
1、先檢查類是否已經被加載過
2、若沒有加載則調用父加載器的loadClass()方法進行加載
3、若父加載器為空則默認使用啟動類加載器作為父加載器。
4、如果父類加載失敗,拋出ClassNotFoundException異常后,再調用自己的findClass()方法進行加載。
如何主動破壞雙親委派機制?
知道了雙親委派模型的實現,那么想要破壞雙親委派機制就很簡單了。
因為他的雙親委派過程都是在loadClass方法中實現的,那么想要破壞這種機制,那么就自定義一個類加載器,重寫其中的loadClass方法,使其不進行雙親委派即可。
雙親委派被破壞的例子
雙親委派機制的破壞不是什么稀奇的事情,很多框架、容器等都會破壞這種機制來實現某些功能。
- 第一種被破壞的情況是在雙親委派出現之前。
由于雙親委派模型是在JDK1.2之后才被引入的,而在這之前已經有用戶自定義類加載器在用了。所以,這些是沒有遵守雙親委派原則的。
第二種,是JNDI、JDBC等需要加載SPI接口實現類的情況。
DriverManager是被根加載器加載的,那么在加載時遇到以上代碼,會嘗試加載所有Driver的實現類,但是這些實現類基本都是第三方提供的,根據雙親委派原則,第三方的類不能被根加載器加載。
于是,就在JDBC中通過引入ThreadContextClassLoader(線程上下文加載器,默認情況下是AppClassLoader)的方式破壞了雙親委派原則。
第三種是為了實現熱插拔熱部署工具。為了讓代碼動態生效而無需重啟,實現方式時把模塊連同類加載器一起換掉就實現了代碼的熱替換。
第四種時tomcat等web容器的出現。
Tomcat是web容器,那么一個web容器可能需要部署多個應用程序。
不同的應用程序可能會依賴同一個第三方類庫的不同版本,但是不同版本的類庫中某一個類的全路徑名可能是一樣的。
如多個應用都要依賴hollis.jar,但是A應用需要依賴1.0.0版本,但是B應用需要依賴1.0.1版本。這兩個版本中都有一個類是com.hollis.Test.class。
如果采用默認的雙親委派類加載機制,那么是無法加載多個相同的類。
所以,Tomcat破壞雙親委派原則,提供隔離的機制,為每個web容器單獨提供一個WebAppClassLoader加載器。
第五種時OSGI、Jigsaw等模塊化技術的應用。
面試官:java雙親委派機制及作用
-
概念:當某個類加載器需要加載某個.class文件時,它首先把這個任務委托給他的上級類加載器,遞歸這個操作,如果上級的類加載器沒有加載,自己才會去加載這個類。
-
例子:當一個Hello.class這樣的文件要被加載時。不考慮我們自定義類加載器,首先會在AppClassLoader中檢查是否加載過,如果有那就無需再加載了。如果沒有,那么會拿到父加載器,然后調用父加載器的loadClass方法。父類中同理也會先檢查自己是否已經加載過,如果沒有再往上。注意這個類似遞歸的過程,直到到達Bootstrap classLoader之前,都是在檢查是否加載過,并不會選擇自己去加載。直到BootstrapClassLoader,已經沒有父加載器了,這時候開始考慮自己是否能加載了,如果自己無法加載,會下沉到子加載器去加載,一直到最底層,如果沒有任何加載器能加載,就會拋出ClassNotFoundException。
從上到下第一個ExtClassLoader 改為 BootstrapClassLoader
作用:
- 防止重復加載同一個.class。通過委托去向上面問一問,加載過了,就不用再加載一遍。保證數據安全。
- 保證核心.class不能被篡改。通過委托方式,不會去篡改核心.class,即使篡改也不會去加載,即使加載也不會是同一個.class對象了。不同的加載器加載同一個.class也不是同一個Class對象。這樣保證了Class執行安全。
比如:如果有人想替換系統級別的類:String.java。篡改它的實現,在這種機制下這些系統的類已經被Bootstrap classLoader加載過了(為什么?因為當一個類需要加載的時候,最先去嘗試加載的就是BootstrapClassLoader),所以其他類加載器并沒有機會再去加載,從一定程度上防止了危險代碼的植入。
5.沙箱安全機制
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一個限制程序運行的環境。沙箱機制就是將Java代碼限定在虛擬機(JVM)特定的運行范圍中,并且嚴格限制代碼對本地系統資源訪問,通過這樣的措施來保證對代碼的有效隔離,防止對本地系統造成破壞。沙箱主要限制系統資源訪問,那系統資源包括什么?CPU、內存、文件系統、網絡。不同級別的沙箱對這些資源訪問的限制也可以不一樣。
? 所有的Java程序運行都可以指定沙箱,可以定制安全策略。
? 在]ava中將執行程序分成本地代碼和遠程代碼兩種,本地代碼默認視為可信任的,而遠程代碼則被看作是不受信的。對于授信的本地代碼,可以訪問一切本地資源。而對于非授信的遠程代碼在早期的ava實現中,安全依賴于沙箱(Sandbox)機制。如下圖所示JDK1.0安全模型。
? 但如此嚴格的安全機制也給程序的功能擴展帶來障礙,比如當用戶希望遠程代碼訪問本地系統的文件時候,就無法實現。因此在后續的Java1.1 版本中,針對安全機制做了改進,增加了安全策略,允許用戶指定代碼對本地資源的訪問權限。如下圖所示JDK1.1安全模型。
? 在Java1.2版本中,再次改進了安全機制,增加了代碼簽名。不論本地代碼或是遠程代碼,都會按照用戶的安全策略設定,由類加載器加載到虛擬機中權限不同的運行空間,來實現差異化的代碼執行權限控制。如下圖所示JDK1.2安全模型。
? 當前最新的安全機制實現,則引入了域(Domain)的概念。虛擬機會把所有代碼加載到不同的系統域和應用域,系統域部分專門負責與關鍵資源進行交互,而各個應用域部分則通過系統域的部分代理來對各種需要的資源進行訪問。虛擬機中不同的受保護域(Protected Domain),對應不一樣的權限(Permission)。存在于不同域中的類文件就具有了當前域的全部權限,如下圖所示最新的安全模型(jdk 1.6)。
組成沙箱的基本組件:
-
字節碼校驗器(bytecode verifier)
︰確保Java類文件遵循lava語言規范。這樣可以幫助java程序實現內存保護。但并不是所有的類文件都會經過字節碼校驗,比如核心類。 -
類裝載器(class loader)
:其中類裝載器在3個方面對Java沙箱起作用:。它防止惡意代碼去干涉善意的代碼;
。它守護了被信任的類庫邊界;
。它將代碼歸入保護域,確定了代碼可以進行哪些操作。
? 虛擬機為不同的類加載器載入的類提供不同的命名空間,命名空間由一系列唯一的名稱組成,每一個被裝載的類將有一個名字,這個命名空間是由Java虛擬機為每一個類裝載器維護的,它們互相之間甚至不可見。
類裝載器采用的機制是雙親委派模式。
1.從最內層VM自帶類加載器開始加載,外層惡意同名類得不到加載從而無法使用;
2.由于嚴格通過包來區分了訪問域,外層惡意的類通過內置代碼也無法獲得權限訪問到內層類,破壞代碼就自然無法生效。
存取控制器(access controller)
︰存取控制器可以控制核心API對操作系統的存取權限,而這個控制的策略設定,可以由用戶指定。安全管理器(security manager)
︰是核心API和操作系統之間的主要接口。實現權限控制,比存取控制器優先級高。安全軟件包(security package)
: java.security下的類和擴展包下的類,允許用戶為自己的應用增加新的安全特性,包括:- 安全提供者
- 消息摘要
- 數字簽名
- 加密
- 鑒別
6.Native關鍵字
- 編寫一個多線程類啟動。
public static void main(String[] args) { new Thread(()->{ },"your thread name").start(); }
- 點進去看start方法的源碼:
public synchronized void start() {if (threadStatus != 0)throw new IllegalThreadStateException();group.add(this);boolean started = false;try {start0(); // 調用了一個start0方法started = true;} finally {try {if (!started) {group.threadStartFailed(this);}} catch (Throwable ignore) {}}}// 這個Thread是一個類,這個方法定義在這里是不是很詭異!看這個關鍵字native;private native void start0();
-
凡是帶了native關鍵字的,說明 java的作用范圍達不到,去調用底層C語言的庫!
-
JNI:Java Native Interface(Java本地方法接口)
-
凡是帶了native關鍵字的方法就會進入本地方法棧;
-
Native Method Stack 本地方法棧
-
本地接口的作用是融合不同的編程語言為Java所用,它的初衷是融合C/C++程序,Java在誕生的時候是C/C++橫行的時候,想要立足,必須有調用C、C++的程序,于是就在內存中專門開辟了一塊區域處理標記為native的代碼,它的具體做法是 在 Native Method Stack 中登記native方法,在 執行引擎( ExecutionEngine )執行的時候加載Native Libraies。
-
目前該方法使用的越來越少了,除非是與硬件有關的應用,比如通過Java程序驅動打印機或者Java系統管理生產設備,在企業級應用中已經比較少見。因為現在的異構領域間通信很發達,比如可以使用Socket通信,也可以使用Web Service等等,不多做介紹!
7.程序計數器(PC, Program Counter Register)
程序計數器:Program Counter Register
- 每個線程都有一個程序計數器,是線程私有的,就是一個指針,指向方法區中的方法字節碼(用來存儲指向像一條指令的地址,也即將要執行的指令代碼),在執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以忽略不計。
PC寄存器(Program Counter Register)是計算機體系結構中的一個重要概念,特別是在處理器的執行過程中。它通常用于指示處理器當前正在執行的指令的位置或下一條要執行的指令的位置。
它是一塊很小的內存空間,幾乎可以忽略不計,也是運行速度最快的存儲區域.
在 JVM 規范中,每個線程都有它自己的程序計數器,是線程私有的,生命周期與線程生命周期保持一致.
程序計數器會存儲當前線程正在執行的 Java 方法的 JVM 指令地址.
它是程序控制流的指示器,分支,循環,跳轉,異常處理,線程恢復等基礎功能都需要依賴這個計數器來完成.
它是唯一一個在java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域
8.棧
8.1 Java 虛擬機棧(Java Virtual Machine Stacks)
早期也叫 Java 棧,描述的是 Java 方法執行的內存模型,每個方法在執行的同時都會創建一個線幀(Stack Frame)用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,每個方法從調用直至執行完成的過程,都對應著一個線幀在虛擬機棧中入棧到出棧的過程。
棧的特點
棧是一種快速有效的分配存儲方式,訪問速度僅次于程序計數器.
JVM 直接對 java 棧的操作只有兩個:調用方法 入棧 .執行結束后 出棧 .
對于棧來說不存在垃圾回收問題.
棧中會出現異常,當線程請求的棧深度大于虛擬機所允許的深度時 , 會出現StackOverflowError.
棧的運行原理
- JVM 對 Java 棧的操作主要是對棧幀的入棧和出棧,遵循先進后出(后進先出)的原則。
- 在一條活動的線程中,只會有一個活動棧,即當前執行的方法的棧幀(棧頂)是有效的,這個棧幀稱為當前棧(Current Frame),對應的方法稱為當前方法(Current Method),定義該方法的類稱為當前類(Current Class)。
- 執行引擎運行的所有字節碼指令只針對當前棧幀進行操作。
- 如果在方法中調用了其他方法,會創建新的棧幀放在棧的頂端,成為新的當前棧幀。
- 不同線程中的棧幀不允許相互引用,即不可能在一個棧中引用另一個線程的棧幀。
- 當前方法調用其他方法后,在方法返回時,當前棧幀會傳回此方法的執行結果給前一個棧幀,然后丟棄當前棧幀,使前一個棧幀重新成為當前棧幀。
- Java 方法有兩種返回方式:正常的函數返回使用 return 指令,拋出異常也會導致棧幀被彈出。
棧的內部結構
- 局部變量表(Local Variables):
- 用于存儲方法參數和方法內部定義的局部變量。
- 對于基本數據類型的變量,直接存儲其值;對于引用類型的變量,存儲指向對象的引用。
- 操作數棧(Operand Stack)或表達式棧:
- 用于表達式求值。在方法執行過程中,實際上是不斷執行語句并進行計算的過程,這些計算過程都借助于操作數棧來完成。
- 動態鏈接(Dynamic Linking)或指向運行時常量池的方法引用:
- 方法執行過程中可能需要使用類中的常量,因此需要一個引用指向運行時常量池。
- 方法返回地址(Return Address)或方法正常/異常退出的定義:
- 當方法執行完畢時,需要返回到之前調用它的地方,因此在棧幀中保存著方法返回地址。
- 其他輔助數據
- 如異常處理信息、synchronized塊的鎖信息等。
8.2 本地方法棧(Native Method Stack)
與虛擬機棧的作用是一樣的,只不過虛擬機棧是服務 Java 方法的,而本地方法棧是為虛擬機調用 Native 方法服務的。
- Java 虛擬機棧用于管理 Java 方法的調用,而本地方法棧則用于管理本地方法的調用。
- 本地方法棧也是線程私有的,每個線程都有自己的本地方法棧。
- 本地方法棧的內存大小可以被實現成固定的或者可動態擴展的,與 Java 虛擬機棧類似。內存溢出的處理方式也相同。
- 如果線程請求分配的棧容量超過本地方法棧允許的最大容量,會拋出 StackOverflowError。
- 本地方法是用 C 語言編寫的,因此需要一種機制將 Java 虛擬機中的 Java 方法與本地方法庫中的本地方法進行連接。
- 具體的做法是在 Native Method Stack 中登記 native 方法,在 Execution Engine 執行時加載本地方法庫,從而實現本地方法的調用。
9.方法區(Method Area)
方法區基本理解
- 方法區概述:
- 方法區是 Java 虛擬機內存的一部分,它是被所有線程共享的內存區域。
- 方法區主要用于存儲加載的類字節碼、類的元數據信息(包括 class/method/field 等)、static final 常量、static 變量、即時編譯器編譯后的代碼等數據。
- 方法區還包含一個重要的子區域,即運行時常量池。
- 方法區與堆的關系:
- 雖然方法區在邏輯上是堆的一部分,但是在 HotSpot JVM 中,方法區也被稱為非堆,目的是為了和堆區分開。
- 在 HotSpot JVM 中,方法區是一塊獨立于 Java 堆的內存空間。
- 方法區的作用:
- 存儲加載的類信息和元數據,包括類的結構信息、方法、字段等。
- 存儲靜態變量,如 static 變量。
- 存儲常量,包括 static final 常量。
- 存儲運行時常量池,即類加載時將字面量和符號引用轉換為直接引用的內存區域。
- 方法區的特點:
- 線程共享:方法區被所有線程共享,因為它存儲的是類的元數據信息,而類是被所有線程共享的。
- 非堆內存:雖然在邏輯上是堆的一部分,但是在 HotSpot JVM 中,方法區被認為是非堆內存,與 Java 堆區分開。
-
當 JVM 啟動時,方法區被創建。
-
它的物理內存空間可以與 Java 堆一樣不連續。
-
方法區的大小可以選擇固定或可擴展,與堆空間類似。
-
方法區的大小決定了系統可以保存多少個類,如果定義了太多類導致方法區溢出,虛擬機會拋出內存溢出錯誤。
-
關閉 JVM 將釋放方法區的內存。
方法區,棧,堆的交互關系
-
方法區用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯后的代碼等數據。方法區是很重要的系統資源,是硬盤和 CPU 的中間橋梁,承載著操作系統和應用程序的實時運行.
-
JVM 內存布局規定了 Java 在運行過程中內存申請,分配,管理的策略,保證了 JVM的高效穩定運行.
-
方法區是被所有線程共享,所有字段和方法字節碼,以及一些特殊方法,如構造函數,接口代碼也在此定義,簡單說,所有定義的方法的信息都保存在該區域,此區域屬于共享區間;
-
靜態變量、常量、類信息(構造方法、接口定義)、運行時的常量池存在方法區中,但是實例變量存在堆內存中,和方法區無關。
-
static ,final ,Class ,常量池~
存儲類的結構信息、靜態變量、常量、方法字節碼等數據
方法區主要包含以下內容:
類信息(Class Information):方法區存儲了加載的類的元數據信息,包括類的名稱、父類的名稱、類的修飾符、方法和字段的描述符等。這些信息對于JVM在運行時動態加載、鏈接和執行類非常重要。
靜態變量(Static Variables):類的靜態變量被存儲在方法區中。這些變量在類加載時被分配內存,并在整個程序執行期間保持不變。靜態變量的生命周期與類的生命周期相同。
常量池(Constant Pool):方法區中包含了常量池,用于存儲類中的字面常量、符號引用等信息。常量池中的內容包括類和接口的全限定名、字段和方法的名稱和描述符、字符串常量等。
方法字節碼(Method Bytecode):類中的方法字節碼被存儲在方法區中。這些字節碼被JVM解釋執行或者編譯成本地機器代碼執行。
運行時常量池(Runtime Constant Pool):與常量池對應的是運行時常量池,它是常量池的一部分,用于存儲編譯時生成的字面常量和符號引用,在類加載后被JVM轉換為運行時數據結構。
過度使用靜態變量、大量的類加載、頻繁的字符串常量等操作都可能導致方法區內存溢出的問題。
10.三種JVM
- Sun公司HotSpot java Hotspot?64-Bit server vw (build 25.181-b13,mixed mode)
- BEA JRockit
- IBM 39 VM
- 我們學習都是:Hotspot
11.堆(Java Heap)
概述:
- JVM 實例只有一個堆內存,堆是 Java 內存管理的核心區域。
- Java 堆區在 JVM 啟動時被創建,其空間大小確定,是 JVM 管理的最大內存塊。
- 堆內存大小可調節,如
-Xms:10m
(堆起始大小)和-Xmx:30m
(堆最大內存大小)。 - 通常將起始值和最大值設置為一致,以減少垃圾回收后重新分配堆內存大小的次數,提高效率。
- 根據《Java 虛擬機規范》,堆可以處于物理上不連續的內存空間,但在邏輯上應視為連續的。
- 所有線程共享 Java 堆,也可劃分線程私有的緩沖區。
- 《Java 虛擬機規范》指出,所有對象實例都應在運行時分配在堆上。
- 方法結束后,堆中的對象不會立即移除,只有在垃圾收集時才會被移除。
- 堆是 GC(Garbage Collection,垃圾收集器)執行垃圾回收的重點區域。
堆內存區域劃分:
Java8 及之后堆內存分為 :新生區(新生代)+老年區(老年代)
新生區分為 Eden(伊甸園)區和 Survivor(幸存者)區
為什么分區(代)?
-
將對象根據存活概率進行分類,對存活時間長的對象,放到固定區,從而減少掃描垃圾時間及 GC 頻率。
-
針對分類進行不同的垃圾回收算法,對算法揚長避短。
對象創建內存分配過程
在 JVM 中,為新對象分配內存是一個復雜而嚴謹的任務。設計者需要考慮內存分配的位置、算法以及與內存回收相關的問題,包括內存碎片的產生。整個過程如下:
- 新對象首先被分配到伊甸園區(Eden),該區域大小有限制。
- 當伊甸園區(Eden)填滿時,如果程序需要創建更多對象,JVM 的垃圾回收器將執行垃圾回收(Minor GC),銷毀伊甸園區(Eden)中不再被引用的對象,并將新對象放入伊甸園區(Eden)。
- 剩余對象從伊甸園區(Eden)移動到幸存者 0 區(to 區)。
- 如果再次觸發垃圾回收,上次存放在幸存者 0 區(from 區)的對象,如果沒有被回收,將被移到幸存者 1 區(to 區),保證每次都有一個空的幸存者區(to 區)。
- 如果再次經歷垃圾回收,存放在幸存者 0 區的對象會重新移到幸存者 0 區,然后繼續移到幸存者 1 區。
- 當對象的 GC 年齡達到默認值(通常為 15 次)或者自定義的閾值時,將會從新生代轉移到老年代,這個閾值可以通過參數
-XX:MaxTenuringThreshold=<N>
設置。
- 在對象頭中,它是由 4 位數據來對 GC 年齡進行保存的,所以最大值為 1111,即為15。所以在對象的 GC 年齡達到 15 時,就會從新生代轉到老年代。
- 在老年區,當內存不足時,會觸發 Major GC,對老年區進行內存清理。
- 如果在執行了 Major GC 后仍然無法保存對象,就會導致 OutOfMemoryError 異常,例如
Java.lang.OutOfMemoryError:Java heap space
。
新生區與老年區配置比例
在 Java 虛擬機中,可以通過調整堆結構的配置來優化內存使用,其中包括新生代與老年代的占比以及伊甸園和幸存者空間的比例。
-
默認情況下,新生代與老年代在堆結構中的占比由參數 -XX:NewRatio 控制,默認值為 2。這表示新生代占 1,老年代占 2,新生代占整個堆的 1/3。如果需要調整,可以設置為其他值,例如 -XX:NewRatio=4,表示新生代占 1,老年代占 4,新生代占整個堆的 1/5。通過調整老年代的大小,可以優化項目中生命周期較長對象的存儲。
-
在 HotSpot 虛擬機中,默認情況下,伊甸園區(Eden)和兩個幸存者空間(Survivor)的比例為 8:1:1。可以通過選項 -XX:SurvivorRatio 進行調整。例如,設置 -XX:SurvivorRatio=8,表示調整幸存者空間的比例,此時新生區的對象默認生命周期超過 15 次,將會被轉移到老年代進行養老。
分代收集思想 Minor GC、Major GC、Full GC
在 JVM 進行垃圾回收(GC)時,并非每次都會同時回收新生代和老年代,通常大部分情況下只會回收新生代。HotSpot VM 實現了兩種主要類型的垃圾回收,即部分收集和整堆收集:
- 部分收集:這種類型并非完整收集整個 Java 堆的垃圾,而是針對特定區域的垃圾回收。部分收集又分為兩種類型:
- 新生代收集(Minor GC / Yong GC):僅回收新生代(包括伊甸園區、幸存者區 S0 和 S1)的垃圾。
- 老年代收集(Major GC / Old GC):僅回收老年代的垃圾。
- 整堆收集(Full GC):這種類型涉及到整個 Java 堆(包括新生代、老年代以及永久代或者元空間)的垃圾回收。
- 整堆收集的出現情況包括:
- 手動調用
System.gc();
方法時。 - 當老年代空間不足時。
- 當方法區空間不足時。
- 在開發期間應盡量避免整堆收集,因為它會引起停頓時間較長的暫停,影響程序的性能。
- 手動調用
堆空間的參數設置
官網地址
| 參數 |描述 |
| :–: | :–: |
| -XX:+PrintFlagsInitial | 查看所有參數的默認初始值 |
| -XX:+PrintFlagsFinal | 查看所有參數的最終值(修改后的值) |
| -Xms | 初始堆空間內存(默認為物理內存的 1/64) |
| -Xmx | 最大堆空間內存(默認為物理內存的 1/4) |
| -Xmn | 設置新生代的大小(初始值及最大值) |
| -XX:NewRatio | 配置新生代與老年代在堆結構的占比 |
| -XX:SurvivorRatio | 設置新生代中 Eden 和 S0/S1 空間比例 |
| -XX:MaxTenuringTreshold| 設置新生代垃圾的最大年齡 |
| XX:+PrintGCDetails | 輸出詳細的 GC 處理日志 |
字符串常量池為什么要調整位置(<JDK7: 永久代;>=JDK7: 堆空間
)
-
提高回收效率,避免永久代溢出
JDK7 及以后的版本中將字符串常量池放到了堆空間中。因為方法區的回收效率很低,在 Full GC 的時候才會執行永久代的垃圾回收,而 Full GC 是老年代的空間不足、方法區不足時才會觸發。
這就導致字符串常量池回收效率不高,而我們開發中會有大量的字符串被創建,回收效率低,導致永久代內存不足。放到堆里,能及時回收內存。
將字符串常量池移到堆空間中,使得它可以享受到堆空間自動調整大小的優勢,避免了永久代內存溢出的問題。
-
更好的 GC 控制
永久代的垃圾收集行為與新生代和老年代不同,這增加了 GC 管理的復雜性。通過將字符串常量池移到堆空間中,可以統一 GC 對整個堆空間的管理,簡化了 GC 管理的邏輯。
12.新生區、養老區
-
新生區是類誕生,成長,消亡的區域,一個類在這里產生,應用,最后被垃圾回收器收集,結束生命。
-
新生區又分為兩部分:伊甸區(Eden Space)和幸存者區(Survivor Space),所有的類都是在伊甸區被new出來的,幸存區有兩個:0區 和 1區,當伊甸園的空間用完時,程序又需要創建對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC)。將伊甸園中的剩余對象移動到幸存0區,若幸存0區也滿了,再對該區進行垃圾回收,然后移動到1區,那如果1區也滿了呢?(這里幸存0區和1區是一個互相交替的過程)再移動到養老區,若養老區也滿了,那么這個時候將產生MajorGC(Full GC),進行養老區的內存清理,若養老區執行了Full GC后發現依然無法進行對象的保存,就會產生OOM異常 “OutOfMemoryError ”。如果出現 java.lang.OutOfMemoryError:java heap space異常,說明Java虛擬機的堆內存不夠,原因如下:
-
1、Java虛擬機的堆內存設置不夠,可以通過參數 -Xms(初始值大小),-Xmx(最大大小)來調整。
-
2、代碼中創建了大量大對象,并且長時間不能被垃圾收集器收集(存在被引用)或者死循環。
-
13.永久區(Perm)
- 永久存儲區是一個常駐內存區域,用于存放JDK自身所攜帶的Class,Interface的元數據,也就是說它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉JVM才會釋放此區域所占用的內存。
- 如果出現 java.lang.OutOfMemoryError:PermGen space,說明是 Java虛擬機對永久代Perm內存設置不夠。一般出現這種情況,都是程序啟動需要加載大量的第三方jar包,
- 例如:在一個Tomcat下部署了太多的應用。或者大量動態反射生成的類不斷被加載,最終導致Perm區被占滿。
注意: 常量池所在區域的變化?為啥? \red{常量池所在區域的變化?為啥?} 常量池所在區域的變化?為啥?
? ? 1 ) 提高回收效率,避免永久代溢出; 2 )更好的 G C 控制 {--1)提高回收效率,避免永久代溢出;2)更好的GC控制} ??1)提高回收效率,避免永久代溢出;2)更好的GC控制
-
JDK1.6之前: 有永久代,常量池1.6在方法區;
-
JDK1.7: 有永久代,但是已經逐步 “去永久代”,常量池1.7在堆;
-
JDK1.8及之后:無永久代,常量池1.8在元空間。
熟悉三區結構后方可學習JVM垃圾回收機制
-
實際而言,方法區(Method Area)和堆一樣,是各個線程共享的內存區域,它用于存儲虛擬機加載的:類信息+普通常量+靜態常量+編譯器編譯后的代碼,雖然JVM規范將方法區描述為堆的一個邏輯部分,但它卻還有一個別名,叫做Non-Heap(非堆),目的就是要和堆分開。
-
對于HotSpot虛擬機,很多開發者習慣將方法區稱之為 “永久代(Parmanent Gen)”,但嚴格本質上說兩者不同,或者說使用永久代實現方法區而已,永久代是方法區(相當于是一個接口interface)的一個實現,Jdk1.7的版本中,已經將原本放在永久代的字符串常量池移走。
-
常量池(Constant Pool)是方法區的一部分,Class文件除了有類的版本,字段,方法,接口描述信息外,還有一項信息就是常量池,這部分內容將在類加載后進入方法區的運行時常量池中存放!
14.堆內存調優
- -Xms:設置初始分配大小,默認為物理內存的 “1/64”。
- -Xmx:最大分配內存,默認為物理內存的 “1/4”。
- -XX:+PrintGCDetails:輸出詳細的GC處理日志。
runtime.totalMemory()
:
- 這個方法返回Java虛擬機當前已經使用的內存量。
- 它表示當前已經分配給Java虛擬機的內存量,包括已使用的和尚未使用的部分。
- 在Java程序運行期間,隨著內存的分配和釋放,這個值可能會不斷變化。
runtime.maxMemory()
:
- 這個方法返回Java虛擬機試圖使用的最大內存量。
- 它表示Java虛擬機能夠從操作系統獲取的最大內存量。
- 當Java程序運行時,Java虛擬機會根據需要動態調整可用內存的大小,但不會超過這個最大內存量。
runtime.freeMemory()
這個方法返回Java虛擬機當前空閑的內存量,即還沒有被分配給程序使用的內存。
它表示Java虛擬機中尚未被分配的、可用于新對象分配的內存量。
在Java程序運行期間,這個值可能會隨著程序的內存分配和釋放而不斷變化。
測試1
代碼測試
默認情況下:分配的總內存是電腦內存的1/4,初始化的內存是電腦的1/64
public class Test {public static void main(String[] args) {Runtime runtime = Runtime.getRuntime();System.out.println("runtime.availableProcessors(): " + runtime.availableProcessors());double maxMemory = (double) runtime.maxMemory();double totalMemory = (double) runtime.totalMemory();double freeMemory = (double) runtime.freeMemory();double usedMemory = totalMemory - freeMemory;System.out.println("runtime.maxMemory(): " + maxMemory + "bytes, " + (maxMemory / 1024) + "KB, " + (maxMemory / 1024 / 1024) + "MB, " + (maxMemory / 1024 / 1024 / 1024) + "GB");System.out.println("runtime.totalMemory(): " + totalMemory + "bytes, " + (totalMemory / 1024) + "KB, " + (totalMemory / 1024 / 1024) + "MB");System.out.println("runtime.freeMemory(): " + freeMemory + "bytes, " + (freeMemory / 1024) + "KB, " + (freeMemory / 1024 / 1024) + "MB");System.out.println("usedMemory: " + usedMemory + "bytes, " + (usedMemory / 1024) + "KB, " + (usedMemory / 1024 / 1024) + "MB");}
}
runtime.availableProcessors(): 16
runtime.maxMemory(): 3.754426368E9bytes, 3666432.0KB, 3580.5MB, 3.49658203125GB
runtime.totalMemory(): 2.53231104E8bytes, 247296.0KB, 241.5MB
runtime.freeMemory(): 2.47940152E8bytes, 242129.0546875KB, 236.45415496826172MB
usedMemory: 5290952.0bytes, 5166.9453125KB, 5.045845031738281MB
JVM參數縮寫的含義–幫助記憶
JVM的參數縮寫很容易混淆,理解每個參數的具體含義可以幫助記憶。VM選項有三種:
- : 標準VM選項,VM規范的選項
-X: 非標準VM選項,不保證所有VM支持
-XX: 高級選項,高級特性,但屬于不穩定的選項對于第二類參數,其語義分別是
-Xms
: 堆的初始化初始化大小,助記:memory startup
-Xmx
: 堆的最大內存數,等同于-XX:MaxHeapSize,助記:memory maximum
-Xmn
: 堆中新生代初始及最大大小,如果需要進一步細化,初始化大小用-XX:NewSize,最大大小用-XX:MaxNewSize,助記:memory nursery/new
-Xss
: 線程棧大小,等同于-XX:ThreadStackSize,助記:stack sizeJVM參數縮寫的含義–幫助記憶_jvm參數 x是什么意義-CSDN博客
- IDEA中進行VM調優參數設置,然后啟動。
發現,默認的情況下分配的內存是總內存的 1/4,而初始化的內存為 1/64 !
- VM options:
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
- VM參數調優:把初始內存,和總內存都調為 1024M,運行,查看結果!
- 來大概計算分析一下!
305664 K ( P S Y o u n g G e n ) + 699392 K ( P a r O l d G e n ) = 1005056 K B = 981.5 M B 305664K(PSYoungGen) + 699392K(ParOldGen) = 1005056KB = 981.5MB 305664K(PSYoungGen)+699392K(ParOldGen)=1005056KB=981.5MB
- 再次證明:元空間并不在虛擬機中,而是使用本地內存。(邏輯上存在,物理上不存在)
測試2
代碼:
package github.JVM.Demo02;import java.util.Random;/** * @author subeiLY * @create 2021-06-08 10:22 */
public class Demo02 {public static void main(String[] args) {String str = "suneiLY";while (true) {str += str + new Random().nextInt(88888888)+ new Random().nextInt(999999999);}}
}
- vm參數:
-Xms8m -Xmx8m -XX:+PrintGCDetails
- 測試,查看結果!
-
這是一個young 區域撐爆的JAVA 內存日志,其中 PSYoungGen 表示 youngGen分區的變化, 1536k 表示 GC 之前的大小, 488k 表示GC 之后的大小。
-
整個Young區域的大小從 1536K 到 672K , young代的總大小為 7680K。
-
user – 總計本次 GC 總線程所占用的總 CPU 時間。
-
sys – OS 調用 or 等待系統時間。
-
real – 應用暫停時間。
-
如果GC 線程是 Serial Garbage Collector 串行搜集器的方式的話(只有一條GC線程,), real time 等于user 和 system 時間之和。
-
通過日志發現Young的區域到最后 GC 之前后都是0,old 區域 無法釋放,最后報堆溢出錯誤。
其他文章鏈接
- 一文讀懂 - 元空間和永久代
- Java方法區、永久代、元空間、常量池詳解
15.GC
1.Dump內存快照
? 在運行java程序的時候,有時候想測試運行時占用內存情況,這時候就需要使用測試工具查看了。在eclipse里面有 Eclipse Memory Analyzer tool(MAT)插件可以測試,而在idea中也有這么一個插件,就是JProfiler,一款性能瓶頸分析工具!
作用:
-
分析Dump文件,快速定位內存泄漏;
-
獲得堆中對象的統計數據
-
獲得對象相互引用的關系
-
采用樹形展現對象間相互引用的情況
安裝JPro?ler
- IDEA插件安裝
- 安裝JPro?ler監控軟件
- 下載地址:ej-technologies - Java APM, Java Profiler, Java Installer Builder
- 下載完雙擊運行,選擇自定義目錄安裝,點擊Next。
- 注意:安裝路徑,建議選擇一個文件名中沒有中文,沒有空格的路徑 ,否則識別不了。然后一直點Next。
- 注冊
// 注冊碼僅供大家參考
L-Larry_Lau@163.com#23874-hrwpdp1sh1wrn#0620
L-Larry_Lau@163.com#36573-fdkscp15axjj6#25257
L-Larry_Lau@163.com#5481-ucjn4a16rvd98#6038
L-Larry_Lau@163.com#99016-hli5ay1ylizjj#27215
L-Larry_Lau@163.com#40775-3wle0g1uin5c1#0674
- 配置IDEA運行環境
- Settings–Tools–JPro?ier–JPro?ier executable選擇JPro?le安裝可執行文件。(如果系統只裝了一個版本, 啟動IDEA時會默認選擇)保存。
- 代碼測試:
package github.JVM.Demo02;import java.util.ArrayList;/** * @author subeiLY * @create 2021-06-08 11:13 */
public class Demo03 {byte[] byteArray = new byte[1*1024*1024]; // 1M = 1024Kpublic static void main(String[] args) {ArrayList<Demo03> list = new ArrayList<>();int count = 0;try {while (true) {list.add(new Demo03()); // 問題所在count = count + 1;}} catch (Error e) {System.out.println("count:" + count);e.printStackTrace();}}
}
- vm參數 :
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
- 尋找文件:
使用 Jpro?ler 工具分析查看
雙擊這個文件默認使用 Jpro?ler 進行 Open大的對象!
大對象
Thread Dump – 線程轉儲
- 從軟件開發的角度上,dump文件就是當程序產生異常時,用來記錄當時的程序狀態信息(例如堆棧的狀態),用于程序開發定位問題。
2.GC四大算法
0.引用計數法
-
每個對象有一個引用計數器,當對象被引用一次則計數器加1,當對象引用失效一次,則計數器減1,對于計數器為0的對象意味著是垃圾對象,可以被GC回收。
-
目前虛擬機基本都是采用可達性算法,從GC Roots 作為起點開始搜索,那么整個連通圖中的對象邊都是活對象,對于GC Roots 無法到達的對象變成了垃圾回收對象,隨時可被GC回收。
引用計數法(Reference Counting)是一種簡單的垃圾回收算法,它基于一個簡單的概念:每個對象都有一個與之相關的引用計數,用于記錄指向該對象的引用數量。當引用計數歸零時,說明沒有任何指針指向該對象,可以將其視為垃圾并進行回收。
這個算法的實現比較直觀和簡單,它的主要優點包括:
實時性: 由于對象的回收與引用計數的增減直接相關,一旦引用計數歸零,對象就會立即被回收,不需要等待垃圾回收器的運行。
部分對象生命周期的確定性: 對于一些循環引用的情況,引用計數法可以及時地回收這些循環引用的對象,而不需要等待整個循環成為不可達。
然而,引用計數法也存在一些缺點:
循環引用問題: 如果兩個或多個對象之間形成了循環引用,即使它們已經不再被程序使用,它們的引用計數也不會歸零,導致這些對象永遠無法被回收,從而造成內存泄漏。
性能開銷: 每次增減引用計數都需要額外的操作,包括增減操作本身以及更新引用計數可能帶來的并發控制開銷。這會導致一定的性能開銷,并且可能影響程序的運行效率。
無法處理循環引用: 由于無法處理循環引用問題,引用計數法通常需要與其他垃圾回收算法(如標記-清除、復制、標記-整理等)結合使用,以解決循環引用導致的內存泄漏問題。
1.復制算法(Copying)
- 年輕代中使用的是Minor GC,采用的就是復制算法(Copying)。
什么是復制算法?
算法的執行過程如下:
對象分配階段: 初始時,所有的新對象都會被分配到 Eden 區。
Minor GC 觸發: 當 Eden 區填滿時,會觸發 Minor GC(也稱為新生代 GC)。
存活對象復制: 在 Minor GC 過程中,GC 首先會檢查所有存活的對象,并將它們復制到 Survivor 區中的一個。如果一個對象經過一次 Minor GC 后仍然存活,它就會被復制到另一個 Survivor 區,而不是被清除。通常來說,每次 Minor GC 后,存活的對象都會被復制到一個新的 Survivor 區。
年齡增加: 經過多次 Minor GC 后,存活的對象會逐漸增加年齡。一般來說,每經過一次 Minor GC,對象的年齡就會增加一歲。當一個對象的年齡達到一定閾值(通常是 15 歲),它就會被晉升到老年代(Old Generation)。
Major GC 觸發: 在老年代空間不足或進行 Full GC(全局垃圾回收)時,會觸發 Major GC(老年代 GC)。這時,會對整個堆內存進行清理和整理。
復制算法的優點包括:
- 解決了內存碎片問題:由于每次回收后存活對象都會被移動到一個新的區域,因此不會產生內存碎片。
- 簡單高效:實現簡單,不需要進行復雜的內存整理操作,同時也可以高效地回收大量的短生命周期對象。
然而,復制算法的缺點也是比較明顯的:
- 雙倍內存消耗:由于需要將存活對象復制到另一個區域,因此需要額外的內存空間。
- 無法處理長生命周期對象:對于長時間存活的對象,需要經過多次 Minor GC 后才會被晉升到老年代,這可能會導致老年代的內存占用過高。
-
Minor GC 會把Eden中的所有活的對象都移到Survivor區域中,如果Survivor區中放不下,那么剩下的活的對象就被移動到Old generation中,也就是說,一旦收集后,Eden就是變成空的了
-
當對象在Eden(包括一個Survivor區域,這里假設是From區域)出生后,在經過一次Minor GC后,如果對象還存活,并且能夠被另外一塊Survivor區域所容納 (上面已經假設為from區域,這里應為to區域,即to區域有足夠的內存空間來存儲Eden 和 From 區域中存活的對象),則使用復制算法將這些仍然還活著的對象復制到另外一塊Survivor區域(即 to 區域)中,然后清理所使用過的Eden 以及Survivor 區域(即form區域),并且將這些對象的年齡設置為1,以后對象在Survivor區,每熬過一次MinorGC,就將這個對象的年齡 + 1,當這個對象的年齡達到某一個值的時候(默認是15歲,通過- XX:MaxTenuringThreshold 設定參數)這些對象就會成為老年代。
-
-XX:MaxTenuringThreshold
任期門檻=>設置對象在新生代中存活的次數
面試題:如何判斷哪個是to區呢?一句話:誰空誰是to
原理解釋:
-
年輕代中的GC,主要是復制算法(Copying)
-
HotSpot JVM 把年輕代分為了三部分:一個 Eden 區 和 2 個Survivor區(from區 和 to區)。默認比例為 8:1:1,一般情況下,新創建的對象都會被分配到Eden區(一些大對象特殊處理),這些對象經過第一次Minor GC后,如果仍然存活,將會被移到Survivor區,對象在Survivor中每熬過一次Minor GC , 年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中,因為年輕代中的對象基本上 都是朝生夕死,所以在年輕代的垃圾回收算法使用的是復制算法!復制算法的思想就是將內存分為兩塊,每次只用其中一塊,當這一塊內存用完,就將還活著的對象復制到另外一塊上面。復制算法不會產 生內存碎片!
- 在GC開始的時候,對象只會在Eden區和名為 “From” 的Survivor區,Survivor區“TO” 是空的,緊接著進行GC,Eden區中所有存活的對象都會被復制到 “To”,而在 “From” 區中,仍存活的對象會根據他們的年齡值來決定去向。
- 年齡達到一定值的對象會被移動到老年代中,沒有達到閾值的對象會被復制到 “To 區域”,經過這次GC后,Eden區和From區已經被清空,這個時候, “From” 和 “To” 會交換他們的角色, 也就是新的 “To” 就是GC前的“From” , 新的 “From” 就是上次GC前的 “To”。
- 不管怎樣,都會保證名為To 的Survicor區域是空的。 Minor GC會一直重復這樣的過程。直到 To 區 被填滿 ,“To” 區被填滿之后,會將所有的對象移動到老年代中。
-
因為Eden區對象一般存活率較低,一般的,使用兩塊10%的內存作為空閑和活動區域,而另外80%的內存,則是用來給新建對象分配內存的。一旦發生GC,將10%的from活動區間與另外80%中存活的Eden 對象轉移到10%的to空閑區域,接下來,將之前的90%的內存,全部釋放,以此類推;
-
好處:沒有內存碎片;壞處:浪費內存空間。
劣勢:
- 復制算法它的缺點也是相當明顯的。
- 1、他浪費了一半的內存,這太要命了。
- 2、如果對象的存活率很高,我們可以極端一點,假設是100%存活,那么我們需要將所有對象都復制一遍,并將所有引用地址重置一遍。復制這一工作所花費的時間,在對象存活率達到一定程度時,將會變的不可忽視,所以從以上描述不難看出。復制算法要想使用,最起碼對象的存活率要非常低才行,而且 最重要的是,我們必須要克服50%的內存浪費。
2.標記清除(Mark-Sweep)
如何選擇Eden區復制到Survivor區的對象?---- 標記清除
標記存活對象: GC 會從根對象開始,通過可達性分析(Reachability Analysis)標記所有在程序執行過程中仍然是活動的對象。這些根對象可能是線程棧中的引用、靜態變量、本地變量表中的引用等。所有與這些根對象直接或間接相連的對象都會被標記為存活對象。
復制到 Survivor 區: 在標記階段結束后,GC 會遍歷 Eden 區中的所有對象,將其中標記為存活的對象復制到 Survivor 區的其中一個。通常情況下,復制后的對象是按照它們在 Eden 區中的順序復制到 Survivor 區的,但具體實現可能有所不同。
清空 Eden 區: 復制完所有存活對象后,Eden 區中所有未被復制的對象都會被認定為垃圾,并且整個 Eden 區將被清空,以便為后續的對象分配提供空間。
年齡增加: 存活在 Survivor 區的對象的年齡會增加。每次經過一次 Minor GC,存活對象會被復制到另一個 Survivor 區,并且它們的年齡會增加。當達到一定的年齡閾值后,對象會被晉升到老年代。
標記清除(Mark-Sweep)
-
回收時,對需要存活的對象進行標記;
-
回收不是綠色的對象。
-
當堆中的有效內存空間被耗盡的時候,就會停止整個程序(也被稱為stop the world),然后進行兩項工作,第一項則是標記,第二項則是清除。
-
標記:從引用根節點開始標記所有被引用的對象,標記的過程其實就是遍歷所有的GC Roots ,然后將所有GC Roots 可達的對象,標記為存活的對象。
-
清除: 遍歷整個堆,把未標記的對象清除。
-
缺點:這個算法需要暫停整個應用,會產生內存碎片。兩次掃描,嚴重浪費時間。
用通俗的話解釋一下 標記/清除算法,就是當程序運行期間,若可以使用的內存被耗盡的時候,GC線程就會被觸發并將程序暫停,隨后將依舊存活的對象標記一遍,最終再將堆中所有沒被標記的對象全部清 除掉,接下來便讓程序恢復運行。
劣勢:
-
首先、它的缺點就是效率比較低(遞歸與全堆對象遍歷),而且在進行GC的時候,需要停止應用 程序,這會導致用戶體驗非常差勁
-
其次、主要的缺點則是這種方式清理出來的空閑內存是不連續的,這點不難理解,我們的死亡對象 都是隨機的出現在內存的各個角落,現在把他們清除之后,內存的布局自然亂七八糟,而為了應付 這一點,JVM就不得不維持一個內存空間的空閑列表,這又是一種開銷。而且在分配數組對象的時 候,尋找連續的內存空間會不太好找。
Q: 可達性分析(Reachability Analysis)具體過程?
A: 可達性分析是垃圾回收算法中用于確定對象是否可訪問(或者說“可達”)的一種技術。它的基本思想是從一組稱為“根”的起始對象開始,遞歸地查找所有通過引用鏈與根對象相連接的對象,并將它們標記為可達的。而無法通過這些引用鏈到達的對象,則被視為不可達的,即可被回收的垃圾對象。
具體的可達性分析過程如下:
- 標記根對象: 可達性分析從一組稱為根的對象開始。這些根對象通常包括:
- 程序的活動線程的棧中引用的對象;
- 靜態變量引用的對象;
- 特定于應用程序的任何其它對象,被認為是程序的入口點。
遞歸遍歷: 從根對象開始,遞歸地遍歷所有與根對象直接或間接相關的對象。這一步驟通常使用深度優先搜索(DFS)或廣度優先搜索(BFS)等算法來實現。
標記可達對象: 在遍歷的過程中,將所有訪問到的對象標記為可達。如果一個對象已經被標記為可達,那么它的所有引用對象也會被繼續遍歷和標記。
清除不可達對象: 一旦所有可達的對象都被標記完畢,剩余的未被標記的對象即為不可達對象,即垃圾對象。這些對象可以被安全地回收,以釋放其所占用的內存空間。
Q: 存活對象定義?
A: 在可達性分析過程中,存活對象指的是通過根對象可達的對象,即那些可以被程序使用到的對象。垃圾對象則是不可達的對象,即那些程序不再使用的對象,可以被垃圾回收器回收的對象。
Q: 引用根節點是啥?
A: 引用根節點是指可達性分析開始時所使用的起始對象集合。這些根節點通常包括程序中活動線程的棧幀中的引用、靜態變量引用的對象、以及其他特定于應用程序的入口點對象。通過從這些根節點出發,可達性分析可以遍歷整個對象圖,并確定哪些對象是可達的,哪些對象是不可達的。
3.標記壓縮/標記整理(Mark-Compact)
- 標記整理說明:老年代一般是由標記清除或者是標記清除與標記整理的混合實現。
什么是標記壓縮?
原理:
-
在整理壓縮階段,不再對標記的對象作回收,而是通過所有存活對象都像一端移動,然后直接清除邊界以外的內存。可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被 清理掉,如此一來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閑列表顯然少了許多開銷。
-
標記、整理算法 不僅可以彌補 標記、清除算法當中,內存區域分散的缺點,也消除了復制算法當中,內存減半的高額代價;
標記壓縮是一種垃圾回收算法,主要用于老年代(Old Generation)的內存區域,旨在解決長生命周期對象的內存管理問題。與標記清除算法不同,標記壓縮算法在回收垃圾對象后會對存活對象進行整理,以減少內存碎片化。
該算法一般包括以下步驟:
標記存活對象: 與標記清除算法相似,標記壓縮算法首先通過可達性分析標記所有存活的對象。
壓縮存活對象: 在標記階段結束后,存活對象通常會在堆內存中是不連續的,這可能導致內存碎片的產生。為了解決這個問題,標記壓縮算法會將所有存活對象向堆內存的一端移動,使它們成為連續的塊。這個過程被稱為壓縮(Compaction)。
更新引用: 在移動存活對象的過程中,需要更新所有指向這些對象的引用,以確保它們仍然指向正確的內存地址。
清除未標記的對象: 在壓縮存活對象后,所有未被標記的對象被認為是垃圾,并且可以被安全地回收。
標記壓縮算法的優點包括:
- 減少內存碎片化:通過將存活對象整理成連續的塊,可以降低內存碎片的產生,提高內存利用率。
- 改善內存分配性能:連續的內存塊有利于快速、高效地分配內存空間。
然而,標記壓縮算法也有一些缺點:
- 需要額外的移動操作:將存活對象整理成連續的塊需要額外的內存移動操作,可能會增加垃圾回收的時間成本。
- 不適用于所有場景:對于內存分配頻繁、存活對象較多的情況,壓縮操作可能會帶來較大的性能開銷。
盡管標記壓縮算法在某些情況下效果顯著,但它通常用于老年代的垃圾回收,而在新生代通常采用復制算法。
4.標記清除壓縮(Mark-Sweep-Compact)
- 先標記清除幾次,再壓縮。
3.總結
-
內存效率:復制算法 > 標記清除算法 > 標記壓縮算法 (時間復雜度);
-
內存整齊度:復制算法 = 標記壓縮算法 < 標記清除算法;
-
內存利用率:標記壓縮算法 = 標記清除算法 > 復制算法;
? 可以看出,效率上來說,復制算法是當之無愧的老大,但是卻浪費了太多內存,而為了盡量兼顧上面所 提到的三個指標,標記壓縮算法相對來說更平滑一些 , 但是效率上依然不盡如人意,它比復制算法多了一個標記的階段,又比標記清除多了一個整理內存的過程。
難道就沒有一種最優算法嗎?
答案: 無,沒有最好的算法,只有最合適的算法 。 -----------> 分代收集算法
年輕代:(Young Gen)
- 年輕代特點是區域相對老年代較小,對象存活低。
- 這種情況復制算法的回收整理,速度是最快的。復制算法的效率只和當前存活對象大小有關,因而很適 用于年輕代的回收。而復制算法內存利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解。
老年代:(Tenure Gen)
- 老年代的特點是區域較大,對象存活率高!
- 這種情況,存在大量存活率高的對象,復制算法明顯變得不合適。一般是由標記清除或者是標記清除與標記整理的混合實現。Mark階段的開銷與存活對象的數量成正比,這點來說,對于老年代,標記清除或 者標記整理有一些不符,但可以通過多核多線程利用,對并發,并行的形式提標記效率。Sweep階段的 開銷與所管理里區域的大小相關,但Sweep “就地處決” 的 特點,回收的過程沒有對象的移動。使其相對其他有對象移動步驟的回收算法,仍然是是效率最好的,但是需要解決內存碎片的問題。
16.JMM(Java Memory Model)
- 什么是JMM?
- JMM:(Java Memory Model的縮寫)
- 他干嘛的?官方,其他人的博客,對應的視頻!
-
作用:緩存一致性協議,用于定義數據讀寫的規則(遵守,找到這個規則)。
-
JMM定義了線程工作內存和主內存之間的抽象關系∶線程之間的共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory)。
-
- 解決共享對象可見性這個問題:volilate
- 它該如何學習?
-
JMM:抽象的概念,理論。
-
JMM對這八種指令的使用,制定了如下規則:(java內存模型JMM理解整理 - 阿姆斯特朗回旋炮 - 博客園)
- 不允許read和load、store和write操作之一單獨出現。即使用了read必須load,使用了store必須write。
- 不允許線程丟棄他最近的assign操作,即工作變量的數據改變了之后,必須告知主存。
- 不允許一個線程將沒有assign的數據從工作內存同步回主內存。
- 一個新的變量必須在主內存中誕生,不允許工作內存直接使用一個未被初始化的變量。就是懟變量實施use、store操作之前,必須經過assign和load操作。
- 一個變量同一時間只有一個線程能對其進行lock。多次lock后,必須執行相同次數的unlock才能解鎖。
- 如果對一個變量進行lock操作,會清空所有工作內存中此變量的值,在執行引擎使用這個變量前,必須重新load或assign操作初始化變量的值。
- 如果一個變量沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他線程鎖住的變量。
- 對一個變量進行unlock操作之前,必須把此變量同步回主內存。
JMM對這八種操作規則和對volatile的一些特殊規則就能確定哪里操作是線程安全,哪些操作是線程不安全的了。但是這些規則實在復雜,很難在實踐中直接分析。所以一般我們也不會通過上述規則進行分析。更多的時候,使用java的happen-before規則來進行分析。
17. 執行引擎
Java 虛擬機(JVM)執行引擎是 Java 虛擬機的核心組件之一,負責將 Java 字節碼轉換為機器碼并執行程序。執行引擎通常包含解釋器和即時編譯器兩種執行方式。
-
解釋器(Interpreter): 解釋器逐條解釋執行 Java 字節碼,將字節碼翻譯成對應平臺的機器指令,然后由處理器執行。解釋器的優點是簡單、易于實現,適用于快速啟動和簡單測試。然而,解釋執行通常速度較慢,因為它需要動態地將每條字節碼翻譯為機器碼。
-
即時編譯器(Just-In-Time Compiler,JIT): 即時編譯器將字節碼編譯成本地機器碼,以提高執行速度。JIT 編譯器可以選擇性地將頻繁執行的熱點代碼編譯成機器碼,而不是每次都解釋執行。這種方式可以顯著提高程序的性能,尤其是對于密集計算型和性能敏感的應用程序。
在實際中,JVM 的執行引擎通常會結合解釋器和即時編譯器兩種執行方式,稱為混合模式執行。在程序啟動時,解釋器可以快速地啟動并執行字節碼,同時即時編譯器會監視程序的執行情況,并且根據一定的觸發條件對熱點代碼進行即時編譯,從而提高程序的整體性能。
另外,現代的 JVM 還可以使用 Ahead-Of-Time(AOT)編譯器,預先將整個應用程序編譯成本地機器碼,以減少啟動時間和減少運行時的開銷。這種方式適用于一些對啟動時間和運行時性能要求較高的場景,例如移動設備、嵌入式系統等。
綜上所述,JVM 執行引擎通過解釋器和即時編譯器的組合,以及可能的 AOT 編譯器,實現了 Java 程序的高效執行,并且在性能和啟動時間之間做出了權衡。
- 執行引擎除了包括解釋器和即時編譯器外,還包括垃圾回收器(Garbage Collector,GC),它是 Java 虛擬機(JVM)中的另一個核心組件。
參考
Java方法區、永久代、元空間、常量池詳解_java永久地址-CSDN博客
狂神說筆記——JVM入門07 - subeiLY - 博客園
java內存模型JMM理解整理 - 阿姆斯特朗回旋炮 - 博客園
JVM(Java虛擬機)-史上最全、最詳細JVM筆記-CSDN博客
什么是Java內存模型 - 簡書
深入理解JVM-內存模型(jmm)和GC - 簡書