Markdown自動生成的目錄在掘金失效, 能夠在頁面右邊使用掘金自動生成的目錄html
介紹
- 初學Java虛擬機幾天, 被方法區, 永久代這些混雜的概念搞混了. 我以爲學習這部分知識應該把官方定義的虛擬機運行時數據區域和虛擬機內存結構分開敘述, 要否則容易誤導.
- 本文先介紹官方文檔規定的運行時數據區域, 而後以JDK1.8的HotSpot虛擬機爲例, 介紹虛擬機的內存結構.
官方文檔規定的運行時數據區域
- 官方文檔中規定的運行時數據區一共就幾塊: PC計數器, 虛擬機棧, 本地方法棧, 堆區, 方法區, 運行時常量池. 這裏的官方規定是說, 若是你要作一個Java虛擬機的話, 必需要包含這幾個區域, 可是這幾個區域在你的虛擬機中是用哪塊內存實現的, 這由虛擬機制做者決定.
程序計數器
- The pc Register, 程序計數器. 若是瞭解過計算機系統, 對這個名詞應該不陌生了, 它指向下一條指令的地址, 程序靠它跑起來.
- Java虛擬機支持多線程, 每條線程都有本身的程序計數器.
- 若是當前線程正在執行一個Java方法, 它的計數器記錄的是正在執行的Java虛擬機指令的地址. 若是執行的是本地方法(好比系統的C語言函數), 計數器中的值爲空(Undefined).
- 正由於程序計數器記錄的是指令地址, 因此它佔用的空間較少, Java虛擬機規範中並無規定這塊內存有OutOfMemoryError(內存溢出)的狀況.
Java虛擬機棧
- Java Virtual Machine Stacks, Java虛擬機棧.
- Java虛擬機棧是線程私有的, 生命週期與線程相同. 虛擬機棧存放棧幀, 棧幀用於存儲局部變量表, 部分結果值, 方法的初始化參數和返回信息, 方法的執行經過棧幀的壓棧和出棧實現.
本地方法棧
- 本地方法棧和上面的虛擬機棧是類似的, 從名字也看出, 虛擬機方法棧是用來執行Java代碼的, 而本地方法棧則是用來執行本地系統代碼的, 好比C代碼.
- 也由於規範中沒有規定本地方法棧執行的代碼, 若是想執行Java代碼也是能夠的, 咱們能夠看到Oracle官方的虛擬機HotSpot虛擬機把Java虛擬機棧和本地方法棧合二爲一, 這麼作避免了要爲不一樣的語言設計棧, 提升了虛擬機的性能.
虛擬機棧和本地方法棧溢出
- 那麼當出現錯誤信息後, 咱們在什麼錯誤信息下能夠去排查是否虛擬機棧和本地方法棧這兩塊內存出錯呢? 這裏以HotSpot虛擬機爲例講解(HotSpot把兩塊棧結構合在一塊兒實現了), 在JDK1.8的虛擬機規範中對這兩塊棧空間可能出現的錯誤給出了相同的描述.
- 一: 若是一條線程所須要的內存大於虛擬機所分配給它的內存, 將拋出
StackOverflowError
異常.
- 二: 若是棧內存能夠擴展並嘗試擴展時可用的內存不足, 或者建立新線程併爲其分配棧內存時可能的內存不足, 會拋出
OutOfMemoryError
- 下面先演示第一個
StackOverflowError
異常
public class StackErrorTest1 {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
StackErrorTest1 set1 = new StackErrorTest1();
try{
set1.stackLeak();
}catch (Throwable e){
System.out.println("stack length:" + set1.stackLength);
e.printStackTrace();
}
}
}
stack length:1000
java.lang.StackOverflowError
at jvm.StackErrorTest1.stackLeak(StackErrorTest1.java:7)
at jvm.StackErrorTest1.stackLeak(StackErrorTest1.java:8)
...
複製代碼
- 因此當遇到
StackOverflowError
時能夠考慮是不是是虛擬機的棧容量過小, 好比這裏的無窮遞歸, 棧空間不夠用. 固然生產環境中確定不會寫無窮遞歸, 這時能夠經過設置-Xss參數調整單條線程的棧內存大小.
- 上面描述的棧內存能夠擴展並嘗試擴展時可用的內存不足致使出現
OutOfMemoryError
的狀況暫時沒有好的演示代碼, 在周志明的《深刻理解Java虛擬機》中提到"定義了大量本地變量,增大方法幀中本地變量表的長度, 結果仍拋出StackOverflowError
". 不知道是否是沒有觸發虛擬機動態擴充棧空間, 因此仍然斷定是棧所需的空間超出了虛擬機規定的大小. 總結來講不管是棧幀太大仍是棧空間過小都會拋出StackOverflowError
, 能夠考慮調整-Xss參數.
- 上面還提到當建立新線程並分配新的棧空間時, 若是可用的內存不夠, 會拋出
OutOfMemoryError
異常, 下面是這種狀況的代碼演示.
public class StackErrorTest2 {
private void keepRunning(){
while(true){
}
}
public void stackLeakByThread(){
while(true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
keepRunning();
}
});
thread.start();
}
}
public static void main(String[] args){
StackErrorTest2 set2 = new StackErrorTest2();
set2.stackLeakByThread();
}
}
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
複製代碼
- 這段代碼也來自深刻理解jvm, 書中也說明跑這段代碼要當心, 由於Java的線程是映射到內核線程上的, 果不其然個人機子一跑就死機了.
- 問什麼會出現這樣的錯誤? 32位Windows系統分配給一個進程的內存最大爲2GB(32位能尋址4GB地址空間, 除去內核的空間剩2GB, 64位則大得多). 這2GB減去最大堆容量, 減去方法區的容量, 剩下的就是虛擬機棧和本地方法區棧的內存空間了. (補充: PC計數器佔的空間很小, 運行時常量池在方法區中, HotSpot中虛擬機棧和本地方法棧一塊兒實現, 因此能分紅這麼三大塊內存).
- 瞭解了三大塊內存區後(HotSpot下), 解決思路也出來了: 1. 減少最大堆內存, 騰出更多位置給棧空間. 2. 若是程序的線程數量不能夠減小, 那麼就看看是否能夠減小每條線程的棧內存.
- 固然用一臺配置高的機器, 該用64位的Java虛擬機也是一種方法.
Java堆
- Java堆是隨着虛擬機的啓動而建立的, 用於存放對象實例, 全部的對象實例和數組都在堆內存分配, 它被全部線程共享. Java堆是Java虛擬機管理的內存中最大的一塊, 也是垃圾回收器管理的主要區域. 從內存回收的角度看, Java堆內存還能夠被繼續劃分, 而且和具體的虛擬機實現有關.
- 當前主流的虛擬機都是支持堆內存動態擴展的, 就是說當堆內存的大不夠時, 它會擴充容量; 當不要太多的空間時, 它能本身進行壓縮. 咱們能夠人爲地經過-Xmx和-Xms設定堆內存的最大值和最小值(初始大小). 若是咱們把-Xmx和-Xms設置爲相同的值, 就等同於設定了固定大小的Java堆. (這是gc調優的一種手段)
- 若堆內存分配內存時發現已經沒有更過可用空間時, 會拋出
OutOfMemoryError
.
演示堆內存溢出
- 堆內存是存放對象實例的地方, 這個應該比較好理解, 直接上代碼
public class HeapErrorTest {
static class Object{
}
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
while(true){
list.add(new Object());
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
複製代碼
- 由結果能夠看到當堆內存溢出後除了有
java.lang.OutOfMemoryError
外, 還會提示Java heap space
. 在這個例子中, 咱們明確地知道了是因爲堆內存不夠大而形成的溢出. 然而在生產環境中, 當系統報出堆內存溢出時, 咱們首先要搞清楚是由於內存泄漏致使的內存溢出, 仍是純粹的內存溢出.
- 內存溢出指的是分配內存的時候, 沒有足夠的空間供其使用. 內存泄漏指的是在分配一塊內存使用完後沒有釋放, 在Java中對應的場景是沒有被垃圾回收器回收. 一點點的內存泄漏用戶可能感覺不到, 可是當泄漏的內存聚沙成塔的時候, 會耗盡內存, 致使內存溢出.
- 有一些經常使用的分析內存溢出的手段和工具, 這裏就不詳細敘述了, 能夠參考書籍或網上的資料. 當咱們判斷是內存泄漏致使的溢出後, 能夠根據工具定位出現泄漏的代碼位置; 若是不存在泄漏只是單純的溢出的話, 能夠經過設置虛擬參數調整堆內存大小(前提是機器的配置可以支持相應的內存大小), 或者看看代碼中是否存在一些生命週期很長的對象實例, 看看可否做出修改.
方法區
- 方法區用於存儲以被虛擬機加載的類信息, 常量, 靜態變量, 即時編譯器編譯後的代碼數據等, 它是全部線程共享的. 虛擬機規範中說方法區在邏輯上是堆的一部分, 可是它的別名叫"non-Heap"也就是非堆的意思, 代表它和堆內存是兩塊獨立的內存. 至於說在邏輯上是堆區的一部分, 是由於在物理實現上, 方法區的內存地址包含於堆中, 因此說是邏輯上的一部分, 實際用的時候是徹底不一樣的部分. 這麼設計多是由於便於垃圾收集器統一管理吧.
運行時常量池
- 運行時常量池的內存由方法區分配, 也就是說它屬於方法區的一部分. 它用於存儲Class文件中的類版本, 字段, 方法, 接口和常量池等, 也用於存放編譯期生成的各類字面量和符號引用.
- 運行時常量池區別於Class文件常量池的一個重要特徵是具有動態特性. 也就說並不是在Class文件中定義的常量才能進入運行時常量池, 在程序運行的過程當中也有可能將新的常量放入池中.
演示方法區溢出
- 演示方法區溢出和堆區的思路同樣, 不斷往方法堆中加入東西使其溢出. 只是方法區中保存的是類信息, 咱們經過不斷動態生成類演示
- 本代碼示例來源於深刻理解jvm, 可是其中的參數須要改變, 該書的最新版本是基於JDK1.7的, JDK1.7中方法區是在永久代中實現的, 而JDK1.8中已經沒有永久代了, 方法區中Metaspace元數據區中, 經過設置
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
來指定方法區的大小
/**
* VM Args: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*/
public class MethodAreaTest {
static class Object{
}
public static void main(String[] args) {
int count = 0;
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public java.lang.Object intercept(java.lang.Object o, Method method, java.lang.Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(objects, objects);
}
});
enhancer.create();
System.out.println(++count);
}
}
}
運行結果:
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
... 8 more
複製代碼
HotSpot虛擬機的內存模型
- 在介紹完Java虛擬機運行時數據區域後, 接着以HotSpot虛擬機爲例介紹虛擬機內存模型.
- 首先有一個重要的概念要搞清楚, 要否則容易犯暈.
- 在前面介紹Java運行時數據區域時咱們談到PC計數器, 虛擬機棧, 本地方法棧這3塊內存都是線程私有的, 它們的隨線程的建立而分配, 隨線程的結束而釋放, 也就是說Java虛擬機是明確知道這三塊內存是何時該被回收的, 只要線程沒執行完就不能回收, 不然線程跑不起來.
- 而咱們在談論虛擬機的內存模型時, 一般要和垃圾回收結合在一塊兒討論. 既然上面的三塊內存回收的時間已定, 暫時不須要過多考慮, 虛擬機分配內存時給它們留有空間就行.
- 但另外的兩塊內存堆內存和方法區則不同, 它們是全部線程共享的, 在這裏面內存的分配和釋放具備不肯定性. 好比說在多態的狀況下, 一個接口對應的實現類不一樣, 具體的實現方法也不一樣, 虛擬機只有在程序運行的過程當中才知道要建立哪些對象, 這部份內存的分配和釋放都是動態的, 垃圾收集器關注的也是這部分的內容.
- 因此說咱們後續描述的虛擬機內存模型是創建在Java堆內存和方法區上的.
JVM實現的堆內存和方法區
- 正如上述所說, 當談論JVM的內存結構時, 討論的重點就由整個運行時數據區域轉爲對堆內存和方法區的討論, 由於這兩部分是垃圾回收的重點區域(若是二者要比較的話, 重點收集區域是堆區).
- 而HotSpot虛擬機的內存結構由三大部分組成: 新生代, 老年代和元數據區(JDK1.7及之前叫老年代). 其中新生代和老年代是虛擬機規範中Java堆內存的實現, 元數據區是規範中方法區的實現. 在講述爲何這麼定義以前, 先明確這個關係對於理解概念是很重要的, 下面有幅圖幫助理解.
- 這裏有個小失誤, 題目中明明講的是JDK1.8, 爲何還提永久代呢? 因爲永久代存在的時間長, 永久代的說法通過這麼多年可能已經深刻人心, 因此先並列講, 要知道永久代和元數據區是有本質的差異的, 這留到後面講, 先認清概念.
- 但願圖片加描述可以幫助你當即規範定義的數據區域和JVM內存結構之間的關係. 下面將對HotSpot虛擬機的內存模型作進一步分析.
新生代和老年代.
- Java堆內存被實現爲新生代和老年代, 是爲了更方便地進行垃圾回收. 咱們知道對象是存儲在堆內存中的, 從字面上理解新生代就是新建立的對象區域, 老年代就是使用屢次生命週期長的對象區域. 新生代對象生命週期一般較短, 不少用完便可以釋放; 老年代對象的生命週期較長, 可能在整個程序的運行過程當中都是有用的.
- 因爲新對象和老對象具備不一樣的性質, 爲對這兩種對象設計的垃圾回收算法也不一樣, 因此要把它們分開.
新生代中的內存劃分
- 新生代的內存被分爲一個Eden區和兩個Survivor區. 爲了講述爲何要這麼分, 需簡單引入垃圾回收算法.
- 首先最基礎, 最簡單的垃圾回收算法叫標記-清除算法. 算法流程和算法名徹底一致: 首先標記出哪些是能夠回收的對象, 標記完後把對象清除. 若是按照這麼個流程, 新生代應該就是一塊簡單的內存就行, 現實結論告訴咱們這個算法是能夠優化的.
- 標記清除算法的不足在於一塊完整的內存在通過標記-清除算法後有些內存會被釋放掉, 這時會形成內存空間不連續, 可能不可以存放一些較大的對象.
- 標記-清除算法的升級版是複製算法, 它在標記-清除的思路上做出了些改變. 首先將內存分爲兩塊, 當建立新對象分配內存的時候只用兩塊中的一塊A. 當進行垃圾回收的時候只對有對象的一塊A內存使用標記-清除算法進行回收, 回收後剩餘的存活對象從內存A移到另外一塊空的內存B中, 這樣A內存從新變爲空內存, 繼續重複此分配回收過程. 這個算法彷佛更好一些, 可是也只是兩塊內存, 說明還不是現實中的最優解.
- 考慮新的算法, 把內存分配成均等兩塊, 等同於可以使用的內存變爲原來的二分之一了, 根據IBM專門部分研究新生代中百分之98%的對象都是"朝生夕死"的, 也就是說在進行垃圾回收時98%的對象都被回收掉, 只有2%會從A內存移動到B內存. 這麼一想咱們把兩塊內存割爲相同的兩塊是否是有點太虧了?
- 下面揭曉答案: HotSpot虛擬機回收虛擬機時使用的是複製算法, 可是它分紅三塊內存, 一個佔80%內存的Eden區(堆內存), 兩個分別佔10%的Survivor區. 具體操做是這樣的: 程序運行時, 用Eden區和一個Survivor區A存放新建立的對象. 當發生垃圾回收時, 把存活下來的對象(不多)複製到另外一塊Survivor區B中, 使得Eden區和Survivor區A從新爲空, 而後繼續重複這個分配回收的過程.
- 因此說詳細點的Jvm的內存模型是下面這樣的
由JDK1.7及之前的永久代到JDK1.8的元數據區
- 搞定完堆區在JVM內存模型中的實現, 下面談論方法區的實現.
- 在JDK1.7及之前, JVM使用永久代來實現方法區. 這裏用"實現"二字是通過斟酌的, 由於永久代並不等同於方法區. 從名字也能夠看出它和新生代, 老年代是一脈相承的, 邏輯上是一體的, 命名爲永久代是由於這部份內存不多幾乎不被回收. 這一不多幾乎不被回收的特性正好對應方法區中存儲的類信息, 常量, 靜態變量等元素. 因此說用永久代來實現方法區.
- 可是用永久代來實現方法區並非最優解, 好比容易出現內存溢出問題(具體分析去除永久代, 改用Metaspace的緣由能夠參考文章末尾所列出的資料). 在JDK1.8中JVM改成使用元數據區來實現方法區.
- 元數據區和永久代有着本質的區別, 永久代屬於虛擬機內存的一部分, 也就是說當在操做系統中啓動虛擬機進程時爲它分配了一塊內存, 而虛擬機爲永久代分配內存時用的是它本身分配得的內存.
- 而元數據區Metaspace是直接在本地內存(Native Memory)中申請的, 這樣元數據區的大小(方法區大小)只會受本地內存大小限制, 和虛擬機進程所分得內存無關.
- 因此最後JVM內存模型圖的終極版應該是這樣子
參考資料