阿里面試官:請你聊一下JVM內存管理機制

前言

面試過阿里的同學都知道,阿里的面試官喜歡往深了懟,尤其是一些平時開發不怎麼常用但非常重要的知識點,更受他們的青睞,而且往往他們喜歡以連環炮的形式無限火力轟炸,這誰頂得住啊!所以我們平時一定要有意識的去積累總結這些底層的基礎的知識點,形成厚厚的盔甲,面試才能一往無前,遊刃有餘~


1. 什麼是 JVM?

JVM總結成以下幾點:

  • Java Virtual Machine(Java虛擬機)的縮寫,主要是通過在實際計算機模仿各種計算機功能來實現的
  • 由堆、方法區、棧、本地方法棧、程序計算器等部分組成
  • 實現跨平臺的最核心的部分
  • .class 文件會在 JVM 上執行,JVM 會解釋給操作系統執行
  • 有自己的指令集,解釋自己的指令集到 CPU 指令集和系統資源的調用
  • JVM 只關注被編譯的 .class 文件,不關心 .java 源文件

2. JVM內存區域劃分

JVM的內部體系結構分爲三部分,分別是:類裝載器(ClassLoader)子系統,運行時數據區,和執行引擎。

類裝載器

每一個Java虛擬機都由一個類加載器子系統(class loader subsystem),負責加載程序中的類型(類和接口),並賦予唯一的名字。每一個Java虛擬機都有一個執行引擎(execution engine)負責執行被加載類中包含的指令。JVM的兩種類裝載器包括:啓動類裝載器和用戶自定義類裝載器,啓動類裝載器是JVM實現的一部分,用戶自定義類裝載器則是Java程序的一部分,必須是ClassLoader類的子類。

執行引擎

它或者在執行字節碼,或者執行本地方法

主要的執行技術有:解釋,即時編譯,自適應優化、芯片級直接執行其中解釋屬於第一代JVM,即時編譯JIT屬於第二代JVM,自適應優化(目前Sun的HotspotJVM採用這種技術)則吸取第一代JVM和第二代JVM的經驗,採用兩者結合的方式 。

自適應優化:開始對所有的代碼都採取解釋執行的方式,並監視代碼執行情況,然後對那些經常調用的方法啓動一個後臺線程,將其編譯爲本地代碼,並進行仔細優化。若方法不再頻繁使用,則取消編譯過的代碼,仍對其進行解釋執行。

3.Java運行時數據區域劃分

運行時的數據區域按照生命週期的不同可以分爲兩個部分,分別是

  1. 隨着虛擬機進程的啓動而一直存在:方法區+Java堆

  2. 隨着用戶線程的啓動和結束而建立和銷燬:虛擬機棧+本地方法棧+程序計數器

運行時的數據區域按照共享的不同可以分爲兩個部分,分別是:

  1. 由所有線程共享的數據區:方法區+Java堆

  2. 線程隔離的數據區:虛擬機棧+本地方法棧+程序計數器

Java虛擬機所管理的運行數據區域分佈如下圖:
JVM運行時數據模型

3.1 程序計數器

3.1.1 程序計數器的概念

程序計數器是一個記錄着當前線程所執行的字節碼的行號指示器。

JAVA代碼編譯後的字節碼在未經過JIT(實時編譯器)編譯前,其執行方式是通過“字節碼解釋器”進行解釋執行。簡單的工作原理爲解釋器讀取裝載入內存的字節碼,按照順序讀取字節碼指令。讀取一個指令後,將該指令“翻譯”成固定的操作,並根據這些操作進行分支、循環、跳轉等流程。

從上面的描述中,可能會產生程序計數器是否是多餘的疑問。因爲沿着指令的順序執行下去,即使是分支跳轉這樣的流程,跳轉到指定的指令處按順序繼續執行是完全能夠保證程序的執行順序的。假設程序永遠只有一個線程,這個疑問沒有任何問題,也就是說並不需要程序計數器。但實際上程序是通過多個線程協同合作執行的。

首先我們要搞清楚JVM的多線程實現方式。JVM的多線程是通過CPU時間片輪轉(即線程輪流切換並分配處理器執行時間)算法來實現的。也就是說,某個線程在執行過程中可能會因爲時間片耗盡而被掛起,而另一個線程獲取到時間片開始執行。當被掛起的線程重新獲取到時間片的時候,它要想從被掛起的地方繼續執行,就必須知道它上次執行到哪個位置,在JVM中,通過程序計數器來記錄某個線程的字節碼執行位置。因此,程序計數器是具備線程隔離的特性,也就是說,每個線程工作時都有屬於自己的獨立計數器。

3.1.2 程序計數器的特點:

  • 線程私有,每個線程工作時都有屬於自己的獨立計數器
  • 執行java方法時,程序計數器是有值的,且記錄的是正在執行的字節碼指令的地址
  • JVM規範中唯一沒有規定OutOfMemoryError情況的區域
  • 如果正在執行的是Native 方法,則這個計數器值爲空(Undefined),因爲native方法是java通過JNI直接調用本地C/C++庫,可以近似的認爲native方法相當於C/C++暴露給java的一個接口,java通過調用這個接口從而調用到C/C++方法。由於該方法是通過C/C++而不是java進行實現。那麼自然無法產生相應的字節碼,並且C/C++執行時的內存分配是由自己語言決定的,而不是由JVM決定的
    在這裏插入圖片描述

3.2 Java虛擬機棧

  • Java虛擬機棧也是線程私有的,它的生命週期與線程相同(隨線程而生,隨線程而滅)
  • 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常;(當前大部分JVM都可以動態擴展,只不過JVM規範也允許固定長度的虛擬機棧)
  • Java虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀( Stack Framell )用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對 應着一個棧幀在虛擬機棧中入棧到出棧的過程。

3.3 本地方法棧

本地方法棧(NativeMethodStack)與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。在虛擬機規範中對本地方法棧中方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧一樣, 本地方法棧區域也會拋出StackOvrflowError和OutOfMemoryError異常。

3.4 Java堆

Java 中的堆是 JVM 所管理的最大的一塊內存空間,主要用於存放各種類的實例對象。

在 Java 中,堆被劃分成兩個不同的區域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被劃分爲三個區域:Eden、From Survivor、To Survivor。這樣劃分的目的是爲了使 JVM 能夠更好的管理堆內存中的對象,包括內存的分配以及回收。
堆的內存模型大致爲:
java堆
從圖中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通過參數 –Xms、-Xmx 來指定。

3.5 方法區

方法區( Method Area )與Java堆一樣,是各個線程共享的內存區域 ,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的-一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。

特點:

  • 方法區是線程共享的;當有多個線程都用到一個類的時候,而這個類還未被加載,則應該只有一個線程去加載類,讓其他線程等待;
  • 方法區的大小不必是固定的,jvm可以根據應用的需要動態調整。jvm也可以允許用戶和程序指定方法區的初始大小,最小和最大限制;
  • 方法區同樣存在垃圾收集,因爲通過用戶定義的類加載器可以動態擴展Java程序,這樣可能會導致一些類,不再被使用,變爲垃圾。這時候需要進行垃圾清理。

3.6 運行時常量池

運行時常量池( Runtime Constant Pool )是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(ConstantPoolTable),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。

運行時常量池與Class文件常量池區別

  • JVM對Class文件中每一部分的格式都有嚴格的要求,每一個字節用於存儲那種數據都必須符合規範上的要求才會被虛擬機認可、裝載和執行;但運行時常量池沒有這些限制,除了保存Class文件中描述的符號引用,還會把翻譯出來的直接引用也存儲在運行時常量區
  • 相較於Class文件常量池,運行時常量池更具動態性,在運行期間也可以將新的變量放入常量池中,而不是一定要在編譯時確定的常量才能放入。最主要的運用便是String類的intern()方法
  • 在方法區中,常量池有運行時常量池和Class文件常量池;但其中的內容是否完全不同,暫時還未得知

Class常量池圖示
在這裏插入圖片描述

3.7 直接內存

直接內存(Direct Memory)就是Java堆外內存

直接內存並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現。

在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆裏面的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在Java堆和Native堆中來回複製數據。

顯然,本機直接內存的分配不會受到Java堆大小的限制,但是,既然是內存,則肯定還是會受到本機總內存(包括RAM及SWAP區或者分頁文件)的大小及處理器尋址空間的限制。服務器管理員配置虛擬機參數時,一般會根據實際內存設置-Xmx等參數信息,但經常會忽略掉直接內存,使得各個內存區域的總和大於物理內存限制(包括物理上的和操作系統級的限制),從而導致動態擴展時出現OutOfMemoryError異常。

特點:

直接內存的讀寫操作比普通Buffer快,但它的創建、銷燬比普通Buffer慢(猜測原因是DirectBuffer需向OS申請內存涉及到用戶態內核態切換,而後者則直接從堆內存劃內存即可)。

因此直接內存使用於需要大內存空間且頻繁訪問的場合,不適用於頻繁申請釋放內存的場合。

使用堆外內存的原因:

  • 對垃圾回收停頓的改善。由於堆外內存是直接受操作系統管理而不是JVM,所以當我們使用堆外內存時,即可保持較小的堆內內存規模。從而在GC時減少回收停頓對於應用的影響。
  • 提升程序I/O操作的性能。通常在I/O通信過程中,會存在堆內內存到堆外內存的數據拷貝操作,對於需要頻繁進行內存間數據拷貝且生命週期較短的暫存數據,都建議存儲到堆外內存。

爲了方便大家交流學習,我建了一個QQ羣,入羣可領2020年最新Java基礎進階全套學習資料 + BAT大廠面試題,羣號: 1080345378


創作不易,您的點贊和關注是對我最大的支持٩(๑❛ᴗ❛๑)۶
↓↓↓

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章