天天看點

jvm系列(二):JVM記憶體結構

所有的Java開發人員可能會遇到這樣的困惑?我該為堆記憶體設定多大空間呢?OutOfMemoryError的異常到底涉及到運作時資料的哪塊區域?該怎麼解決呢?其實如果你經常解決伺服器性能問題,那麼這些問題就會變的非常常見,了解JVM記憶體也是為了伺服器出現性能問題的時候可以快速的了解那塊的記憶體區域出現問題,以便于快速的解決生産故障。

先看一張圖,這張圖能很清晰的說明JVM記憶體結構布局。

jvm系列(二):JVM記憶體結構

JVM記憶體結構主要有三大塊:堆記憶體、方法區和棧。堆記憶體是JVM中最大的一塊由年輕代和老年代組成,而年輕代記憶體又被分成三部分,Eden空間、From Survivor空間、To Survivor空間,預設情況下年輕代按照8:1:1的比例來配置設定;

方法區存儲類資訊、常量、靜态變量等資料,是線程共享的區域,為與Java堆區分,方法區還有一個别名Non-Heap(非堆);棧又分為java虛拟機棧和本地方法棧主要用于方法的執行。

在通過一張圖來了解如何通過參數來控制各區域的記憶體大小

jvm系列(二):JVM記憶體結構
控制參數

  • -Xms設定堆的最小空間大小。
  • -Xmx設定堆的最大空間大小。
  • -XX:NewSize設定新生代最小空間大小。
  • -XX:MaxNewSize設定新生代最大空間大小。
  • -XX:PermSize設定永久代最小空間大小。
  • -XX:MaxPermSize設定永久代最大空間大小。
  • -Xss設定每個線程的堆棧大小。

沒有直接設定老年代的參數,但是可以設定堆空間大小和新生代空間大小兩個參數來間接控制。

老年代空間大小=堆空間大小-年輕代大空間大小

從更高的一個次元再次來看JVM和系統調用之間的關系

jvm系列(二):JVM記憶體結構

方法區和對是所有線程共享的記憶體區域;而java棧、本地方法棧和程式員計數器是運作是線程私有的記憶體區域。

下面我們詳細介紹每個區域的作用

Java堆(Heap)

對于大多數應用來說,Java堆(Java Heap)是Java虛拟機所管理的記憶體中最大的一塊。Java堆是被所有線程共享的一塊記憶體區域,在虛拟機啟動時建立。此記憶體區域的唯一目的就是存放對象執行個體,幾乎所有的對象執行個體都在這裡配置設定記憶體。

Java堆是垃圾收集器管理的主要區域,是以很多時候也被稱做“GC堆”。如果從記憶體回收的角度看,由于現在收集器基本都是采用的分代收集算法,是以Java堆中還可以細分為:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。

根據Java虛拟機規範的規定,Java堆可以處于實體上不連續的記憶體空間中,隻要邏輯上是連續的即可,就像我們的磁盤空間一樣。在實作時,既可以實作成固定大小的,也可以是可擴充的,不過目前主流的虛拟機都是按照可擴充來實作的(通過-Xmx和-Xms控制)。

如果在堆中沒有記憶體完成執行個體配置設定,并且堆也無法再擴充時,将會抛出OutOfMemoryError異常。

方法區(Method Area)

方法區(Method Area)與Java堆一樣,是各個線程共享的記憶體區域,它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。雖然Java虛拟機規範把方法區描述為堆的一個邏輯部分,但是它卻有一個别名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。

對于習慣在HotSpot虛拟機上開發和部署程式的開發者來說,很多人願意把方法區稱為“永久代”(Permanent Generation),本質上兩者并不等價,僅僅是因為HotSpot虛拟機的設計團隊選擇把GC分代收集擴充至方法區,或者說使用永久代來實作方法區而已。

Java虛拟機規範對這個區域的限制非常寬松,除了和Java堆一樣不需要連續的記憶體和可以選擇固定大小或者可擴充外,還可以選擇不實作垃圾收集。相對而言,垃圾收集行為在這個區域是比較少出現的,但并非資料進入了方法區就如永久代的名字一樣“永久”存在了。這個區域的記憶體回收目标主要是針對常量池的回收和對類型的解除安裝,一般來說這個區域的回收“成績”比較難以令人滿意,尤其是類型的解除安裝,條件相當苛刻,但是這部分區域的回收确實是有必要的。

根據Java虛拟機規範的規定,當方法區無法滿足記憶體配置設定需求時,将抛出OutOfMemoryError異常。

方法區有時被稱為持久代(PermGen)。

jvm系列(二):JVM記憶體結構

所有的對象在執行個體化後的整個運作周期内,都被存放在堆記憶體中。堆記憶體又被劃分成不同的部分:伊甸區(Eden),幸存者區域(Survivor Sapce),老年代(Old Generation Space)。

方法的執行都是伴随着線程的。原始類型的本地變量以及引用都存放線上程棧中。而引用關聯的對象比如String,都存在在堆中。為了更好的了解上面這段話,我們可以看一個例子:

  1. import java.text.SimpleDateFormat;
  2. import java.util.Date;
  3. import org.apache.log4j.Logger;
  4. public class HelloWorld {
  5.    private static Logger LOGGER = Logger.getLogger(HelloWorld.class.getName());
  6.    public void sayHello(String message) {
  7.        SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.YYYY");
  8.        String today = formatter.format(new Date());
  9.        LOGGER.info(today + ": " + message);
  10.    }
  11. }

這段程式的資料在記憶體中的存放如下:

jvm系列(二):JVM記憶體結構

通過JConsole工具可以檢視運作中的Java程式(比如Eclipse)的一些資訊:堆記憶體的配置設定,線程的數量以及加載的類的個數;

jvm系列(二):JVM記憶體結構

程式計數器(Program Counter Register)

程式計數器(Program Counter Register)是一塊較小的記憶體空間,它的作用可以看做是目前線程所執行的位元組碼的行号訓示器。在虛拟機的概念模型裡(僅是概念模型,各種虛拟機可能會通過一些更高效的方式去實作),位元組碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程恢複等基礎功能都需要依賴這個計數器來完成。

由于Java虛拟機的多線程是通過線程輪流切換并配置設定處理器執行時間的方式來實作的,在任何一個确定的時刻,一個處理器(對于多核處理器來說是一個核心)隻會執行一條線程中的指令。是以,為了線程切換後能恢複到正确的執行位置,每條線程都需要有一個獨立的程式計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類記憶體區域為“線程私有”的記憶體。

如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛拟機位元組碼指令的位址;如果正在執行的是Natvie方法,這個計數器值則為空(Undefined)。

此記憶體區域是唯一一個在Java虛拟機規範中沒有規定任何OutOfMemoryError情況的區域。

JVM棧(JVM Stacks)

與程式計數器一樣,Java虛拟機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。虛拟機棧描述的是Java方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用于存儲局部變量表、操作棧、動态連結、方法出口等資訊。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛拟機棧中從入棧到出棧的過程。

局部變量表存放了編譯期可知的各種基本資料類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同于對象本身,根據不同的虛拟機實作,它可能是一個指向對象起始位址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress類型(指向了一條位元組碼指令的位址)。

其中64位長度的long和double類型的資料會占用2個局部變量空間(Slot),其餘的資料類型隻占用1個。局部變量表所需的記憶體空間在編譯期間完成配置設定,當進入一個方法時,這個方法需要在幀中配置設定多大的局部變量空間是完全确定的,在方法運作期間不會改變局部變量表的大小。

在Java虛拟機規範中,對這個區域規定了兩種異常狀況:如果線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflowError異常;如果虛拟機棧可以動态擴充(目前大部分的Java虛拟機都可動态擴充,隻不過Java虛拟機規範中也允許固定長度的虛拟機棧),當擴充時無法申請到足夠的記憶體時會抛出OutOfMemoryError異常。

本地方法棧(Native Method Stacks)

本地方法棧(Native Method Stacks)與虛拟機棧所發揮的作用是非常相似的,其差別不過是虛拟機棧為虛拟機執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛拟機使用到的Native方法服務。虛拟機規範中對本地方法棧中的方法使用的語言、使用方式與資料結構并沒有強制規定,是以具體的虛拟機可以自由實作它。甚至有的虛拟機(譬如Sun HotSpot虛拟機)直接就把本地方法棧和虛拟機棧合二為一。與虛拟機棧一樣,本地方法棧區域也會抛出StackOverflowError和OutOfMemoryError異常。

哪兒的OutOfMemoryError

對記憶體結構清晰的認識同樣可以幫助了解不同OutOfMemoryErrors:

  1. Exception in thread “main”: java.lang.OutOfMemoryError: Java heap space

原因:對象不能被配置設定到堆記憶體中

  1. Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space

原因:類或者方法不能被加載到老年代。它可能出現在一個程式加載很多類的時候,比如引用了很多第三方的庫;

  1. Exception in thread “main”: java.lang.OutOfMemoryError: Requested array size exceeds VM limit

原因:建立的數組大于堆記憶體的空間

  1. Exception in thread “main”: java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?

原因:配置設定本地配置設定失敗。JNI、本地庫或者Java虛拟機都會從本地堆中配置設定記憶體空間。

  1. Exception in thread “main”: java.lang.OutOfMemoryError: <reason> <stack trace>(Native method)

原因:同樣是本地方法記憶體配置設定失敗,隻不過是JNI或者本地方法或者Java虛拟機發現

維基百科對JVM的介紹

Overview of a Java virtual machine (JVM) architecture based on The Java Virtual Machine Specification Java SE 7 Edition

A Java virtual machine (JVM) is an abstract computing machine that enables a computer to run a Java program. There are three notions of the JVM: specification, implementation, and instance. The specification is a document that formally describes what is required of a JVM implementation. Having a single specification ensures all implementations are interoperable. A JVM implementation is a computer program that meets the requirements of the JVM specification. An instance of a JVM is an implementation running in a process that executes a computer program compiled into Java bytecode.

Java Runtime Environment (JRE) is a software package that contains what is required to run a Java program. It includes a Java Virtual Machine implementation together with an implementation of the Java Class Library. The Oracle Corporation, which owns the Java trademark, distributes a Java Runtime environment with their Java Virtual Machine called HotSpot.

Java Development Kit (JDK) is a superset of a JRE and contains tools for Java programmers, e.g. a javaccompiler. The Java Development Kit is provided free of charge either by Oracle Corporation directly, or by the OpenJDK open source project, which is governed by Oracle.

繼續閱讀