聊聊Java對象在內存中的大小

聊聊Java對象在內存中的大小

本文討論的Java對象在內存中的大小指的是在堆(Heap)中的大小;未特殊說明,提到JVM的地方都指的是:Java HotSpot(TM) 64-Bit Server VM,版本:1.8.0_131html

  • Java中Object的組成:java

    Object = Header + Primitive Fields + Reference Fields + Alignment & Padding`git

    1. Header由兩部分組成:標記部分(Mark Word)和原始對象引用(Klass Pointer/Object Original Pointer)- mark word & klass pointergithub

      • 標記部分的大小是一個word size(64-bit JVM上是8 bytes,32-bit JVM上是4 bytes),包括了該對象的identity hash code和一些標記(好比鎖和年代信息)。
      • 原始對象引用在32-bit JVM上的大小是4 bytes,在64-bit JVM上能夠是4 bytes,也能夠是8 bytes,由JVM參數「是否壓縮原始對象」決定,在HotSpot中是UseCompressedOops參數(jdk1.8 和jdk1.9默認是開啓的)。
    2. Primitive Fields && Reference Fields數組

      類型 大小
      Object Reference word size
      byte 1 byte
      boolean 1 byte
      char 2 bytes
      short 2 bytes
      int 4 bytes
      float 4 bytes
      double 8 bytes
      long 8 bytes
    3. 對齊(Alignment)和補齊(Padding)jvm

      • 對齊,任何對象都是以8 bytes的粒度來對齊的

        怎麼理解這句話呢?請看一個例子,new Object()產生的對象的大小是多少呢?12 bytes的header,但對齊必須是8的倍數,還有4 bytes的alignment,因此對象的大小是16 bytes.ide

      • 補齊,補齊的粒度是4 byteswordpress

      • 能夠簡單理解爲,JVM分配內存空間一次最少分配8 bytes,對象中字段對齊的最小粒度爲4 bytes
  • 準備工做oop

    本文使用Maven管理Jar包,源碼在這裏佈局

    1. pom.xml中引入JOL(Java Object Layout, 使用實例 )依賴,用於展現對象在Heap中的分佈(layout):

      <dependency>
          <groupId>org.openjdk.jol</groupId>
          <artifactId>jol-core</artifactId>
          <version>0.9</version>
      </dependency>
    2. 第一個測試:

      public static void main(String[] args) {
          System.out.println(VM.current().details());
      }

      執行後,會輸出:

      # Running 64-bit HotSpot VM.
      # Using compressed oop with 3-bit shift.
      # Using compressed klass with 3-bit shift.
      # WARNING | Compressed references base/shifts are guessed by the experiment!
      # WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
      # WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
      # Objects are 8 bytes aligned.  // 以 8 bytes的粒度對齊
      # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]    // 分別對應[Oop(Object Original Pointer), boolean, byte, char, short, int, float, long, double]的大小
      # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]    // 數組中元素的大小,分別對應的是[Oop(Object Original Pointer), boolean, byte, char, short, int, float, long, double]
  • 對象在Heap中的分佈遵循的規則:

    1. 重排序, JVM在Heap中給對象佈局時,會對field進行重排序,以節省空間。

      例-1,對於類:

      public class Reorder {
      
          private byte a;
      
          private int b;
      
          private boolean c;
          
          private float d;
      
          private Object e;
          
          public static void main(String[] args) {
              System.out.println(ClassLayout.parseClass(Reorder.class).toPrintable());
          }
      }

      若是沒有重排序,對象的分佈會是這個樣子的:

      objectsize.Reorder object internals:
       OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
            0    12           (object header)                           N/A
           12     1      byte Reorder.a                                 N/A
           13     3           (alignment/padding gap)                  
           16     4       int Reorder.b                                 N/A
           20     1   boolean Reorder.c                                 N/A
           21     3           (alignment/padding gap)                  
           24     4     float Reorder.d                                 N/A
           28     2      char Reorder.e                                 N/A
           30     2           (loss due to the next object alignment)
      Instance size: 32 bytes
      Space losses: 6 bytes internal + 2 bytes external = 8 bytes total

      對象實例總大小:32 bytes,空間損失:8 bytes。

      而實際是(運行main方法會看到結果):

      objectsize.Reorder object internals:
       OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
            0    12           (object header)                           N/A
           12     4       int Reorder.b                                 N/A
           16     4     float Reorder.d                                 N/A
           20     2      char Reorder.e                                 N/A
           22     1      byte Reorder.a                                 N/A
           23     1   boolean Reorder.c                                 N/A
      Instance size: 24 bytes
      Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

      對象實例總大小:24 bytes,空間損失:0 bytes。

    2. 爲了不空間浪費,通常狀況下,field分配的優先依次順序是:double > long > int > float > char > short > byte > boolean > object reference
      注意到了沒,這裏有個基本的原則是:儘量先分配佔用空間大的類型(除了object reference)。這裏的儘量有兩層含義:

      • 在同等優先級狀況下,按這個順序分配。 例-2

        public class Order {
            
            private int ignoreMeTentatively;
            
            private byte a;
            
            private boolean b;
            
            private char c;
            
            private short d;
            
            private int e;
            
            private float f;
            
            private double g;
            
            private long h;
            
            private Object i;
            
            public static void main(String[] args) {
                System.out.println(ClassLayout.parseClass(Order.class).toPrintable());
            }
        }

        這個類的實例在內存中分佈是:

        objectsize.Reorder object internals:
         OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
              0    12           (object header)                           N/A
             12     4       int Reorder.b                                 N/A
             16     4     float Reorder.d                                 N/A
             20     2      char Reorder.e                                 N/A
             22     1      byte Reorder.a                                 N/A
             23     1   boolean Reorder.c                                 N/A
        Instance size: 24 bytes
        Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

        請先忽略ignoreMeTentatively字段,能夠驗證類型分配的順序。

      • 在考慮到補齊(Padding)的狀況下,排在後面的類型有可能比排在前面的優先級更高。

        回過頭來看例-1例-2,會發現header後的字一個field(offset 12)都是int類型的。爲何呢?
        這就是AlignmentPadding共同做用的結果。

        JVM每次最少分配8 bytes的空間,而header的大小是12。
        也就是說,已經分配了16 bytes的空間了,若是嚴格按照前面說的那個順序,最早分配一個double類型的field,就須要在這以前先分配4 bytes的空間來補齊,也就這4 bytes的空間就白白浪費了。
        這中狀況下,<=Padding Size(4 bytes)的類型的優先級就高於大小>Padding Size的類型了。
        而在全部大小<=Padding Size的類型中,int的優先級又是最高的,因此header後的第一個field是int

        爲了進一步理解,再來看個例子,例-3

        public class Padding {
              
            private char a;
              
            private boolean b;
              
            private long c;
              
            private Object d;
          
            public static void main(String[] args) {
                  System.out.println(ClassLayout.parseClass(Padding.class).toPrintable());
           }
        }

        這個類的實例在內存中分佈是:

        objectsize.Padding object internals:
         OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
              0    12                    (object header)                           N/A
             12     2               char Padding.a                                 N/A
             14     1            boolean Padding.b                                 N/A
             15     1                    (alignment/padding gap)                  
             16     8               long Padding.c                                 N/A
             24     4   java.lang.Object Padding.d                                 N/A
             28     4                    (loss due to the next object alignment)
        Instance size: 32 bytes
        Space losses: 1 bytes internal + 4 bytes external = 5 bytes total

        能夠看到header後的4個bytes空間分配狀況,在全部大小<=Padding Size的類型中,char的優先級最高,其次是boolean
        這兩個加起來只有3 bytes(<Padding Size),而已經沒有1 byte大小的field了,因此只能分配1 byte的Padding。
        接下來,JVM再分配一個8 bytes大小的空間,很明顯空間足夠的狀況下,long的優先級最高,也正好用完這8 bytes的空間。
        而後,JVM繼續分配一個8 bytes大小的空間,最後一個類型object reference(這裏是Object)了,在開啓UseCompressedOops的狀況下,使用4 bytes的空間,還有4 bytes的空間只能用來對齊了。

    3. 子類和父類的field永遠不會混合在一塊兒,而且父類的field分配完以後纔會給子類的field分配空間。

      例-4

      public class SuperA {
          
          long a;
          
          private int b;
          
          private float c;
              
          private char d;
              
          private short e;
      }
      
      public class SubA extends SuperA {
          
          private long d;
      
          public static void main(String[] args) {
              System.out.println(ClassLayout.parseClass(SubA.class).toPrintable());
          }
          
      }

      SubA的實例在內存中的分佈是:

      objectsize.SubA object internals:
         OFFSET  SIZE    TYPE DESCRIPTION                               VALUE
              0    12         (object header)                           N/A
             12     4     int SuperA.b                                  N/A
             16     8    long SuperA.a                                  N/A
             24     4   float SuperA.c                                  N/A
             28     2    char SuperA.d                                  N/A
             30     2   short SuperA.e                                  N/A
             32     8    long SubA.d                                    N/A
      Instance size: 40 bytes
      Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

      父類SuperA中的field所有分配完後,才分配子類SubAfield

    4. 父類的的最後一個字段與子類的第一個字段以一個Padding Size(4 bytes)來對齊。

      例-5

      public class SuperB {
          
          private byte a;
          
          private int b;
      
      }
      
      public class SubB extends SuperB {
          
          private int a;
          
          private long b;
      
          public static void main(String[] args) {
              System.out.println(ClassLayout.parseClass(SubB.class).toPrintable());
          }
      }

      SubB的實例在內存中分佈是:

      objectsize.SubB object internals:
        OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
             0    12        (object header)                           N/A
            12     4    int SuperB.b                                  N/A
            16     1   byte SuperB.a                                  N/A
            17     3        (alignment/padding gap)                  
            20     4    int SubB.a                                    N/A
            24     8   long SubB.b                                    N/A
      Instance size: 32 bytes
      Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

      從offset 16的位置開始看,父類還有最後一個字段a未分配,這時JVM分配一個8 bytes的空間,a佔用1 byte,
      還有7 bytes未使用,而這7 bytes空間沒有所有用於對齊,也就是說子類字段的分配並非從offset 24 開始的。
      實際上只用了3 bytes空間來對齊(湊夠4 bytes的Padding Size),剩下的4 bytes分配給了子類的a字段。

    5. 數組也是對象,但數組的header中包含有一個int類型的length值,又多佔了4 bytes的空間,因此數組的header大小是16 bytes。

      例-6

      public class ArrayTest {
          public static void main(String[] args) {
              System.out.println(ClassLayout.parseInstance(new boolean[1]).toPrintable());
          }
      }

      長度爲1的boolean數組的實例在內存的分佈是:

      [Z object internals:
       OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
          0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
          4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4           (object header)                           05 00 00 f8 (00000101 00000000 00000000 11111000) (-134217723)
         12     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
         16     1   boolean [Z.<elements>                             N/A
         17     7           (loss due to the next object alignment)
      Instance size: 24 bytes
      Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

      能夠看到,header佔用了16 bytes,一個boolean元素佔用了1 bytes,剩餘7 bytes用於對齊。

  • 參考資料

相關文章
相關標籤/搜索