部分內容來源:JavaGuide+二哥Java
圖解JVM內存結構
內存管理快速復習
棧幀:局部變量表,動態鏈接(符號引用轉為真實引用),操作數棧(存儲中間結算結果),方法返回地址
運行時常量池:常量池表,符號引用,字面量
對象創建過程:類加載檢查(類沒有加載就進行類加載)+分配內存+初始化零值+設置對象頭+執行對象初始化方法
類加載過程:
- 加載:通過類的全限定名獲取該類的二進制字節流,存到類常量池,內存中生成Class類對象
- 連接:
-
- 驗證:驗證Class二進制字節流合規(例如驗證魔數和版本號)
- 準備:為類對象分配內存
- 解析:符號引用轉為直接引用
- 初始化:執行初始化方法
內存分配:指針碰撞(CAS重試)+空閑列表
內存分配失敗:CAS配上重試機制+TLAB為每個線程預先在Eden區分配一塊內存
對象:對象頭(運行時數據+類型指針,GC年齡,Hash碼)+實例數據
對象訪問定位:使用句柄(間接訪問)+直接指針訪問
JVM堆內存分區:新生代(Eden+S1+S2)+老年代
對象什么時候會進入老年代:長期存活的對象,大對象,動態年齡判斷,分配擔保機制
逃逸分析:在棧內為對象分配內存
Stop The World :停止所有用戶線程
Oop Map:記錄了對象內部所有引用字段(指針)的位置
安全點:可以暫停所有線程執行特定操作的位置
常量池包括:
類常量池
運行時常量池
字符串常量池
JDK1.6:常量池在永久代
JDK1.7:運行時常量池+類常量池在永久代,字符串常量池在堆
JDK1.8:運行時常量池+類常量池在元空間,字符串常量池在堆
引用類型有哪些?有什么區別?
強引用指的就是代碼中普遍存在的賦值方式,比如 A a = new A () 這種。強引用關聯的對象,永遠不會被 GC 回收
軟引用可以用 SoftReference 來描述,指的是那些有用但是不是必須要的對象
系統在發生內存溢出前會對這類引用的對象進行回收
弱引用可以用 WeakReference 來描述,他的強度比軟引用更低一點
弱引用的對象下一次 GC 的時候一定會被回收,而不管內存是否足夠
虛引用也被稱作幻影引用,是最弱的引用關系,可以用 PhantomReference 來描述,他必須和 ReferenceQueue 一起使用,同樣的當發生 GC 的時候,虛引用也會被回收
可以用虛引用來管理堆外內存
說一下弱引用?舉例子在哪里可以引用?
Java 中的弱引用是一種引用類型,它不會阻止一個對象被垃圾回收
在 Java 中,弱引用是通過 Java.lang.ref.WeakReference 類實現的
弱引用的一個主要用途是創建非強制性的對象引用,這些引用可以在內存壓力大時被垃圾回收器清理,從而避免內存泄露
弱引用的使用場景:
- 緩存系統:弱引用常用于實現緩存,特別是當希望緩存項能夠在內存壓力下自動釋放時。如果緩存的大小不受控制,可能會導致內存溢出。使用弱引用來維護緩存,可以讓 JVM 在需要更多內存時自動清理這些緩存對象。
- 對象池:在對象池中,弱引用可以用來管理那些暫時不使用的對象。當對象不再被強引用時,它們可以被垃圾回收,釋放內存。
- 避免內存泄露:當一個對象不應該被長期引用時,使用弱引用可以防止該對象被意外地保留,從而避免潛在的內存泄露
說一下你對內存泄露和內存溢出的了解
什么是內存泄露:
內存泄漏是指程序在運行過程中不再使用的對象仍然被引用,而無法被垃圾收集器回收,從而導致可用內存逐漸減少
雖然在 Java 中,垃圾回收機制會自動回收不再使用的對象,但如果有對象仍被不再使用的引用持有,垃圾收集器無法回收這些內存,最終可能導致程序的內存使用不斷增加
內存泄露常見原因:
- 靜態集合:使用靜態數據結構(如 HashMap 或 ArrayList)存儲對象,且未清理。
- 事件監聽:未取消對事件源的監聽,導致對象持續被引用。
- 線程沒被回收:未停止的線程可能持有對象引用,無法被回收。
內存溢出:
內存溢出是指 Java 虛擬機(JVM)在申請內存時,無法找到足夠的內存,最終引發 OutOfMemoryError 。這通常發生在堆內存不足以存放新創建的對象時
內存溢出常見原因:
- 大量對象創建:程序中不斷創建大量對象,超出 JVM 堆的限制。
- 持久引用:大型數據結構(如緩存、集合等)長時間持有對象引用,導致內存累積。
- 遞歸調用:深度遞歸導致棧溢出
JVM的內存泄露有幾種溢出情況?
堆內存溢出:當出現 Java.lang.OutOfMemoryError:Java heap space 異常時,就是堆內存溢出了。原因是代碼中可能存在大對象分配,或者發生了內存泄露,導致在多次 GC 之后,還是無法找到一塊足夠大的內存容納當前對象
棧溢出:如果我們寫一段程序不斷的進行遞歸調用,而且沒有退出條件,就會導致不斷地進行壓棧。類似這種情況,JVM 實際會拋出 StackOverFlowError;當然,如果 JVM 試圖去擴展棧空間的時候失敗,則會拋出 OutOfMemoryError
元空間溢出:元空間的溢出,系統會拋出 Java.lang.OutOfMemoryError: Metaspace。出現這個異常的問題的原因是系統的代碼非常多或引用的第三方包非常多或者通過動態代碼生成類加載等方法,導致元空間的內存占用很大
直接內存內存溢出:在使用 ByteBuffer 中的 allocateDirect () 的時候會用到,很多 JavaNIO (像 netty) 的框架中被封裝為其他的方法,出現該問題時會拋出 Java.lang.OutOfMemoryError: Direct buffer memory 異常
棧中存的是指針還是對象?
在 JVM 內存模型中,棧(Stack)主要用于管理線程的局部變量和方法調用的上下文
而堆(Heap)則是用于存儲所有類的實例和數組
當我們在棧中討論 “存儲” 時,實際上指的是存儲基本類型的數據(如 int, double 等)和對象的引用,而不是對象本身。
這里的關鍵點是,棧中存儲的不是對象,而是對象的引用
也就是說,當你在方法中聲明一個對象,比如 MyObject obj = new MyObject ();,
這里的 obj 實際上是一個存儲在棧上的引用,指向堆中實際的對象實例
這個引用是一個固定大小的數據(例如在 64 位系統上是 8 字節),它指向堆中分配給對象的內存區域
說一下程序計數器的作用?為什么程序計數器是私有的?
Java 程序是支持多線程一起運行的,多個線程一起運行的時候 cpu 會有一個調動器組件給它們分配時間片,比如說會給線程 1 分給一個時間片,它在時間片內如果它的代碼沒有執行完,它就會把線程 1 的狀態執行一個暫存,切換到線程 2 去,執行線程 2 的代碼,等線程 2 的代碼執行到了一定程度,線程 2 的時間片用完了,再切換回來,再繼續執行線程 1 剩余部分的代碼
我們考慮一下,如果在線程切換的過程中,下一條指令執行到哪里了,是不是還是會用到我們的程序計數器啊。
沒個線程都有自己的程序計數器,因為它們各自執行的代碼的指令地址是不一樣的呀,所以每個線程都應該有自己的程序計數器
說一下方法區中方法的執行過程
當程序中通過對象或類直接調用某個方法時,主要包括以下幾個步驟:
解析方法調用:JVM 會根據方法的符號引用找到實際的方法地址(如果之前沒有解析過的話)
棧幀創建:在調用一個方法前,JVM 會在當前線程的 Java 虛擬機棧中為該方法分配一個新的棧幀,用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息
執行方法:執行方法內的字節碼指令,涉及的操作可能包括局部變量的讀寫、操作數棧的操作、跳轉控制、對象創建、方法調用等。
返回處理:方法執行完畢后,可能會返回一個結果給調用者,并清理當前棧幀,恢復調用者的執行環境
JVM內存中的棧和堆有什么區別?
用途
棧主要用于存儲局部變量、方法調用的參數、方法返回地址以及一些臨時數據。每當一個方法被調用,一個棧幀(stack frame)就會在棧中創建,用于存儲該方法的信息,當方法執行完畢,棧幀也會被移除
堆用于存儲對象的實例(包括類的實例和數組)。當你使用 new 關鍵字創建一個對象時,對象的實例就會在堆上分配空間
生命周期
棧中的數據具有確定的生命周期,當一個方法調用結束時,其對應的棧幀就會被銷毀,棧中存儲的局部變量也會隨之消失
堆中的對象生命周期不確定,對象會在垃圾回收機制(Garbage Collection, GC)檢測到對象不再被引用時才被回收
存取速度
棧的存取速度通常比堆快,因為棧遵循先進后出(LIFO, Last In First Out)的原則,操作簡單快速
堆的存取速度相對較慢,因為對象在堆上的分配和回收需要更多的時間,而且垃圾回收機制的運行也會影響性能
存儲空間
棧的空間相對較小,且固定,由操作系統管理
當棧溢出時,通常是因為遞歸過深或局部變量過大
堆的空間較大,動態擴展,由 JVM 管理
堆溢出通常是由于創建了太多的大對象或未能及時回收不再使用的對象
可見性
棧中的數據對線程是私有的,每個線程有自己的棧空間。堆中的數據對線程是共享的,所有線程都可以訪問堆上的對象
static修飾的類型
未被static
修飾的基本類型:基本類型的變量(局部變量)存放在棧中,而不是堆中。例如int num = 10;
,變量num
是在方法執行時在棧中開辟空間存儲的。基本類型的成員變量存放在堆中(當所屬對象在堆中時) 。
被static
修飾的基本類型:被static
修飾的基本類型(靜態變量)存放在方法區(在 JDK 8 及之后,方法區的實現是元空間),而不是棧中。靜態變量屬于類,在類加載時就會分配空間并初始化,存儲在方法區供類的所有對象共享。
包裝類型:包裝類型屬于對象類型,其對象實例確實幾乎都存在堆中,但包裝類型的對象在某些場景下會有緩存機制。例如Integer
在-128
到127
之間的值會被緩存,當創建這個范圍內的Integer
對象時,不會在堆中重新創建,而是直接引用緩存中的對象
此外,部分包裝類(如Integer
、Short
、Byte
、Character
、Long
)存在對象緩存機制。以Integer
為例,在創建-128
到127
之間的Integer
對象時,不會在堆中重新創建,而是直接引用方法區中緩存的對象;超出這個范圍才會在堆中創建新對象
線程的內部有什么?
程序計數器
本機方法棧
Java虛擬機棧
說一下JVM棧的內部組成
JVM棧的內部組成
Java虛擬機棧是線程私有的
它的生命周期和線程相同
除了Native方法是調用本地方法棧實現,其他的所有方法的調用都是通過Java虛擬機棧來實現的
Java虛擬機棧的內部是由棧幀組成
方法調用的數據需要通過棧進行傳遞,每一次方法調用都會有一個對應的棧幀被壓入棧中,每一個方法調用結束后,都會有一個棧幀被彈出
Java虛擬機棧的內部是由一個又一個的棧幀組成
棧幀內部:局部變量表、操作數棧、動態鏈接、方法返回地址
?
說一下棧幀的內部
局部變量表
存放了數據類型,對象引用
操作數棧
主要作為方法調用的中轉站使用,用于存放方法執行過程中產生的中間計算結果
另外,計算過程中產生的臨時變量也會放在操作數棧中
動態鏈接
場景:主要服務一個方法需要調用其他方法的場景
Class 文件的常量池里保存有大量的符號引用比如方法引用的符號引用。
當一個方法要調用其他方法,需要將常量池中指向方法的符號引用轉化為其在內存地址中的直接引用。動態鏈接的作用就是為了將符號引用轉換為調用方法的直接引用
方法返回地址
就是我們方法結束后的返回地址
說一下棧幀內部有啥?
局部變量表
操作數棧
動態鏈接
方法返回地址
說一下JVM棧會出現的問題
tackOverFlowError錯誤:棧幀過多爆了
函數循環調用過多
我們這個線程遞歸調用的時候,我們會往棧里面壓入棧幀,如果壓入的棧幀過多,就會爆出
Java方法的兩種返回方式
一:Returen正常返回
二:拋出異常
OutOfMemoryError:內存空間不夠爆了
虛擬機動態擴展棧時,無法申請到足夠的內存空間
什么是本地方法棧
為本地方法服務
(也就是和我們的操作系統有關,我們的操作系統的方法)
說一下JVM的內存區域
JVM 內存區域最粗略的劃分可以分為 堆 和 棧 ,
當然,按照虛擬機規范,可以劃分為以下?個區域:
JVM 內存分為線程私有區和線程共享區,
線程共享區:方法區和堆
線程隔離的數據區: 虛擬機棧 、本地方法棧 和 程序計數器
1)程序計數器
程序計數器(Program Counter Register)也被稱為 PC 寄存器,是?塊較?的內存空間。
它可以看作是當前線程所執?的字節碼的?號指示器。
2)Java 虛擬機棧
Java 虛擬機棧(Java Virtual Machine Stack)也是線程私有的,它的?命周期與線程相同。
Java 虛擬機棧描述的是 Java ?法執?的線程內存模型:?法執?時,JVM 會同步創建?個棧幀,?來存儲局部變量表、操作數棧、動態連接等。
3)本地方法棧
本地?法棧(Native Method Stacks)與虛擬機棧所發揮的作?是?常相似的,其區別只是虛擬機棧為虛擬機執?
Java ?法(也就是字節碼)服務,?本地?法棧則是為虛擬機使?到的本地(Native)?法服務。
Java 虛擬機規范允許本地?法棧被實現成固定??的或者是根據計算動態擴展和收縮的。
4)Java 堆
對于 Java 應?程序來說,Java 堆(Java Heap)是虛擬機所管理的內存中最?的?塊。Java 堆是被所有線程共享
的?塊內存區域,在虛擬機啟動時創建。此內存區域的唯??的就是存放對象實例,Java ?“幾乎”所有的對象實例
都在這?分配內存。Java 堆是垃圾收集器管理的內存區域,因此?些資料中它也被稱作“GC 堆”(Garbage Collected Heap,)。從回
收內存的?度看,由于現代垃圾收集器?部分都是基于分代收集理論設計的,所以 Java 堆中經常會出現 新?代 、?年代 、 Eden空間 、 From Survivor空間 、 To Survivor空間 等名詞,需要注意的是這種劃分只是根據垃圾回收機制來進?的劃分,不是 Java 虛擬機規范本身制定的。
5)方法區
?法區是?較特別的?塊區域,和堆類似,它也是各個線程共享的內存區域,?于存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯后的代碼緩存等數據。
它特別在 Java 虛擬機規范對它的約束?常寬松,所以?法區的具體實現歷經了許多變遷,例如 jdk1.7 之前使?永久代作為?法區的實現
JVM的堆是用來干嘛的
Java 虛擬機所管理的內存中最大的一塊
Java 堆是所有線程共享的一塊內存區域,在虛擬機啟動時創建
此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這里分配內存
什么是逃逸分析
jdk1.7之后,已經默認開啟逃逸分析
也就是某些方法中的對象引用沒有被返回或者未被外面使用,那么就可以直接在棧上分配內存
也就是我們不用在堆給這個對象分配內存,我們在棧上給這個對象分內存就可以了