二、Java內存區域
1、Java內存結構

內存結構
- 程序計數器
當前線程所執行字節碼的行號指示器。若當前方法是native的,那么程序計數器的值就是undefined。
線程私有,Java內存區域中唯一一塊不會發生OOM或StackOverflow的區域。
- 虛擬機棧
就是常說的Java棧,存放棧幀,棧幀里存放局部變量表等信息,方法執行到結束對應着一個棧幀的入棧到出棧。
線程私有,會發生StackOverflow。
- 本地方法棧
與虛擬機棧的作用是一樣的,只不過虛擬機棧是服務 Java 方法的,而本地方法棧是為虛擬機調用 Native 方法服務的。
線程私有,會發生StackOverflow。
- 堆
Java 虛擬機中內存最大的一塊,幾乎所有的對象實例都在這里分配內存。
是被所有線程共享的,會發生OOM。
- 方法區
也稱非堆,用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯后的代碼等數據。
是被所有線程共享的,會發生OOM。
- 運行時常量
是方法區的一部分,存常量(比如static final修飾的,比如String 一個字符串)和符號引用。
是被所有線程共享的,會發生OOM。
2、對象創建時堆內存分配算法
- 指針碰撞
前提要求堆內存的絕對工整的。
所有用過的內存放一邊,沒用過的放另一邊,中間放一個分界點的指示器,當有對象新生時就已經知道大小了,指示器只需要像沒用過的內存那邊移動與對象等大小的內存區域即可。

指針碰撞
- 空閑列表
假設堆內存並不工整,那么空閑列表最合適。
JVM維護一個列表 ,記錄哪些內存塊是可用的,當對象創建時從列表中找到一塊足夠大的空間划分給新生對象,並將這塊內存標記為已用內存。

空閑列表
3、對象在內存中的存儲布局
分為三部分:
- 對象頭
包含兩部分:自身運行時數據和類型指針。
自身運行時數據包含:hashcode、gc分代年齡、鎖狀態標識、線程持有的鎖、偏向線程ID、偏向時間戳等
對象指針就是對象指向它的類元數據的指針,虛擬機通過這個指針來確定對象是哪個類的實例
- 實例數據
用來存儲對象真正的有效信息(包括父類繼承下來的和自己定義的)
- 對齊填充
JVM要求對象起始地址必須是8字節的整數倍(8字節對齊),所以不夠8字節就由這部分來補充。
4、對象怎么定位
如下兩種,具體用哪種有JVM來選擇,hotspot虛擬機采取的直接指針方式來定位對象。
- 直接指針
棧上的引用直接指向堆中的對象。好處就是速度快。沒額外開銷。
- 句柄
Java堆中會單獨划分出一塊內存空間作為句柄池,這么一來棧上的引用存儲的就是句柄地址,而不是真實對象地址,而句柄中包含了對象的實例數據等信息。好處就是即使對象在堆中的位置發生移動,棧上的引用也無需變化。因為中間有個句柄。
5、判斷對象是否能被回收的算法
- 引用計數法
給對象添加一個引用計數器,每當有一個地方引用他的時候該計數器的值就+1,當引用失效的時候該計數器的值就-1;當計數器的值為0的時候,jvm判定此對象為垃圾對象。存在內存泄漏的bug,比如循環引用的時候,所以jvm虛擬機采取的是可達性分析法。
- 可達性分析法
有一些根節點GC Roots作為對象起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連的時候,則證明此對象為垃圾對象。
補充:哪些可作為GC Roots?
- 虛擬機棧中的引用的對象
- 方法區中的類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(native方法)引用的對象
6、如何判斷對象是否能被回收
-
該對象沒有與GC Roots相連
-
該對象沒有重寫finalize()方法或finalize()已經被執行過則直接回收(第一次標記)、否則將對象加入到F-Queue隊列中(優先級很低的隊列)在這里finalize()方法被執行,之后進行第二次標記,如果對象仍然應該被GC則GC,否則移除隊列。(在finalize方法中,對象很可能和其他 GC Roots中的某一個對象建立了關聯,那就自救了,就不會被GC掉了,finalize方法只會被調用一次,且不推薦使用finalize方法)
7、Java堆內存組成部分

堆組成部分
堆大小 = 新生代 + 老年代。如果是Java8則沒有Permanent Generation,Java8將此區域換成了Metaspace。
其中新生代(Young) 被分為 Eden和S0(from)和S1(to)。
默認情況下Edem : from : to = 8 : 1 : 1,此比例可以通過 –XX:SurvivorRatio 來設定
8、什么時候拋出StackOverflowError
方法運行的時候棧的深度超過了虛擬機容許的最大深度的時候,所以不推薦遞歸的原因之一也是因為這個,效率低,死歸的話很容易就StackOverflowError了。
9、Java中會存在內存泄漏嗎,請簡單描述。
雖然Java會自動GC,但是使用不當的話還是存在內存泄漏的,比如ThreadLocal忘記remove的情況。(ThreadLocal篇幅過長,不適合放到這里,懂者自懂,不懂Google)
10、棧幀是什么?包含哪些東西
棧幀中存放的是局部變量、操作數棧、動態鏈接、方法出口等信息,棧幀中的局部變量表存放基本類型+對象引用+returnAddress,局部變量所需的內存空間在編譯期間就完成分配了,因為基本類型和對象引用等都能確定占用多少slot,在運行期間也是無法改變這個大小的。
11、簡述一個方法的執行流程
方法的執行到結束其實就是棧幀的入棧到出棧的過程,方法的局部變量會存到棧幀中的局部變量表里,遞歸的話會一直壓棧壓棧,執行完后進行出棧,所以效率較低,因為一直在壓棧,棧是有深度的。
12、方法區會被回收嗎
方法區回收價值很低,主要回收廢棄的常量和無用的類。
如何判斷無用的類:
-
該類所有實例都被回收(Java堆中沒有該類的對象)
-
加載該類的ClassLoader已經被回收
-
該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方利用反射訪問該類
13、一個對象包含多少個字節
會占用16個字節。比如
Object obj = new Object();
因為obj引用占用棧的4個字節,new出來的對象占用堆中的8個字節,4+8=12,但是對象要求都是8的倍數,所以對象的字節對齊(Padding)部分會補齊4個字節,也就是占用16個 字節。
再比如:
public class NewObj { int count; boolean flag; Object obj; } NewObj obj = new NewObj();
這個對象大小為:空對象8字節+int類型4字節+boolean類型1字節+對象的引用4字節=17字節,需要8的倍數,所以字節對齊需要補充7個字節,也就是這段程序占用24字節。
14、為什么把堆棧分成兩個
-
棧代表了處理邏輯,堆代表了存儲數據,分開后邏輯更清晰,面向對象模塊化思想。
棧是線程私有,堆是線程共享區,這樣分開也節省了空間,比如多個棧中的地址指向同一塊堆內存中的對象。
-
棧是運行時的需要,比如方法執行到結束,棧只能向上增長,因此會限制住棧存儲內容的能力,而堆中的對象是可以根據需要動態增長的。
15、棧的起始點是哪
main函數,也是程序的起始點。
16、為什么基本類型不放在堆里
因為基本類型占用的空間一般都是1-8個字節(所需空間很少),而且因為是基本類型,所以不會出現動態增長的情況(長度是固定的),所以存到棧上是比較合適的。反而存到可動態增長的堆上意義不大。
17、Java參數傳遞是值傳遞還是引用傳遞
值傳遞。
基本類型作為參數被傳遞時肯定是值傳遞;引用類型作為參數被傳遞時也是值傳遞,只不過“值”為對應的引用。假設方法參數是個對象引用,當進入被調用方法的時候,被傳遞的這個引用的值會被程序解釋到堆中的對象,這個時候才對應到真正的對象,若此時進行修改,修改的是引用對應的對象,而不是引用本身,也就是說修改的是堆中的數據,而不是棧中的引用。
18、為什么不推薦遞歸
因為遞歸一直在入棧入棧,短時間無法出棧,導致棧的壓力會很大,棧也有深度的,容易爆掉,所以效率低下。
19、為什么參數大於2個要放到對象里
因為除了double和long類型占用局部變量表2個slot外,其他類型都占用1個slot大小,如果參數太多的話會導致這個棧幀變大,因為slot大,放個對象的引用上去的話只會占用1個slot,增加堆的壓力減少棧的壓力,堆自帶GC,所以這點壓力可以忽略。
20、常見筆試題
問題:輸出結果是什么?
答案:aaa、aaa、abc
原因:其實也是值傳遞還是引用傳遞的問題。具體核心原因:main函數的str引用和zcd在棧上,而其對應的值在方法區或堆上。test1、test2、test3的參數也在棧上,這個空間和main上的不是同一塊,是不同的棧幀。所以你修改test方法的數據對於main函數其實是無感知的。但是對象的引用的話修改的是堆內存中的對象屬性值,所以有感知,那為什么test2輸出的是aaa而不是abc呢?因為test2把堆中的對象都給換了,重新生成一個全新對象,對main上的引用來講是看不到的,具體如下三幅圖:
(1)aaa

題1
(2)aaa

題2
(3)abc

題3
public class TestChuandi { public static void main(String[] args) { String str = "aaa"; test1(str); // aaa System.out.println(str); Zhichuandi zcd = new Zhichuandi(); zcd.setName("aaa"); // aaa test2(zcd); System.out.println(zcd.getName()); // abc test3(zcd); System.out.println(zcd.getName()); } private static void test1(String s) { s = "abc"; } private static void test2(Zhichuandi zcd) { zcd = new Zhichuandi(); zcd.setName("abc"); } private static void test3(Zhichuandi zcd) { zcd.setName("abc"); } } class Zhichuandi { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
END
推薦好文
強大,10k+點贊的 SpringBoot 后台管理系統竟然出了詳細教程!
