Java 內(nèi)存模型

c ++ 與 java 很大的一點區(qū)別就是 c++ 每次創(chuàng)建對象,都要寫配對的delete/free 語句,否則會出現(xiàn)內(nèi)存泄漏或者內(nèi)存溢出。而java在強大的內(nèi)存管理機制下(gc),不用去擔心這些問題。因為gc會被那些“失去聯(lián)系”的對象清理掉。想要了解GC,就要先了解一下JVM運行內(nèi)存區(qū)域是怎么樣的。按照Java虛擬機規(guī)范 8中的描述,JVM運行內(nèi)存區(qū)域可分為Java虛擬機棧,pc寄存器,Java堆,方法區(qū),運行時常量池,本地方法棧;以及不屬于JVM運行內(nèi)存區(qū)域的直接內(nèi)存。


JVM內(nèi)存模型
  • Java虛擬機棧

線程-Java虛擬機棧-棧幀

JVM每創(chuàng)建一個線程會為之創(chuàng)建對應的java虛擬機棧,用來存儲棧幀。Java虛擬機棧是Java線程的私有內(nèi)存區(qū)域,即其它線程無法訪問到該線程內(nèi)的數(shù)據(jù)。


線程與棧幀
棧幀

棧幀由局部變量表,操作數(shù)棧,指向當前方法所屬類的運行時常量池的引用,返回地址組成。它隨著方法的調(diào)用而創(chuàng)建,隨著方法的結(jié)束而銷毀。線程每調(diào)用一個方法,就會往它的Java虛擬機棧中壓人一個棧幀。比如:

    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.say();
        System.out.println("end ...");
    }

在這個例子中,jvm先壓入main的棧幀,執(zhí)行到cat.say()的時候壓入say()的棧幀,等say()執(zhí)行完以后,say()棧幀銷毀,繼續(xù)執(zhí)行main棧幀,等main都執(zhí)行完以后,方法結(jié)束,Java虛擬機棧銷毀。

  1. 局部變量表
    一個局部變量可以保存一個int,float,byte,char,boolean,short,reference,returnAddress。兩個局部變量保存一個long或者double??梢园丫植孔兞勘砜醋鍪且粋€數(shù)組,使用索引定位,即a[0]表示第一個局部變量。我們可以在局部變量表中找到方法參數(shù),方法中局部變量,返回值對應的局部變量。通常情況下,第一個局部變量保存該方法所在的實例對象的引用。
  2. 操作數(shù)棧
    保存方法執(zhí)行過程中的操作數(shù)。比如我們運行了代碼int a = 100 + 98,代碼被編譯成匯編是這樣子的:
begin
iload_0    // push the int in local variable 0 onto the stack
iload_1    // push the int in local variable 1 onto the stack
iadd       // pop two ints, add them, push result
istore_2   // pop int, store into local variable 2
end

在這個字節(jié)碼序列里,前兩個指令iload_0和iload_1將存儲在局部變量中索引為0和1的整數(shù)壓入操作數(shù)棧中,其后iadd指令從操作數(shù)棧中彈出那兩個整數(shù)相加,再將結(jié)果壓入操作數(shù)棧。第四條指令istore_2則從操作數(shù)棧中彈出結(jié)果,并把它存儲到局部變量區(qū)索引為2的位置。圖5-10詳細表述了這個過程中局部變量和操作數(shù)棧的狀態(tài)變化,圖中沒有使用的局部變量區(qū)和操作數(shù)棧區(qū)域以空白表示。


  1. 動態(tài)鏈接
    棧幀中包含一個指向當前方法所在類型的運行時常量池的引用,以便對當前方法的代碼實現(xiàn)動態(tài)鏈接。
可能錯誤

StackOverFlowError : 棧幀調(diào)用鏈太長了。
OutOfMemoryError : 沒有足夠內(nèi)存去創(chuàng)建新的虛擬機棧。

  • pc寄存器

pc寄存器也是線程的私有內(nèi)存區(qū)域。每條Java線程都會有自己的Java虛擬機棧,也有自己的pc寄存器。pc寄存器保存當前方法的字節(jié)碼指令的地址。當CPU再又切到該線程的時候,可能根據(jù)pc寄存器的值地址調(diào)用到字節(jié)碼指令。該如果方法是native,那么pc寄存器的值為undefined。

  • 本地方法棧

本地方法棧也是線程的私有內(nèi)存區(qū)域。和Java虛擬機棧一樣,區(qū)別是本地方法棧是調(diào)用native方法的所使用的棧。各個JVM對它的實現(xiàn)不盡相同。在HotSpot的實現(xiàn)中,本地方法棧和虛擬機棧合二為一。

  • Java堆

簡介

Java堆是各個線程共享的內(nèi)存區(qū)域。java生成對象實例,會在堆上面分配內(nèi)存(棧存放引用,實際指向這里)。GC管理就是運行在堆上面的?;诂F(xiàn)在gc收集器采用分代收集算法,堆又可以分為新生代和老年代。而新生代又可以分為Eden和from survivor/to survivor區(qū)域。

錯誤

當堆內(nèi)存不足的時候會報OutOfMemoryError 的錯誤。

  • 方法區(qū)

簡介

方法區(qū)也是各個內(nèi)存共享的內(nèi)存區(qū)域。存放java的所有類的結(jié)構(gòu)信息。比如運行時常量池,字段,方法等字節(jié)碼。類的元數(shù)據(jù)是在類的加載過程階段存儲到方法區(qū)的。在加載完元數(shù)據(jù)以后還會生成一個class對象,該對象放在堆中。

實現(xiàn)

方法區(qū)是一個規(guī)范,HotSpot JDK1.7對該區(qū)域的實現(xiàn)叫做永久代,1.8的實現(xiàn)叫做元數(shù)據(jù)區(qū)。元數(shù)據(jù)區(qū)和永久代最大的不同就是元數(shù)據(jù)區(qū)的數(shù)據(jù)放在本地內(nèi)存中,而永久代則放在堆中。

  • 運行時常量池

運行時常量池也是各個線程共享的內(nèi)存區(qū)域。要理解運行時常量池,首先要了解class常量池。

class常量池
public class ClassPoolTest {
    private static String word = "hello world";
    public static void main(String[] args) {
        System.out.println(ClassPoolTest.word);

    }
}

java文件編譯成class文件后,有個constant pool的數(shù)據(jù)結(jié)構(gòu),這個數(shù)據(jù)結(jié)構(gòu)就是class常量池。如下圖:

$ javap -verbose ClassPoolTest.class
Classfile /D:/practice/target/classes/pool/ClassPoolTest.class
  Last modified 2018-8-9; size 725 bytes
  MD5 checksum ce4cb04b698854efd49f64942ae38d35
  Compiled from "ClassPoolTest.java"
public class pool.ClassPoolTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // pool/ClassPoolTest
   #2 = Utf8               pool/ClassPoolTest
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               word
   #6 = Utf8               Ljava/lang/String;
   #7 = Utf8               <clinit>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = String             #11            // hello world
  #11 = Utf8               hello world
  #12 = Fieldref           #1.#13         // pool/ClassPoolTest.word:Ljava/lang/String;
  #13 = NameAndType        #5:#6          // word:Ljava/lang/String;
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               <init>
  #17 = Methodref          #3.#18         // java/lang/Object."<init>":()V
  #18 = NameAndType        #16:#8         // "<init>":()V
  #19 = Utf8               this
  #20 = Utf8               Lpool/ClassPoolTest;
  #21 = Utf8               main
  #22 = Utf8               ([Ljava/lang/String;)V
  #23 = Utf8               org.aspectj.weaver.MethodDeclarationLineNumber
  #24 = Fieldref           #25.#27        // java/lang/System.out:Ljava/io/PrintStream;
  #25 = Class              #26            // java/lang/System
  #26 = Utf8               java/lang/System
  #27 = NameAndType        #28:#29        // out:Ljava/io/PrintStream;
  #28 = Utf8               out
  #29 = Utf8               Ljava/io/PrintStream;
  #30 = Methodref          #31.#33        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #31 = Class              #32            // java/io/PrintStream
  #32 = Utf8               java/io/PrintStream
  #33 = NameAndType        #34:#35        // println:(Ljava/lang/String;)V
  #34 = Utf8               println
  #35 = Utf8               (Ljava/lang/String;)V
  #36 = Utf8               args
  #37 = Utf8               [Ljava/lang/String;
  #38 = Utf8               SourceFile
  #39 = Utf8               ClassPoolTest.java
{
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #10                 // String hello world
         2: putstatic     #12                 // Field word:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 15: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

  public pool.ClassPoolTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #17                 // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 13: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lpool/ClassPoolTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
Error: unknown attribute
      org.aspectj.weaver.MethodDeclarationLineNumber: length = 0x8
       00 00 00 11 00 00 00 EB
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #24                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: getstatic     #12                 // Field word:Ljava/lang/String;
         6: invokevirtual #30                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         9: return
      LineNumberTable:
        line 18: 0
        line 20: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;
}
SourceFile: "ClassPoolTest.java"

class常量池里面主要放兩種類型的常量:字面量和符號引用。
字面量
比較接近于java中的常量,比如文本字符串,聲明為final的常量值等。在上面的例子中,hello world就是一個字面量。

符號引用
符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。在編譯時,java類并不知道所引用的類的實際地址,因此只能使用符號引用來代替。比如org.simple.People類引用了org.simple.Language類,在編譯時People類并不知道Language類的實際內(nèi)存地址,因此只能使用符號org.simple.Language(假設是這個,當然實際中是由類似于CONSTANT_Class_info的常量來表示的)來表示Language類的地址。在上面的例子中,word這個字段就是一個符號引用。符號引用一般包括下面三類常量:

  1. 類和接口的全限定名
  2. 字段的名稱和描述符
  3. 方法的名稱和描述符

直接引用
我們可以從上面知道可以用一組符號來描述所引用的目標,比如一個類名,但是實際上,jvm獲取一個類都是用的地址引用。我們可以把這個地址引用理解為直接引用,把這個類名理解成符號引用。
直接引用可以是:
(1)直接指向目標的指針(比如,指向“類型”【Class對象】、類變量、類方法的直接引用可能是指向方法區(qū)的指針)
(2)相對偏移量(比如,指向?qū)嵗兞俊嵗椒ǖ闹苯右枚际瞧屏浚?br> (3)一個能間接定位到目標的句柄
直接引用是和虛擬機的布局相關(guān)的,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經(jīng)被加載入內(nèi)存中了。

運行時常量池

class常量池被加載進內(nèi)存后的形態(tài),但是它會把class常量池中的符號引用改成直接引用。一個類對應著一個運行時常量池。

string常量池

每個類都有自己的運行時常量池。每個類可能會有很多字符串是相同的,為了更節(jié)省空間,java把字符串單獨抽出來放到一個string常量池中,多個運行時常量池可以共用一個字符串常量池的東西。

  • 直接內(nèi)存

不屬于java虛擬機的內(nèi)存區(qū)域。JDK1.4以后 NIO,引入一種基于通道和緩沖區(qū)的I/0方式,使用native函數(shù)分配對外內(nèi)存。然后通過一個存儲在java堆中的directbybnuffer對象對這塊內(nèi)存引用進行操作。這樣子能夠在顯著提升性能,避免在java堆和native對中來回復制數(shù)據(jù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容