JVM內存區域

C和C++工程師掌握創建和銷毀內存空間的權力,並維護內存中每一個對象從始至終的生命。但Java工程師可以不再繁瑣的進行內存控制,並且更不容易出現內存泄露和溢出的問題,但如果不了解Java是如何自動對內存進行控制的,在出現問題後更難定位。

JVM內存運行時數據區域

JVM運行時會將所管轄的內存劃分為不同區域做不同功能,有的隨虛擬機進程而生死,有的因線程而生死,虛擬機規範規定內存中要包含以下幾個區域:

image

  • 程序計數器:較小空間的內存,用於記錄當前線程所執行的位元組碼文件所在行號。通過改變這裡的行號來實現指令中的分支、循環、跳轉等操作。因為Java的多線程是多個線程輪流切換運行的,在執行另一線程時要讓來源線程記錄好自己所處的行號,所以每個線程都有自己獨立的程序計數器,相互之間不會干擾,這種每個線程獨有的空間就叫線程私有空間。當執行的是Natvie方法時,此空間記錄為空,並且此空間是唯一不存在內存溢出錯誤的空間。

  • 棧:線程私有且生命周期與線程相同,在執行每個方法時都會創建棧幀壓入到棧里,方法的調用開始就是棧幀入棧,完成就是棧幀出棧。棧幀中記錄著方法的局部變量表、操作棧、動態鏈接和方法出口等信息。局部變量表存放了編譯期間可知的基本類型和引用類型,還有就是returnAddress類型,表示一條位元組碼指令的地址。其中64位長度的變量數據將佔用兩個變量空間,其餘佔用一個變量空間。局部變量表會在編譯期間完成分配,當進入一個方法開始指向的時候,這個方法所需要的局部內存就已經確定了,不會再改變了。當方法調用深度過深,比如出現了自調用情況時,就會出現棧溢出異常,當然一些虛擬機棧會動態擴大棧深度,當無法再獲取內存資源來擴展時會出現內存溢出異常。

  • 本地方法棧:跟虛擬機棧很類似,採用同樣的存儲方式也會有同樣的異常,但是用來運行Native方法的,沒有規定其中方法所使用的語言,所以可以自由的實現它。有的虛擬機例如HotSpot,就把虛擬機棧和本地方法棧合二為一了。

  • 堆:堆是JVM中最大的空間,是所有線程都會共享的空間,將在虛擬機啟動時創建,也就是與進程同生死。對里存放對象的實例,所有的對象實例都將在這裡進行分配。垃圾回收也將主要在堆中執行,為了更快的回收還會把堆中的對象進行各種分類。堆是物理不連續的內存空間,只要邏輯上是連續的就可以,這類似磁盤空間。當堆沒法再去獲得空間將出現內存溢出異常。

  • 方法區:存儲被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等,是線程共享的。雖然Java規範將其形容為堆的一個區域,但只是一個邏輯分區。HotSpot虛擬機把方法區在垃圾回收中設置為永生代,但其他的虛擬機並不存在永生代的說法,之所以這麼叫是因為其中的內容很少會被清理,清理起來也是比較的麻煩,Java官方Bug清單中就出現了很多因為此處空間導致的內存泄露。它像堆一樣是不連續的物理內存,可以固定或擴展大小,可以選擇不實現垃圾回收,當無法獲取內存時也會出現內存溢出異常。

  • 運行時常量池:它是方法區的一部分,Class文件中除了類的版本、字段、方法、接口等描述信息外還有就是常量池,用於存放編譯期間生成的字面量和符號引用,在類加載後放入此處空間。JVM對Class文件的每個部分都做了嚴格的規定,每個位元組存儲哪種數據都必須規範才會被JVM認可、裝載和執行,但對於常量池JVM規範並沒有特殊要求,不同的提供商可以自己實現這塊區域,所以通常除了存放字面量和符號引用外,直接引用也會存在這裡。Class文件常量池必須具備動態性,也就是並不一定在編譯期間產生,運行時也可以放入新的常量。因為是方法區的一部分,所以當空間無法擴展也會出現內存溢出異常。

  • 直接內存:並不是JVM運行時內存的一部分,並且也並未在JVM規範中定義,但這塊內存也被頻繁使用並且也會出現內存溢出異常。JDK1.4中引入了NIO,非阻塞的IO方式,可以讓Native函數直接分配堆外的內存,然後通過堆里的DirectByteBuffer對象作為引用指向這款內存,從而避免在堆和Native堆中切換複製數據,從而提高性能。堆外內存實在本機內存中的,雖然不會收到JVM的影響但空間和尋址空間的限制。當內存區域大於物理內存限制的時候會導致內存溢出異常。

Tags: