您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關(guān)Java面試題之JVM的示例分析的內(nèi)容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
該問題主要針對你遇到的實際問題出發(fā),可以根據(jù)你實際遇到過的情況和場景,結(jié)合下面每種情況的具體原因和解決方式,整理后回答。
當堆內(nèi)存(Heap Space)沒有足夠空間存放新創(chuàng)建的對象時,就會拋出 java.lang.OutOfMemoryError:Javaheap space錯誤(根據(jù)實際生產(chǎn)經(jīng)驗,可以對程序日志中的 OutOfMemoryError 配置關(guān)鍵字告警,一經(jīng)發(fā)現(xiàn),立即處理)。
原因分析
Javaheap space 錯誤產(chǎn)生的常見原因可以分為以下幾類:
請求創(chuàng)建一個超大對象,通常是一個大數(shù)組。
超出預(yù)期的訪問量/數(shù)據(jù)量,通常是上游系統(tǒng)請求流量飆升,常見于各類促銷/秒殺活動,可以結(jié)合業(yè)務(wù)流量指標排查是否有尖狀峰值。
過度使用終結(jié)器(Finalizer),該對象沒有立即被 GC。
內(nèi)存泄漏(Memory Leak),大量對象引用沒有釋放,JVM 無法對其自動回收,常見于使用了 File 等資源沒有回收。
解決方案
針對大部分情況,通常只需要通過-Xmx 參數(shù)調(diào)高 JVM 堆內(nèi)存空間即可。如果仍然沒有解決,可以參考以下情況做進一步處理:
如果是超大對象,可以檢查其合理性,比如是否一次性查詢了數(shù)據(jù)庫全部結(jié)果,而沒有做結(jié)果數(shù)限制。
如果是業(yè)務(wù)峰值壓力,可以考慮添加機器資源,或者做限流降級。
如果是內(nèi)存泄漏,需要找到持有的對象,修改代碼設(shè)計,比如關(guān)閉沒有釋放的連接。
當 Java 進程花費 98% 以上的時間執(zhí)行 GC,但只恢復(fù)了不到 2% 的內(nèi)存,且該動作連續(xù)重復(fù)了 5 次,就會拋出 java.lang.OutOfMemoryError:GC overhead limit exceeded 錯誤。簡單地說,就是應(yīng)用程序已經(jīng)基本耗盡了所有可用內(nèi)存, GC 也無法回收。
此類問題的原因與解決方案跟 Javaheap space 非常類似,可以參考上條。
該錯誤表示永久代(Permanent Generation)已用滿,通常是因為加載的 class 數(shù)目太多或體積太大。
原因分析
永久代存儲對象主要包括以下幾類:
加載/緩存到內(nèi)存中的 class 定義,包括類的名稱,字段,方法和字節(jié)碼;
常量池;
對象數(shù)組/類型數(shù)組所關(guān)聯(lián)的 class;
JIT 編譯器優(yōu)化后的 class 信息。
PermGen 的使用量與加載到內(nèi)存的 class 的數(shù)量/大小正相關(guān)。
解決方案
根據(jù) Permgen space 報錯的時機,可以采用不同的解決方案,如下所示:
程序啟動報錯,修改 -XX:MaxPermSize 啟動參數(shù),調(diào)大永久代空間。
應(yīng)用重新部署時報錯,很可能是沒有應(yīng)用沒有重啟,導(dǎo)致加載了多份 class 信息,只需重啟 JVM 即可解決。
運行時報錯,應(yīng)用程序可能會動態(tài)創(chuàng)建大量 class,而這些 class 的生命周期很短暫,但是 JVM 默認不會卸載 class,可以設(shè)置 -XX:+CMSClassUnloadingEnabled 和 -XX:+UseConcMarkSweepGC這兩個參數(shù)允許 JVM 卸載 class。
如果上述方法無法解決,可以通過 jmap 命令 dump 內(nèi)存對象 jmap-dump:format=b,file=dump.hprof ,然后利用 Eclipse MAT https://www.eclipse.org/mat 功能逐一分析開銷最大的 classloader 和重復(fù) class。
JDK 1.8 使用 Metaspace 替換了永久代(Permanent Generation),該錯誤表示 Metaspace 已被用滿,通常是因為加載的 class 數(shù)目太多或體積太大。
此類問題的原因與解決方法跟 Permgenspace 非常類似,可以參考上文。需要特別注意的是調(diào)整 Metaspace 空間大小的啟動參數(shù)為-XX:MaxMetaspaceSize。
每個 Java 線程都需要占用一定的內(nèi)存空間,當JVM 向底層操作系統(tǒng)請求創(chuàng)建一個新的 native 線程時,如果沒有足夠的資源分配就會報此類錯誤。
原因分析
JVM 向 OS 請求創(chuàng)建 native 線程失敗,就會拋出 Unableto createnewnativethread,常見的原因包括以下幾類:
線程數(shù)超過操作系統(tǒng)最大線程數(shù) ulimit 限制;
線程數(shù)超過 kernel.pid_max(只能重啟);
native 內(nèi)存不足;
該問題發(fā)生的常見過程主要包括以下幾步:
JVM 內(nèi)部的應(yīng)用程序請求創(chuàng)建一個新的 Java 線程;
JVM native 方法代理了該次請求,并向操作系統(tǒng)請求創(chuàng)建一個 native 線程;
操作系統(tǒng)嘗試創(chuàng)建一個新的 native 線程,并為其分配內(nèi)存;
如果操作系統(tǒng)的虛擬內(nèi)存已耗盡,或是受到 32 位進程的地址空間限制,操作系統(tǒng)就會拒絕本次 native 內(nèi)存分配;
JVM 將拋出 java.lang.OutOfMemoryError:Unableto createnewnativethread 錯誤。
解決方案
升級配置,為機器提供更多的內(nèi)存;
降低 Java Heap Space 大小;
修復(fù)應(yīng)用程序的線程泄漏問題;
限制線程池大?。?/p>
使用 -Xss 參數(shù)減少線程棧的大?。?/p>
調(diào)高 OS 層面的線程最大數(shù):執(zhí)行 ulimia-a 查看最大線程數(shù)限制,使用 ulimit-u xxx 調(diào)整最大線程數(shù)限制。
該錯誤表示所有可用的虛擬內(nèi)存已被耗盡。虛擬內(nèi)存(Virtual Memory)由物理內(nèi)存(Physical Memory)和交換空間(Swap Space)兩部分組成。當運行時程序請求的虛擬內(nèi)存溢出時就會報 Outof swap space 錯誤。
原因分析
該錯誤出現(xiàn)的常見原因包括以下幾類:
地址空間不足;
物理內(nèi)存已耗光;
應(yīng)用程序的本地內(nèi)存泄漏(native leak),例如不斷申請本地內(nèi)存,卻不釋放。
執(zhí)行 jmap-histo:live 命令,強制執(zhí)行 Full GC;如果幾次執(zhí)行后內(nèi)存明顯下降,則基本確認為 Direct ByteBuffer 問題。
解決方案
根據(jù)錯誤原因可以采取如下解決方案:
升級地址空間為 64 bit;
使用 Arthas 檢查是否為 Inflater/Deflater 解壓縮問題,如果是,則顯式調(diào)用 end 方法。
Direct ByteBuffer 問題可以通過啟動參數(shù) -XX:MaxDirectMemorySize 調(diào)低閾值。
升級服務(wù)器配置/隔離部署,避免爭用。
有一種內(nèi)核作業(yè)(Kernel Job)名為 Out of Memory Killer,它會在可用內(nèi)存極低的情況下“殺死”(kill)某些進程。OOM Killer 會對所有進程進行打分,然后將評分較低的進程“殺死”,具體的評分規(guī)則可以參考 Surviving the Linux OOM Killer。
不同于其他的 OOM 錯誤, Killprocessorsacrifice child 錯誤不是由 JVM 層面觸發(fā)的,而是由操作系統(tǒng)層面觸發(fā)的。
原因分析
默認情況下,Linux 內(nèi)核允許進程申請的內(nèi)存總量大于系統(tǒng)可用內(nèi)存,通過這種“錯峰復(fù)用”的方式可以更有效的利用系統(tǒng)資源。
然而,這種方式也會無可避免地帶來一定的“超賣”風險。例如某些進程持續(xù)占用系統(tǒng)內(nèi)存,然后導(dǎo)致其他進程沒有可用內(nèi)存。此時,系統(tǒng)將自動激活 OOM Killer,尋找評分低的進程,并將其“殺死”,釋放內(nèi)存資源。
解決方案
升級服務(wù)器配置/隔離部署,避免爭用。
OOM Killer 調(diào)優(yōu)。
JVM 限制了數(shù)組的最大長度,該錯誤表示程序請求創(chuàng)建的數(shù)組超過最大長度限制。
JVM 在為數(shù)組分配內(nèi)存前,會檢查要分配的數(shù)據(jù)結(jié)構(gòu)在系統(tǒng)中是否可尋址,通常為 Integer.MAX_VALUE-2。
此類問題比較罕見,通常需要檢查代碼,確認業(yè)務(wù)是否需要創(chuàng)建如此大的數(shù)組,是否可以拆分為多個塊,分批執(zhí)行。
Java 允許應(yīng)用程序通過 Direct ByteBuffer 直接訪問堆外內(nèi)存,許多高性能程序通過 Direct ByteBuffer 結(jié)合內(nèi)存映射文件(Memory Mapped File)實現(xiàn)高速 IO。
原因分析
Direct ByteBuffer 的默認大小為 64 MB,一旦使用超出限制,就會拋出 Directbuffer memory 錯誤。
解決方案
Java 只能通過 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通過 Arthas 等在線診斷工具攔截該方法進行排查。
檢查是否直接或間接使用了 NIO,如 netty,jetty 等。
通過啟動參數(shù) -XX:MaxDirectMemorySize 調(diào)整 Direct ByteBuffer 的上限值。
檢查 JVM 參數(shù)是否有 -XX:+DisableExplicitGC 選項,如果有就去掉,因為該參數(shù)會使 System.gc() 失效。
檢查堆外內(nèi)存使用代碼,確認是否存在內(nèi)存泄漏;或者通過反射調(diào)用 sun.misc.Cleaner 的 clean() 方法來主動釋放被 Direct ByteBuffer 持有的內(nèi)存空間。
內(nèi)存容量確實不足,升級配置。
Java 虛擬機在執(zhí)行 Java 程序的過程中會把它管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域。每個區(qū)域都有各自的作用,其中有一些會隨著虛擬機啟動而創(chuàng)建,隨著虛擬機退出而銷毀。另外一些則是與線程一一對應(yīng)的,這些與線程一一對應(yīng)的數(shù)據(jù)區(qū)域會隨著線程開始和結(jié)束而創(chuàng)建和銷毀。
線程私有:程序計數(shù)器、JVM棧、本地棧
線程共享:堆、方法區(qū)(永久代或元空間、代碼緩存)
堆內(nèi)存(Java Heap)
對于大多數(shù)應(yīng)用來說,Java堆是Java虛擬機所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存。
Java堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱做“GC堆”。如果從內(nèi)存回收的角度看,由于現(xiàn)在收集器基本都是采用的分代收集算法,所以Java堆中還可以細分為:新生代和老年代;再細致一點的有Eden空間、From Survivor空間、To Survivor空間,默認情況下年輕代按照8:1:1的比例來分配。
方法區(qū)(Method Area)
與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。雖然Java虛擬機規(guī)范把方法區(qū)描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應(yīng)該是與Java堆區(qū)分開來。
對于習慣在HotSpot虛擬機上開發(fā)和部署程序的開發(fā)者來說,很多人愿意把方法區(qū)稱為“永久代”(Permanent Generation),本質(zhì)上兩者并不等價,僅僅是因為HotSpot虛擬機的設(shè)計團隊選擇把GC分代收集擴展至方法區(qū),或者說使用永久代來實現(xiàn)方法區(qū)而已。
程序計數(shù)器(Program Counter Register)
程序計數(shù)器是一塊較小的內(nèi)存空間,它的作用可以看做是當前線程所執(zhí)行的字節(jié)碼的行號指示器。在虛擬機的概念模型里(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實現(xiàn)),字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成。
此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
JVM棧(JVM Stacks)
與程序計數(shù)器一樣,Java虛擬機棧也是線程私有的,它的生命周期與線程相同。虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會同時創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作棧、動態(tài)鏈接、方法出口等信息。每一個方法被調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型、對象引用和returnAddress類型(指向了一條字節(jié)碼指令的地址)。
在Java虛擬機規(guī)范中,對這個區(qū)域規(guī)定了兩種異常狀況:
如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常;
如果虛擬機棧可以動態(tài)擴展,當擴展時無法申請到足夠的內(nèi)存時會拋出OutOfMemoryError異常。
本地方法棧(Native Method Stacks)
本地方法棧與虛擬機棧所發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機使用到的Native方法服務(wù)。
虛擬機規(guī)范中對本地方法棧中的方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強制規(guī)定,因此具體的虛擬機可以自由實現(xiàn)它。甚至有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二為一。與虛擬機棧一樣,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常。
當虛擬機遇見new關(guān)鍵字時候,實現(xiàn)判斷當前類是否已經(jīng)加載,如果類沒有加載,首先執(zhí)行類的加載機制,加載完成后再為對象分配空間、初始化等。
首先校驗當前類是否被加載,如果沒有加載,執(zhí)行類加載機制
加載:就是從字節(jié)碼加載成二進制流的過程
驗證:當然加載完成之后,當然需要校驗Class文件是否符合虛擬機規(guī)范,跟我們接口請求一樣,第一件事情當然是先做個參數(shù)校驗了
準備:為靜態(tài)變量、常量賦默認值
解析:把常量池中符號引用(以符號描述引用的目標)替換為直接引用(指向目標的指針或者句柄等)的過程
初始化:執(zhí)行static代碼塊(cinit)進行初始化,如果存在父類,先對父類進行初始化
tips:靜態(tài)代碼塊是絕對線程安全的,只能隱式被java虛擬機在類加載過程中初始化調(diào)用!
當類加載完成之后,緊接著就是對象分配內(nèi)存空間和初始化的過程
首先為對象分配合適大小的內(nèi)存空間
接著為實例變量賦默認值
設(shè)置對象的頭信息,對象hash碼、GC分代年齡、元數(shù)據(jù)信息等
執(zhí)行構(gòu)造函數(shù)(init)初始化 每日小結(jié)
感謝各位的閱讀!關(guān)于“Java面試題之JVM的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。