JDK1.8-Java虛擬機運行時數(shù)據(jù)區(qū)域和HotSpot虛擬機的內(nèi)存模型

官方文檔規(guī)定的運行時數(shù)據(jù)區(qū)域

官方文檔中規(guī)定的運行時數(shù)據(jù)區(qū)一共就幾塊: PC計數(shù)器, 虛擬機棧, 本地方法棧, 堆區(qū), 方法區(qū), 運行時常量池. 這里的官方規(guī)定是說, 如果你要做一個Java虛擬機的話, 必須要包含這幾個區(qū)域, 但是這幾個區(qū)域在你的虛擬機中是用哪塊內(nèi)存實現(xiàn)的, 這由虛擬機制作者決定.

程序計數(shù)器

The pc Register, 程序計數(shù)器. 如果了解過計算機系統(tǒng), 對這個名詞應該不陌生了, 它指向下一條指令的地址, 程序靠它跑起來.

Java虛擬機支持多線程, 每條線程都有自己的程序計數(shù)器.

如果當前線程正在執(zhí)行一個Java方法, 它的計數(shù)器記錄的是正在執(zhí)行的Java虛擬機指令的地址. 如果執(zhí)行的是本地方法(比如系統(tǒng)的C語言函數(shù)), 計數(shù)器中的值為空(Undefined).

正因為程序計數(shù)器記錄的是指令地址, 所以它占用的空間較少, Java虛擬機規(guī)范中并沒有規(guī)定這塊內(nèi)存有OutOfMemoryError(內(nèi)存溢出)的情況.

Java虛擬機棧

Java Virtual Machine Stacks, Java虛擬機棧.

Java虛擬機棧是線程私有的, 生命周期與線程相同. 虛擬機棧存放棧幀, 棧幀用于存儲局部變量表, 部分結(jié)果值, 方法的初始化參數(shù)和返回信息, 方法的執(zhí)行通過棧幀的壓棧和出棧實現(xiàn).

本地方法棧

本地方法棧和上面的虛擬機棧是相似的, 從名字也看出, 虛擬機方法棧是用來執(zhí)行Java代碼的, 而本地方法棧則是用來執(zhí)行本地系統(tǒng)代碼的, 比如C代碼.

也因為規(guī)范中沒有規(guī)定本地方法棧執(zhí)行的代碼, 如果想執(zhí)行Java代碼也是可以的, 我們可以看到Oracle官方的虛擬機HotSpot虛擬機把Java虛擬機棧和本地方法棧合二為一, 這么做避免了要為不同的語言設(shè)計棧, 提高了虛擬機的性能.

虛擬機棧和本地方法棧溢出

如果想學習Java工程化、高性能及分布式、深入淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java高級交流:787707172,群里有阿里大牛直播講解技術(shù),以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費分享給大家。

那么當出現(xiàn)錯誤信息后, 我們在什么錯誤信息下可以去排查是否虛擬機棧和本地方法棧這兩塊內(nèi)存出錯呢? 這里以HotSpot虛擬機為例講解(HotSpot把兩塊棧結(jié)構(gòu)合在一起實現(xiàn)了), 在JDK1.8的虛擬機規(guī)范中對這兩塊??臻g可能出現(xiàn)的錯誤給出了相同的描述.

一: 如果一條線程所需要的內(nèi)存大于虛擬機所分配給它的內(nèi)存, 將拋出StackOverflowError異常.

二: 如果棧內(nèi)存可以擴展并嘗試擴展時可用的內(nèi)存不足, 或者創(chuàng)建新線程并為其分配棧內(nèi)存時可能的內(nèi)存不足, 會拋出OutOfMemoryError

下面先演示第一個StackOverflowError異常

//設(shè)置虛擬機參數(shù) -Xss128k, 設(shè)置單個線程的??臻g大小為128kpublicclassStackErrorTest1{privateintstackLength =1;publicvoidstackLeak(){ stackLength++; stackLeak(); }publicstaticvoidmain(String[] args){ StackErrorTest1 set1 =newStackErrorTest1();try{ set1.stackLeak(); }catch(Throwable e){ System.out.println("stack length:"+ set1.stackLength); e.printStackTrace(); } }}//輸出異常信息stacklength:1000java.lang.StackOverflowError at jvm.StackErrorTest1.stackLeak(StackErrorTest1.java:7) at jvm.StackErrorTest1.stackLeak(StackErrorTest1.java:8) ...

所以當遇到StackOverflowError時可以考慮是否是是虛擬機的棧容量太小, 比如這里的無窮遞歸, ??臻g不夠用. 當然生產(chǎn)環(huán)境中肯定不會寫無窮遞歸, 這時可以通過設(shè)置-Xss參數(shù)調(diào)整單條線程的棧內(nèi)存大小.

上面描述的棧內(nèi)存可以擴展并嘗試擴展時可用的內(nèi)存不足導致出現(xiàn)OutOfMemoryError的情況暫時沒有好的演示代碼, 在周志明的《深入理解Java虛擬機》中提到"定義了大量本地變量,增大方法幀中本地變量表的長度, 結(jié)果仍拋出StackOverflowError". 不知道是不是沒有觸發(fā)虛擬機動態(tài)擴充??臻g, 所以仍然判定是棧所需的空間超出了虛擬機規(guī)定的大小. 總結(jié)來說無論是棧幀太大還是??臻g太小都會拋出StackOverflowError, 可以考慮調(diào)整-Xss參數(shù).

上面還提到當創(chuàng)建新線程并分配新的棧空間時, 如果可用的內(nèi)存不夠, 會拋出OutOfMemoryError異常, 下面是這種情況的代碼演示.

publicclassStackErrorTest2{privatevoidkeepRunning(){while(true){ } }publicvoidstackLeakByThread(){while(true){ Thread thread =newThread(newRunnable() {@Overridepublicvoidrun(){ keepRunning(); } }); thread.start(); } }publicstaticvoidmain(String[] args){ StackErrorTest2 set2 =newStackErrorTest2(); set2.stackLeakByThread(); }}//運行結(jié)果, 來源《深入理解Java虛擬機》Exception in thread"main"java.lang.OutOfMemoryError: unable to createnewnativethread

這段代碼也來自深入理解jvm, 書中也說明跑這段代碼要小心, 因為Java的線程是映射到內(nèi)核線程上的, 果不其然我的機子一跑就死機了.

問什么會出現(xiàn)這樣的錯誤? 32位Windows系統(tǒng)分配給一個進程的內(nèi)存最大為2GB(32位能尋址4GB地址空間, 除去內(nèi)核的空間剩2GB, 64位則大得多). 這2GB減去最大堆容量, 減去方法區(qū)的容量, 剩下的就是虛擬機棧和本地方法區(qū)棧的內(nèi)存空間了. (補充: PC計數(shù)器占的空間很小, 運行時常量池在方法區(qū)中, HotSpot中虛擬機棧和本地方法棧一起實現(xiàn), 所以能分成這么三大塊內(nèi)存).

了解了三大塊內(nèi)存區(qū)后(HotSpot下), 解決思路也出來了: 1. 減小最大堆內(nèi)存, 騰出更多位置給棧空間. 2. 如果程序的線程數(shù)量不可以減少, 那么就看看是否可以減少每條線程的棧內(nèi)存.

當然用一臺配置高的機器, 該用64位的Java虛擬機也是一種方法.

Java堆

Java堆是隨著虛擬機的啟動而創(chuàng)建的, 用于存放對象實例, 所有的對象實例和數(shù)組都在堆內(nèi)存分配, 它被所有線程共享. Java堆是Java虛擬機管理的內(nèi)存中最大的一塊, 也是垃圾回收器管理的主要區(qū)域. 從內(nèi)存回收的角度看, Java堆內(nèi)存還可以被繼續(xù)劃分, 并且和具體的虛擬機實現(xiàn)有關(guān).

當前主流的虛擬機都是支持堆內(nèi)存動態(tài)擴展的, 就是說當堆內(nèi)存的大不夠時, 它會擴充容量; 當不要太多的空間時, 它能自己進行壓縮. 我們可以人為地通過-Xmx和-Xms設(shè)定堆內(nèi)存的最大值和最小值(初始大小). 如果我們把-Xmx和-Xms設(shè)置為相同的值, 就等同于設(shè)定了固定大小的Java堆. (這是gc調(diào)優(yōu)的一種手段)

若堆內(nèi)存分配內(nèi)存時發(fā)現(xiàn)已經(jīng)沒有更過可用空間時, 會拋出OutOfMemoryError.

演示堆內(nèi)存溢出

堆內(nèi)存是存放對象實例的地方, 這個應該比較好理解, 直接上代碼

/**

* VM Args: -Xms20m -Xmx20m

*/publicclassHeapErrorTest{staticclassObject{}publicstaticvoidmain(String[] args){ Listlist=newArrayList<>();while(true){list.add(newObject()); } }}//運行結(jié)果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)

由結(jié)果可以看到當堆內(nèi)存溢出后除了有java.lang.OutOfMemoryError外, 還會提示Java heap space. 在這個例子中, 我們明確地知道了是由于堆內(nèi)存不夠大而造成的溢出. 然而在生產(chǎn)環(huán)境中, 當系統(tǒng)報出堆內(nèi)存溢出時, 我們首先要搞清楚是因為內(nèi)存泄漏導致的內(nèi)存溢出, 還是純粹的內(nèi)存溢出.

內(nèi)存溢出指的是分配內(nèi)存的時候, 沒有足夠的空間供其使用. 內(nèi)存泄漏指的是在分配一塊內(nèi)存使用完后沒有釋放, 在Java中對應的場景是沒有被垃圾回收器回收. 一點點的內(nèi)存泄漏用戶可能感受不到, 但是當泄漏的內(nèi)存積少成多的時候, 會耗盡內(nèi)存, 導致內(nèi)存溢出.

有一些常用的分析內(nèi)存溢出的手段和工具, 這里就不詳細敘述了, 可以參考書籍或網(wǎng)上的資料. 當我們判斷是內(nèi)存泄漏導致的溢出后, 可以根據(jù)工具定位出現(xiàn)泄漏的代碼位置; 如果不存在泄漏只是單純的溢出的話, 可以通過設(shè)置虛擬參數(shù)調(diào)整堆內(nèi)存大小(前提是機器的配置能夠支持相應的內(nèi)存大小), 或者看看代碼中是否存在一些生命周期很長的對象實例, 看看能否作出修改.

方法區(qū)

方法區(qū)用于存儲以被虛擬機加載的類信息, 常量, 靜態(tài)變量, 即時編譯器編譯后的代碼數(shù)據(jù)等, 它是所有線程共享的. 虛擬機規(guī)范中說方法區(qū)在邏輯上是堆的一部分, 但是它的別名叫"non-Heap"也就是非堆的意思, 表明它和堆內(nèi)存是兩塊獨立的內(nèi)存. 至于說在邏輯上是堆區(qū)的一部分, 是因為在物理實現(xiàn)上, 方法區(qū)的內(nèi)存地址包含于堆中, 所以說是邏輯上的一部分, 實際用的時候是完全不同的部分. 這么設(shè)計可能是因為便于垃圾收集器統(tǒng)一管理吧.

運行時常量池

運行時常量池的內(nèi)存由方法區(qū)分配, 也就是說它屬于方法區(qū)的一部分. 它用于存儲Class文件中的類版本, 字段, 方法, 接口和常量池等, 也用于存放編譯期生成的各種字面量和符號引用.

運行時常量池區(qū)別于Class文件常量池的一個重要特征是具備動態(tài)特性. 也就說并非在Class文件中定義的常量才能進入運行時常量池, 在程序運行的過程中也有可能將新的常量放入池中.

演示方法區(qū)溢出

演示方法區(qū)溢出和堆區(qū)的思路一樣, 不斷往方法堆中加入東西使其溢出. 只是方法區(qū)中保存的是類信息, 我們通過不斷動態(tài)生成類演示

本代碼示例來源于深入理解jvm, 但是其中的參數(shù)需要改變, 該書的最新版本是基于JDK1.7的, JDK1.7中方法區(qū)是在永久代中實現(xiàn)的, 而JDK1.8中已經(jīng)沒有永久代了, 方法區(qū)中Metaspace元數(shù)據(jù)區(qū)中, 通過設(shè)置-XX:MetaspaceSize和-XX:MaxMetaspaceSize來指定方法區(qū)的大小

/**

* VM Args: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m

*/publicclassMethodAreaTest{staticclassObject{}publicstaticvoidmain(String[] args) {intcount =0;while(true) { Enhancer enhancer =newEnhancer(); enhancer.setSuperclass(Object.class); enhancer.setUseCache(false); enhancer.setCallback(newMethodInterceptor() {@Overridepublicjava.lang.Object intercept(java.lang.Object o, Method method, java.lang.Object[] objects, MethodProxy methodProxy)throwsThrowable {returnmethodProxy.invokeSuper(objects, objects); } }); enhancer.create(); System.out.println(++count); } }}運行結(jié)果:Causedby:java.lang.OutOfMemoryError:Metaspace at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ...8more

HotSpot虛擬機的內(nèi)存模型

在介紹完Java虛擬機運行時數(shù)據(jù)區(qū)域后, 接著以HotSpot虛擬機為例介紹虛擬機內(nèi)存模型.

首先有一個重要的概念要搞清楚, 要不然容易犯暈.

在前面介紹Java運行時數(shù)據(jù)區(qū)域時我們談到PC計數(shù)器, 虛擬機棧, 本地方法棧這3塊內(nèi)存都是線程私有的, 它們的隨線程的創(chuàng)建而分配, 隨線程的結(jié)束而釋放, 也就是說Java虛擬機是明確知道這三塊內(nèi)存是什么時候該被回收的, 只要線程沒執(zhí)行完就不能回收, 否則線程跑不起來.

而我們在談論虛擬機的內(nèi)存模型時, 通常要和垃圾回收結(jié)合在一起討論. 既然上面的三塊內(nèi)存回收的時間已定, 暫時不需要過多考慮, 虛擬機分配內(nèi)存時給它們留有空間就行.

但另外的兩塊內(nèi)存堆內(nèi)存和方法區(qū)則不一樣, 它們是所有線程共享的, 在這里面內(nèi)存的分配和釋放具有不確定性. 比如說在多態(tài)的情況下, 一個接口對應的實現(xiàn)類不同, 具體的實現(xiàn)方法也不同, 虛擬機只有在程序運行的過程中才知道要創(chuàng)建哪些對象, 這部分內(nèi)存的分配和釋放都是動態(tài)的, 垃圾收集器關(guān)注的也是這部分的內(nèi)容.

所以說我們后續(xù)描述的虛擬機內(nèi)存模型是建立在Java堆內(nèi)存和方法區(qū)上的.

JVM實現(xiàn)的堆內(nèi)存和方法區(qū)

正如上述所說, 當談論JVM的內(nèi)存結(jié)構(gòu)時, 討論的重點就由整個運行時數(shù)據(jù)區(qū)域轉(zhuǎn)為對堆內(nèi)存和方法區(qū)的討論, 因為這兩部分是垃圾回收的重點區(qū)域(如果兩者要比較的話, 重點收集區(qū)域是堆區(qū)).

而HotSpot虛擬機的內(nèi)存結(jié)構(gòu)由三大部分組成: 新生代, 老年代和元數(shù)據(jù)區(qū)(JDK1.7及以前叫老年代). 其中新生代和老年代是虛擬機規(guī)范中Java堆內(nèi)存的實現(xiàn), 元數(shù)據(jù)區(qū)是規(guī)范中方法區(qū)的實現(xiàn). 在講述為什么這么定義之前, 先明確這個關(guān)系對于理解概念是很重要的, 下面有幅圖幫助理解.

這里有個小失誤, 題目中明明講的是JDK1.8, 為什么還提永久代呢? 由于永久代存在的時間長, 永久代的說法經(jīng)過這么多年可能已經(jīng)深入人心, 所以先并列講, 要知道永久代和元數(shù)據(jù)區(qū)是有本質(zhì)的差別的, 這留到后面講, 先認清概念.

希望圖片加描述能夠幫助你立即規(guī)范定義的數(shù)據(jù)區(qū)域和JVM內(nèi)存結(jié)構(gòu)之間的關(guān)系. 下面將對HotSpot虛擬機的內(nèi)存模型做進一步分析.

新生代和老年代.

Java堆內(nèi)存被實現(xiàn)為新生代和老年代, 是為了更方便地進行垃圾回收. 我們知道對象是存儲在堆內(nèi)存中的, 從字面上理解新生代就是新創(chuàng)建的對象區(qū)域, 老年代就是使用多次生命周期長的對象區(qū)域. 新生代對象生命周期通常較短, 很多用完即可以釋放; 老年代對象的生命周期較長, 可能在整個程序的運行過程中都是有用的.

由于新對象和老對象具有不同的性質(zhì), 為對這兩種對象設(shè)計的垃圾回收算法也不同, 所以要把它們分開.

新生代中的內(nèi)存劃分

新生代的內(nèi)存被分為一個Eden區(qū)和兩個Survivor區(qū). 為了講述為什么要這么分, 需簡單引入垃圾回收算法.

首先最基礎(chǔ), 最簡單的垃圾回收算法叫標記-清除算法. 算法流程和算法名完全一致: 首先標記出哪些是可以回收的對象, 標記完后把對象清除. 如果按照這么個流程, 新生代應該就是一塊簡單的內(nèi)存就行, 現(xiàn)實結(jié)論告訴我們這個算法是可以優(yōu)化的.

標記清除算法的不足在于一塊完整的內(nèi)存在經(jīng)過標記-清除算法后有些內(nèi)存會被釋放掉, 這時會造成內(nèi)存空間不連續(xù), 可能不能夠存放一些較大的對象.

標記-清除算法的升級版是復制算法, 它在標記-清除的思路上作出了些改變. 首先將內(nèi)存分為兩塊, 當創(chuàng)建新對象分配內(nèi)存的時候只用兩塊中的一塊A. 當進行垃圾回收的時候只對有對象的一塊A內(nèi)存使用標記-清除算法進行回收, 回收后剩余的存活對象從內(nèi)存A移到另一塊空的內(nèi)存B中, 這樣A內(nèi)存重新變?yōu)榭諆?nèi)存, 繼續(xù)重復此分配回收過程. 這個算法似乎更好一些, 但是也只是兩塊內(nèi)存, 說明還不是現(xiàn)實中的最優(yōu)解.

考慮新的算法, 把內(nèi)存分配成均等兩塊, 等同于能夠使用的內(nèi)存變?yōu)樵瓉淼亩种涣? 根據(jù)IBM專門部分研究新生代中百分之98%的對象都是"朝生夕死"的, 也就是說在進行垃圾回收時98%的對象都被回收掉, 只有2%會從A內(nèi)存移動到B內(nèi)存. 這么一想我們把兩塊內(nèi)存割為相同的兩塊是不是有點太虧了?

下面揭曉答案: HotSpot虛擬機回收虛擬機時使用的是復制算法, 但是它分成三塊內(nèi)存, 一個占80%內(nèi)存的Eden區(qū)(堆內(nèi)存), 兩個分別占10%的Survivor區(qū). 具體操作是這樣的: 程序運行時, 用Eden區(qū)和一個Survivor區(qū)A存放新創(chuàng)建的對象. 當發(fā)生垃圾回收時, 把存活下來的對象(很少)復制到另一塊Survivor區(qū)B中, 使得Eden區(qū)和Survivor區(qū)A重新為空, 然后繼續(xù)重復這個分配回收的過程.

所以說詳細點的Jvm的內(nèi)存模型是下面這樣的

由JDK1.7及以前的永久代到JDK1.8的元數(shù)據(jù)區(qū)

搞定完堆區(qū)在JVM內(nèi)存模型中的實現(xiàn), 下面談論方法區(qū)的實現(xiàn).

在JDK1.7及以前, JVM使用永久代來實現(xiàn)方法區(qū). 這里用"實現(xiàn)"二字是經(jīng)過斟酌的, 因為永久代并不等同于方法區(qū). 從名字也可以看出它和新生代, 老年代是一脈相承的, 邏輯上是一體的, 命名為永久代是因為這部分內(nèi)存很少幾乎不被回收. 這一很少幾乎不被回收的特性正好對應方法區(qū)中存儲的類信息, 常量, 靜態(tài)變量等元素. 所以說用永久代來實現(xiàn)方法區(qū).

但是用永久代來實現(xiàn)方法區(qū)并不是最優(yōu)解, 比如容易出現(xiàn)內(nèi)存溢出問題(具體分析去除永久代, 改用Metaspace的原因可以參考文章末尾所列出的資料). 在JDK1.8中JVM改為使用元數(shù)據(jù)區(qū)來實現(xiàn)方法區(qū).

元數(shù)據(jù)區(qū)和永久代有著本質(zhì)的區(qū)別, 永久代屬于虛擬機內(nèi)存的一部分, 也就是說當在操作系統(tǒng)中啟動虛擬機進程時為它分配了一塊內(nèi)存, 而虛擬機為永久代分配內(nèi)存時用的是它自己分配得的內(nèi)存.

而元數(shù)據(jù)區(qū)Metaspace是直接在本地內(nèi)存(Native Memory)中申請的, 這樣元數(shù)據(jù)區(qū)的大小(方法區(qū)大小)只會受本地內(nèi)存大小限制, 和虛擬機進程所分得內(nèi)存無關(guān).

所以最后JVM內(nèi)存模型圖的終極版應該是這樣子

如果想學習Java工程化、高性能及分布式、深入淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java高級交流:787707172,群里有阿里大牛直播講解技術(shù),以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費分享給大家。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容