<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【問答】JVM哪些區(qū)域會觸發(fā)OOM?實踐檢驗一下

          共 5500字,需瀏覽 11分鐘

           ·

          2021-03-11 16:24

          作者:z小趙

          ★ 

          一枚用心堅持寫原創(chuàng)的“無趣”程序猿,在自身受益的同時也讓朋友們在技術(shù)上有所提升。


          前言

          「君子需嚴(yán)于律己,一日三省吾身」

          在生產(chǎn)環(huán)境中,一向以求穩(wěn)的心態(tài)去慎重升級技術(shù)棧。新版本的發(fā)布,貿(mào)然升級可能會使得生產(chǎn)環(huán)境服務(wù)不穩(wěn)定甚至出現(xiàn)故障,但這不能成為你不去學(xué)習(xí)更加優(yōu)秀技術(shù)的借口。

          現(xiàn)在你的大腦中是否存有這樣的直觀認知?比如:「Synchronized」 就是重量級鎖,在并發(fā)環(huán)境中應(yīng)該摒棄掉它?比如:「ConcurrentHashMap」 還是以分段加鎖的模式來保證線程安全?在如:ArrayList 在刪除元素的時候就比 LinkedList 慢?

          不知道有沒有人調(diào)查過目前 JVM 的各個垃圾回收器在市場中的占有率?如果目前你還停留在 「CMS(Concurrent Mark Sweep)」 階段,那么請和作者一起反思下,為什么 「G1(Garbage First)」 已經(jīng)悄悄占領(lǐng)了垃圾回收的高地?你卻毫不知情,更有甚者,有些技術(shù)團隊已經(jīng)在嘗試使用「ZGC」垃圾回收器了。

          其實在很大一部分情況下,如果你愿意去深入了解技術(shù)升級后帶來的優(yōu)勢(不管是服務(wù)響應(yīng)速度更快,還是能夠解決一些當(dāng)下系統(tǒng)存在的不足),可能你會更加愿意去做技術(shù)升級,而不是一味的求穩(wěn)而容忍系統(tǒng)存在的不足。

          基于此,我們更應(yīng)該回頭重新審視自己的技術(shù)認知在當(dāng)下是否還是正確的。后續(xù)文章我想重新認識下 JVM 開發(fā)團隊為了提高 JVM 的工作效率所作出的不斷改進,并結(jié)合一些實際操作姿勢來深入學(xué)習(xí)一下。如果你有同樣的想法,那請跟隨我的文章來一起學(xué)習(xí)。

          JVM 運行時內(nèi)存數(shù)據(jù)區(qū)分布

          JDK7 以前的運行時數(shù)據(jù)區(qū)分布圖如下:

          JDK8 以后的運行時數(shù)據(jù)區(qū)分布圖如下:

          JVM 運行時,整個內(nèi)存大概被分為以上幾大塊,其中黃色部分是線程私有的,而綠色區(qū)域為線程共享內(nèi)存的,粉色部分是機器自帶的方法庫,不在 JVM 的管理范圍內(nèi),而元數(shù)據(jù)空間是 JDK8 引入的,用于替代方法區(qū)。

          從上面兩個不同的 JDK 版本對應(yīng)的運行時數(shù)據(jù)區(qū)圖可以看出,JDK8 以后 JVM 的運行時數(shù)據(jù)區(qū)發(fā)生了一些變化,JDK8 取消了方法區(qū)并使用元數(shù)據(jù)空間進行替代,元數(shù)據(jù)空間的內(nèi)存是在運行時數(shù)據(jù)區(qū)外分配的一塊內(nèi)存。

          接下來就每個區(qū)域所扮演的角色和功能,分析下運行時數(shù)據(jù)區(qū)每部分的區(qū)域要實現(xiàn)的功能是什么?每部分會發(fā)生哪些內(nèi)存溢出情況,并通過具體示例演示對應(yīng)的內(nèi)存溢出情況,以便在生產(chǎn)環(huán)境中出現(xiàn)內(nèi)存溢出時更快定位問題。

          程序計數(shù)器

          「記錄行號,指示虛擬機下一條應(yīng)該執(zhí)行的命令」。在單線程環(huán)境中,虛擬機可以按照順序、跳轉(zhuǎn)、分支等不同邏輯,選擇相應(yīng)的下一條該執(zhí)行的命令是沒有問題的;但是在多線程環(huán)境下,由于系統(tǒng)采用時間片的方式,導(dǎo)致多條線程的上下文會不斷的切換,如果線程當(dāng)前執(zhí)行到的位置沒有被記錄下來,此時線程讓出 CPU 輪到其他線程執(zhí)行,當(dāng)再次輪到當(dāng)前線程執(zhí)行的時候,由于不知道上一次中斷的位置,也就意味著不知道該從哪里開始接著執(zhí)行了。所以需要一個能夠記錄線程中斷位置的存儲器,即程序計數(shù)器。

          由于每條線程都需要一個對應(yīng)的程序計數(shù)器用于記錄線程中斷時執(zhí)行到的位置,也就意味著程序計數(shù)器是和線程綁定的,所以程序計數(shù)器是線程私有的。

          試想記錄每條線程執(zhí)行過程中被中斷的位置,需要占用的內(nèi)存是非常少的;另外隨著線程的銷毀,對應(yīng)的程序計數(shù)器占用的內(nèi)存也就跟著被回收了;所以 Java 虛擬機規(guī)范規(guī)定此區(qū)域為唯一一塊不會出現(xiàn)任何運行時異常的內(nèi)存區(qū)域,如 StackOverflowError、OutOfMemoryError。

          虛擬機棧

          「存儲方法執(zhí)行時的局部變量表、操作數(shù)棧、動態(tài)連接、方法返回等信息」。方法開始執(zhí)行,創(chuàng)建對應(yīng)的棧幀,隨著方法的執(zhí)行過程變化,棧幀中的數(shù)據(jù)不斷進行入棧出棧操作,當(dāng)方法執(zhí)行完后棧空并銷毀。

          通過棧幀的創(chuàng)建與銷毀的過程可以看出,棧幀和方法是相對應(yīng)的,而虛擬機棧存儲了一個個棧幀,當(dāng)棧幀全部出棧以后,對應(yīng)的線程工作也就完成了,也就是說虛擬機棧是線程私有的。

          如上圖所示,虛擬機棧是有一個一個的棧幀組成,隨著一個方法被調(diào)用,此時會創(chuàng)建出一個對應(yīng)的棧幀,并將其加入到虛擬機棧中。虛擬機棧棧頂?shù)臈Q之為當(dāng)前棧幀,線程只會操作棧頂?shù)臈ū徊僮鞯臈卜Q之為活動棧幀),對應(yīng)的方法被稱之為當(dāng)前方法,每一個方法的執(zhí)行開始到結(jié)束對應(yīng)著一個棧幀在虛擬機棧中的入棧出棧操作。每個棧幀又由局部變量表、操作數(shù)棧、動態(tài)連接、方法返回地址等幾部分組成。

          局部變量表

          局部變量表用于存儲方法入?yún)ⅲ椒▋?nèi)部定義的局部變量等信息。局部變量是通過變量槽來表示的,每個變量槽可以保存的數(shù)據(jù)類型有 boolean、byte、char、short、int、float、reference 等,如果變量是 64 位 的話,會占用兩個變量槽,如果是小于等于 32 位的話,只占用一個變量槽。變量槽的數(shù)量在 Java 文件被編譯后就確定了,但是局部變量表具體占用多大內(nèi)存是由不同虛擬機機制決定的。

          操作數(shù)棧

          顧名思義,操作數(shù)棧是一個存放操作數(shù)的棧(先進后出的數(shù)據(jù)結(jié)構(gòu))。那么操作數(shù)是什么? 簡單來說就是指令(在JVM中就是字節(jié)碼指令)操作的對象(操作的數(shù)),比如說JVM的iadd 指令的作用就是將操作數(shù)棧中棧頂?shù)膬蓚€int類型的數(shù)值相加,然后將結(jié)果壓入操作數(shù)棧。此時這兩個int類型的數(shù)值就是針對 iadd 指令的操作數(shù)。

          所以你編寫的那些方法里的語句,最終會編譯成一個個字節(jié)碼指令,然后由JVM去執(zhí)行,執(zhí)行的過程中就會用到操作數(shù)棧。

          趣談編程注:JVM是基于棧的指令架構(gòu)。

          動態(tài)連接

          試想當(dāng)程序需要執(zhí)行某個方法時,如何確定被調(diào)用的方法的內(nèi)存位置呢?首先需要通過符號引用(只是一個符號,代表了這個方法)轉(zhuǎn)化為直接引用(可以簡單理解為可以實際操作目標(biāo)的引用,比如指向目標(biāo)的指針),符號引用被存儲在方法區(qū)的運行時常量區(qū)(JDK7 以前)或者元數(shù)據(jù)空間中(JDK8 以后)。

          對于符號引用,有的是在類加載階段就將其轉(zhuǎn)為直接引用,此類連接稱之為靜態(tài)連接;而有的是在方法調(diào)用時動態(tài)的轉(zhuǎn)為直接引用,這類連接稱之為動態(tài)連接。

          注意:符號引用不僅僅存在于方法,類和字段也有符號引用。

          方法返回地址

          當(dāng)一個方法執(zhí)行完畢或發(fā)生異常時,方法退出后需要返回到之前被調(diào)用的方法的位置,然后程序在繼續(xù)執(zhí)行;當(dāng)方法正常返回時,需要在當(dāng)前棧幀中記錄一些信息,如返回值信息等,幫助調(diào)用者恢復(fù)執(zhí)行狀態(tài)。

          虛擬機棧異常

          Java 虛擬機規(guī)范規(guī)定了棧是有深度的,當(dāng)棧深度超過了指定大小后會拋出 StackOverflowError。為什么 Java 虛擬機要規(guī)定棧的深度呢?細想一下,假設(shè)不規(guī)定棧的深度的話,線程在執(zhí)行方法的時候,如果方法內(nèi)部出現(xiàn)了死循環(huán),比如在方法內(nèi)部在調(diào)用自己,導(dǎo)致不停的創(chuàng)建新的棧幀被壓入虛擬機棧中,隨著棧幀被不斷被加入到棧中,必然需要申請更多的內(nèi)存來存儲數(shù)據(jù),當(dāng)內(nèi)存被不停的占用,最終導(dǎo)致整個虛擬機內(nèi)存被使用完,那將是一個災(zāi)難性的事情,所以虛擬機棧里規(guī)定了棧深度。

          在虛擬機棧區(qū)域內(nèi),Java 虛擬機規(guī)范還規(guī)定了如果此區(qū)域的內(nèi)存大小是動態(tài)可擴展的話,那么當(dāng)內(nèi)存不夠使用的時候,虛擬機棧想要申請更多的內(nèi)存來存儲元素,但如果申請不到足夠多的內(nèi)存來存儲變量的話,就會觸發(fā) OutOfMemoryError(目前生產(chǎn)環(huán)境中,大部分都是用的是 HotSpot 虛擬機,其不支持動態(tài)可擴展,所以一般不會出現(xiàn) OOM)。

          ?

          說明:后續(xù)關(guān)于各種區(qū)域內(nèi)存異常演示,沒有特殊說明情況下,都是基于 jdk1.8 下,并采用 CMS 垃圾收集器環(huán)境進行演示的。

          ?

          下面通過示例代碼來演示一下虛擬機棧異常的情況。

          1. 「默認情況下棧溢出」

          如下圖,默認情況下棧深度達到 15970 后拋出了 StackOverflowError 錯誤。(注意:默認時每次測試棧拋出異常時對應(yīng)的棧最大深度都不相同,所以這個默認值的大小和測試機本身還有一些關(guān)系,不過一般方法的棧深度基本不會達到這么大,如果感興趣的朋友可以在研究下為什么每次測試對應(yīng)的棧最大深度都不一樣,作者暫時還沒搞清楚是為什么):

          1. 「配置 -Xss160k 參數(shù)」

          指定棧容量大小后,棧溢出情況如下圖:

          1. 「配置 -Xss128k」

          如果指定棧內(nèi)存小于 160k 會報如下圖所示錯誤,即 JVM 的內(nèi)存大小是有限制的:

          本地方法棧

          本地方法棧和虛擬機棧類似,區(qū)別在于虛擬機棧是服務(wù)于 Java 方法的,而本地方法棧服務(wù)于本地的方法的。

          本地方法是什么?本地方法可以簡單理解成是被 native 關(guān)鍵字修飾的方法,也許平時你對本地方法調(diào)用的使用非常少甚至沒有用過,但如果你看過 Unsafe 類,那么應(yīng)該對 native 方法就不陌生了,這里不會展開講述 Unsafe 類,感興趣的朋友可以自行研究。

          通常我們會把本地方法棧和虛擬機棧混為一個東西,在一定程度上也沒有太大的問題,所以本地方法棧同虛擬機棧一樣,同樣是線程私有的,同時 Java 虛擬機規(guī)范規(guī)定此區(qū)域可以拋出 StackOverflowError 和 OutOfMemoryError 兩種異常。

          Java 堆

          Java 堆是用于存儲程序運行時創(chuàng)建的對象,也是 JVM 虛擬機重點關(guān)注的一塊地方。

          比如通過 new 關(guān)鍵字創(chuàng)建一個對象,那么該對象就會在堆區(qū)中為其分配一部分內(nèi)存存儲該對象,一個對象可以被多個引用去指向,可以類比成 C 和 C++ 中的指針,不同線程內(nèi)部的某個變量都可以指向堆中的同一個對象,所以堆并不是某個線程私有的,而是公共的。

          堆中的對象不停產(chǎn)生,同時伴隨著對象引用的取消,導(dǎo)致創(chuàng)建出來的對象不被 JVM 中任何變量引用,此時認為該對象可以被垃圾回收器回收。對于堆中對象的回收,不同垃圾收集器采用了不同的方式進行回收,以目前使用最為廣泛的兩種收集器為例。

          CMS 垃圾回收器采用分代回收思想進行垃圾回收,根據(jù)對象的產(chǎn)生和生存時間的不同,將堆分為新生代、老年代、永久代,其中新生代又被分為一個 Eden 區(qū)和兩個 Survivor 區(qū),默認比例為 8:1:1。

          G1 垃圾回收器將堆內(nèi)存切分成一個一個的 Region 塊,每個 Region 內(nèi)的對象可能會包含了任何年代的對象(新生代,老年代,幸存區(qū)),每次 G1 垃圾回收器會根據(jù)回收所獲得空間大小以及回收所需要的時間來進行回收。

          Java 堆的大小我們可以通過 -Xmx 和 -Xms 兩個參數(shù)來控制,其中 -Xms 參數(shù)指定了堆內(nèi)存的最小值, -Xmx 指定了當(dāng)堆內(nèi)存不夠時,可以擴展到最大 -Xmx 指定大小的內(nèi)存。Java 虛擬機規(guī)范規(guī)定當(dāng)擴展到 -Xmx 時指定的容量時,還沒有足夠的內(nèi)存去容納新產(chǎn)生的對象時,就會觸發(fā) OutOfMemoryError 的異常。

          下面通過指定 -Xmx15m -Xms10m,堆內(nèi)存異常時的情況如下圖:

          可見,當(dāng)堆內(nèi)存不足以容納新產(chǎn)生的對象時,會拋出 OutOfMemoryError 異常,并且指定說明了是 Java heap space 區(qū)。

          如果指定 -Xmx 值小于 -Xms 時,程序會在初始化時直接拋出如下圖異常:

          方法區(qū)

          用于存放已經(jīng)被虛擬機加載的類型信息、常量、靜態(tài)變量等等信息。在 Java8 之前還有永久代的概念時,方法區(qū)就是在永久代實現(xiàn)的,而在 Java8 之后,已經(jīng)不在有永久代的概念,而是使用了元數(shù)據(jù)空間進行了替代。

          直接內(nèi)存(Direct Memory)

          直接內(nèi)存又稱之為堆外內(nèi)存,這塊內(nèi)存不被JVM所管理,其不屬于 Java 虛擬機運行時數(shù)據(jù)區(qū)的一部分,可以通過 DirectByteBuffer 對象去操作堆外內(nèi)存。

          使用直接內(nèi)存在一些場景下可以顯著提高性能。比如Netty在接收和發(fā)送數(shù)據(jù)的時候使用了 DirectByteBuffer,避免了堆內(nèi)存與直接內(nèi)存之間的拷貝。如果使用傳統(tǒng)的HeapByteBuffer來進行Socket讀數(shù)據(jù)的話,則需要將Socket緩沖區(qū)的內(nèi)容先拷貝到本地內(nèi)存中,然后再將本地內(nèi)存中的內(nèi)容拷貝到堆內(nèi)存中。DirectByteBuffer則不用堆內(nèi)存與直接內(nèi)存之間的拷貝。見下圖:

          Java 虛擬機規(guī)定,直接內(nèi)存在沒有足夠的空間容納新產(chǎn)生的對象時,同樣也會產(chǎn)生 OutOfMemoryError 異常。默認情況下直接內(nèi)存和 Java 堆內(nèi)存大小相等,也可以通過 「-XX:MaxDirectMemorySize」 參數(shù)指定直接內(nèi)存的大小。

          指定 -XX:MaxDirectMemorySize=15m,直接內(nèi)存異常異常情況如下圖,從報錯信息可以看出,是 Native 方法拋出的 OOM:

          總結(jié)

          通過比對不同 JDK 版本發(fā)現(xiàn),JVM 團隊為提高垃圾回收工作效率而做出的一些努力;同時結(jié)合具體的示例驗證 JVM 不同區(qū)域發(fā)生異常時的情況,從而加深對 JVM 不同區(qū)域的理解。

          PS: 問答欄目專注于程序員平時遇到的大大小小的問題,偏實戰(zhàn),如果你平時有遇到什么問題,或者你樂于幫助別人解答問題。歡迎加我微信(QuTanBianCheng_Tao)拉你進問答社區(qū)群,加我時備注問答社區(qū)



          趣談編程

          讓天下沒有

          難懂的技術(shù)

          瀏覽 62
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕A片无码免费看 | 韩国一区二区三区 | 欧美黑人推油视频在线看 | 91麻豆亚洲国产成人久久精品 | 日本一级毛片 |