Java為什么會引入及如何使用Unsafe


綜述

sun.misc.Unsafe至少從2004年Java1.4開始就存在於Java中了。在Java9中,為了提高JVM的可維護性,Unsafe和許多其他的東西一起都被作為內部使用類隱藏起來了。但是究竟是什么取代Unsafe不得而知,個人推測會有不止一樣來取代它,那么問題來了,到底為什么要使用Unsafe?

做一些Java語言不允許但是又十分有用的事情

很多低級語言中可用的技巧在Java中都是不被允許的。對大多數開發者而言這是件好事,既可以拯救你,也可以拯救你的同事們。同樣也使得導入開源代碼更容易了,因為你能掌握它們可以造成的最大的災難上限。或者至少明確你可以不小心失誤的界限。如果你嘗試地足夠努力,你也能造成損害。

那你可能會奇怪,為什么還要去嘗試呢?當建立庫時,Unsafe中很多(但不是所有)方法都很有用,且有些情況下,除了使用JNI,沒有其他方法做同樣的事情,即使它可能會更加危險同時也會失去Java的“一次編譯,永久運行”的跨平台特性。

對象的反序列化

當使用框架反序列化或者構建對象時,會假設從已存在的對象中重建,你期望使用反射來調用類的設置函數,或者更准確一點是能直接設置內部字段甚至是final字段的函數。問題是你想創建一個對象的實例,但你實際上又不需要構造函數,因為它可能會使問題更加困難而且會有副作用。

package com.jvm.study.unsafe;

import java.io.Serializable;

public class A implements Serializable {
    private final int num;

    public A(int num) {
        System.out.println("Hello Mum");
        this.num = num;
    }

    public int getNum() {
        return num;
    }
}

在這個類中,應該能夠重建和設置final字段,但如果你不得不調用構造函數時,它就可能做一些和反序列化無關的事情。有了這些原因,很多庫使用Unsafe創建實例而不是調用構造函數。

    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            return (Unsafe) f.get(null);
        } catch (Exception e) {
            return null;
        }
    }

    public static void test2() throws InstantiationException {
        Unsafe unsafe = getUnsafe();
        Class aClass = A.class;
        A a = (A) unsafe.allocateInstance(aClass);
        System.out.println("test2():"+a.getNum());   //test2():0
    }

調用allocateInstance函數避免了在我們不需要構造函數的時候卻調用它。

線程安全的直接獲取內存

Unsafe的另外一個用途是線程安全的獲取非堆內存。ByteBuffer函數也能使你安全的獲取非堆內存或是DirectMemory,但它不會提供任何線程安全的操作。你在進程間共享數據時使用Unsafe尤其有用。

package com.jvm.study.unsafe;

import sun.misc.Unsafe;
import sun.nio.ch.DirectBuffer;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class PingPongMapMain {
    public static void main(String[] args) throws IOException {
        //test("odd");
    }
    public static void test(String... args) throws IOException {
        boolean odd;
        switch (args.length < 1 ? "usage" : args[0].toLowerCase()) {
        case "odd":
            odd = true;
            break;
        case "even":
            odd = false;
            break;
        default:
            System.err.println("Usage: java PingPongMain [odd|even]");
            return;
        }
        int runs = 10000000;
        long start = 0;
        System.out.println("Waiting for the other odd/even");
        File counters = new File(System.getProperty("java.io.tmpdir"), "counters.deleteme");
        counters.deleteOnExit();

        try (FileChannel fc = new RandomAccessFile(counters, "rw").getChannel()) {
            MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
            long address = ((DirectBuffer) mbb).address();
            for (int i = -1; i < runs; i++) {
                for (;;) {
                    long value = UNSAFE.getLongVolatile(null, address);
                    boolean isOdd = (value & 1) != 0;
                    if (isOdd != odd)
                        // wait for the other side.
                        continue;
                    // make the change atomic, just in case there is more than
                    // one odd/even process
                    if (UNSAFE.compareAndSwapLong(null, address, value, value + 1))
                        break;
                }
                if (i == 0) {
                    System.out.println("Started");
                    start = System.nanoTime();
                }
            }
        }
        System.out.printf("... Finished, average ping/pong took %,d ns%n", (System.nanoTime() - start) / runs);
    }

    static final Unsafe UNSAFE;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }
}

當你分別在兩個程序,一個輸入odd一個輸入even,中運行時,可以看到兩個進程都是通過持久化共享內存交換數據的。

在每個程序中,將相同的磁盤緩存映射到進程中。內存中實際上只有一份文件的副本存在。這意味着內存可以共享,前提是你使用線程安全的操作,比如volatile變量和CAS操作。(譯注:CAS Compare and Swap 無鎖算法)

在兩個進程之間有83ns的往返時間。當考慮到System V IPC(進程間通信)大約需要2500ns,而且用IPC volatile替代persisted內存,算是相當快的了。

Unsafe適合在工作中使用嗎?

個人不建議直接使用Unsafe。它遠比原生的Java開發所需要的測試多。基於這個原因建議還是使用經過測試的庫。如果你只是想自己用Unsafe,建議你最好在一個獨立的類庫中進行全面的測試。這限制了Unsafe在你的應用程序中的使用方式,但會給你一個更安全的Unsafe。

總結

Unsafe在Java中是很有趣的一個存在,你可以一個人在家里隨便玩玩。它也有一些工作的應用程序特別是在寫底層庫的時候,但總的來說,使用經過測試的Unsafe庫比直接用要好。

使用Unsafe操作內存中的Java類和對象

讓我們開始展示內存中Java類和對象結構

你可曾好奇過Java內存管理核心構件?你是否問過自己某些奇怪的問題,比如:

  • 一個類在內存中占據多少空間?
  • 我的對象在內存中消耗了多少空間?
  • 對象的屬性在內存中是如何被布局的?

如果這些問題聽起來很熟悉,那么你就想到了點子上。對於像我們這樣的在RebelLabs的Java極客來說,這些難解的謎題已經在我們腦海中纏繞了很長時間:如果你對探究類檢測器感興趣,想知道如何布局讓所有的類更容易地從內存中取到指定變量,或是想在系統運行時侵入內存中的這些字段。這就意味着你能切實改變內存中的數據甚至是代碼!

其它可能勾起你興趣的知識點有,“堆外緩存”和“高性能序列化”的實現。這是一對構建在對象緩存結構上很好的實例,揭示了獲取類和實例內存地址的方法,緩存中類和實例的布局以及關於對象成員變量布局的詳細解釋。我們希望盡可能簡單地闡釋這些內容,但是盡管如此這篇文章並不適合Java初學者,它要求具備對Java編程原理有一定的了解。

注意:下面關於類和對象的布局所寫的內容特指Java SE 7,所以不推薦使用者想當然地認為這些適用於過去或將來的Java版本。方便起見,我們在GitHub項目上發布了這篇文章的示例代碼,可以在這里找到 https://github.com/serkan-ozal/ocean-of-memories/tree/master/src/main/java/com/zeroturnaround/rebellabs/oceanofmemories/article1

在Java中最直接的內存操作方法是什么?

Java最初被設計為一種安全的受控環境。盡管如此,Java HotSpot還是包含了一個“后門”,提供了一些可以直接操控內存和線程的低層次操作。這個后門類——sun.misc.Unsafe——被JDK廣泛用於自己的包中,如java.nio和java.util.concurrent。但是絲毫不建議在生產環境中使用這個后門。因為這個API十分不安全、不輕便、而且不穩定。這個不安全的類提供了一個觀察HotSpot JVM內部結構並且可以對其進行修改。有時它可以被用來在不適用C++調試的情況下學習虛擬機內部結構,有時也可以被拿來做性能監控和開發工具。

為何變得不安全

sun.misc.Unsafe這個類是如此地不安全,以至於JDK開發者增加了很多特殊限制來訪問它。它的構造器是私有的,工廠方法getUnsafe()的調用器只能被Bootloader加載。如你在下面代碼片段的第8行所見,這個家伙甚至沒有被任何類加載器加載,所以它的類加載器是null。它會拋出SecurityException 異常來阻止侵入者。

public final class Unsafe {
   ...
   private Unsafe() {}
   private static final Unsafe theUnsafe = new Unsafe();
   ...
   public static Unsafe getUnsafe() {
      Class cc = sun.reflect.Reflection.getCallerClass(2);
      if (cc.getClassLoader() != null)
          throw new SecurityException("Unsafe");
      return theUnsafe;
   }
   ...
}

幸運的是這里有一個Unsafe的變量可以被用來取得Unsafe的實例。我們可以輕松地編寫一個復制方法通過反射來實現,如下所示:
http://highlyscalable.wordpress.com/2012/02/02/direct-memory-access-in-java/

public static Unsafe getUnsafe() {
   try {
           Field f = Unsafe.class.getDeclaredField("theUnsafe");
           f.setAccessible(true);
           return (Unsafe)f.get(null);
   } catch (Exception e) {
       /* ... */
   }
}

Unsafe一些有用的特性

    1. 虛擬機“集約化”(VM intrinsification):如用於無鎖Hash表中的CAS(比較和交換)。再比如compareAndSwapInt這個方法用JNI調用,包含了對CAS有特殊引導的本地代碼。在這里你能讀到更多關於CAS的信息:http://en.wikipedia.org/wiki/Compare-and-swap
    2. 主機虛擬機(譯注:主機虛擬機主要用來管理其他虛擬機。而虛擬平台我們看到只有guest VM)的sun.misc.Unsafe功能能夠被用於未初始化的對象分配內存(用allocateInstance方法),然后將構造器調用解釋為其他方法的調用。
    3. 你可以從本地內存地址中追蹤到這些數據。使用java.lang.Unsafe類獲取內存地址是可能的。而且可以通過unsafe方法直接操作這些變量!
    4. 使用allocateMemory方法,內存可以被分配到堆外。例如當allocateDirect方法被調用時DirectByteBuffer構造器內部會使用allocateMemory。
    5. arrayBaseOffset和arrayIndexScale方法可以被用於開發arraylets,一種用來將大數組分解為小對象、限制掃描的實時消耗或者在大對象上做更新和移動。

 

普通對象指針和壓縮普通對象指針

堆中的Java對象使用普通對象指針(OOP)來表示的。OOP是指向Java堆內部某一內存地址的管理指針,在JVM處理器的虛地址空間中是一塊單獨的連續地址區域。

OOP通常和機器指針大小相同,在一個LP64的系統中就是64比特。在一個ILP32系統中,堆最大尺寸比40億字節稍微少一點,足以滿足大多數應用了。

譯注:32位環境涉及”ILP32″數據模型,是因為C數據類型為32位的int、long、指針。而64位環境使用不同的數據模型,此時的long和指針已為64位,故稱作”LP64″數據模型。

Java堆中的管理指針指向8字節對齊對象(https://wikis.oracle.com/display/HotSpotInternals/CompressedOops )。在大多數情況下,壓縮普通對象指針代表了JVM中從64比特的堆基地址偏移32比特的對象管理指針。因為它們都是對象偏移而不是字節偏移,所以可以用於尋址40億對象或者320億字節的堆大小。使用時,必須乘以8並且加上Java堆的基地址去定位對象的位置。使用壓縮普通對象指針的對象大小和ILP32系統差不多。

在Java v6u23及以后的版本中,支持並且默認啟用壓縮普通對象指針。在Java v7中,當沒有指定”-Xmx”時,64比特
JVM處理器默認使用壓縮普通對象指針;指定”-Xmx”時小於320億字節。在6u23版本之前的JDK 6,在Java命令中使用”-XX:+UseCompressedOops”標志啟用這一特性(http://docs.oracle.com/javase/7/docs/technotes/guides/vm/performance-enhancements-7.html )。

在我們的示例中,通過“-XX:-UseCompressedOops” 關閉了壓縮普通對象指針功能,所以64bit的指針大小就是8字節。

關於示例類的一些事

在這篇文章中,我們將使用一個示例類(SampleClass)展示對象地址恢復、列出字段布局等。這是一個簡單類,包含了三個基本數據類型並且繼承了SampleBaseClass,用來展示繼承的內存布局。示例類的定義如下,示例代碼可以在GitHub上找到:

package com.jvm.study.unsafe;

public class SampleBaseClass {
    protected short s = 20;
}
package com.jvm.study.unsafe;

public final class SampleClass extends SampleBaseClass {

    private final static byte b = 100;

    private int i = 5;
    private long l = 10;

    public SampleClass() {

    }

    public SampleClass(int i, long l) {
        this.i = i;
        this.l = l;
    }

    public int getI() {
        return i;
    }

    public void setI(int i) {
        this.i = i;
    }

    public long getL() {
        return l;
    }

    public void setL(long l) {
        this.l = l;
    }

    public static byte getB() {
        return b;
    }
}
 

要得到Java類的內存地址沒有簡便方法。為了得到地址,必須使用一些技巧並且做一些犧牲!本文會介紹兩種獲得Java類內存地址的辦法。

方法一

在JVM中,每個對象都一個指向類的指針。但是只指向具體類,不支持接口或抽象類。如果我們得到一個對象的內存地址,就可以很容易地找到類的地址。這種方法對於那些可以創建實例的類來說非常有用。但是接口或抽象類不能使用這種方法。http://hg.openjdk.java.net/jdk7/hotspot/hotspot/file/9b0ca45cd756/src/share/vm/oops/oop.hpp

For 32 bit JVM:
	_mark	: 4 byte constant
	_klass	: 4 byte pointer to class 

For 64 bit JVM:
	_mark	: 8 byte constant
	_klass	: 8 byte pointer to class

For 64 bit JVM with compressed-oops:
	_mark	: 8 byte constant
	_klass	: 4 byte pointer to class

內存中對象的第二個字段(對32位JVM偏移是4,64位JVM偏移是8)指向了內存中的類定義。你可以使用“sun.misc.Unsafe” 類得到此偏移的內存值。這里用到的是在上一篇中提到的SampleClass

For 32 bit JVM:
	SampleClass sampleClassObject = new SampleClass();
	int addressOfSampleClass = unsafe.getInt(sampleClassObject, 4L);

For 64 bit JVM:
	SampleClass sampleClassObject = new SampleClass();
	long addressOfSampleClass = unsafe.getLong(sampleClassObject, 8L);

For 64 bit JVM with compressed-oops:
	SampleClass sampleClassObject = new SampleClass();
	long addressOfSampleClass = unsafe.getInt(sampleClassObject, 8L);

方法2

使用這種方法,可以得到任何類的內存地址(包括接口、注解、抽象類和枚舉)。Java7中類定義的內存地址結構如下:32位JVM的地址偏移從第4到80字節,64位JVM的地址偏移從8字節到160字節,壓縮普通對象指針的地址偏移從第4到84字節。

沒有預先定義好的偏移,但是在類文件解析器中作為“隱藏”字段給出了注釋(這里實際上有3個字段:classarrayClassresolvedConstructor)。因為在java.lang.Class中有18個非靜態引用字段,他們只是恰好表示了這段偏移。

更多信息可以參見ClassFileParser::java_lang_Class_fix_pre() 和JavaClasses::check_offsets()。文檔地址:http://hg.openjdk.java.net/jdk7/hotspot/hotspot/file/9b0ca45cd756/src/share/vm/classfile/.

獲取內存地址的示例代碼如下:

For 32 bit JVM:
	int addressOfSampleClass = unsafe.getInt(SampleClass.class, 80L);

For 64 bit JVM:
	long addressOfSampleClass = unsafe.getLong(SampleClass.class, 160L);

For 64 bit JVM with compressed-oops:
	long addressOfSampleClass = unsafe.getInt(SampleClass.class, 84L);

 

怎樣才能獲得對象內存地址?

獲取對象內存地址要比獲取類內存地址更加需要技巧。我們需要使用長度和 java.lang.Object 類型的輔助數組(長度為1)獲得對象的內存地址。

下面是獲取對象內存地址的詳細步驟:

1、將目標對象設為輔助數組的第一個元素(也是唯一的元素)。由於這是一個復雜類型元素(不是基本數據類型),它的地址存儲在數組的第一個元素。

2、然后,獲取輔助數組的基本偏移量。數組的基本偏移量是指數組對象的起始地址與數組第一個元素之間的偏移量。

3、確定JVM的地址空間:

  • 如果是32位JVM,可以通過 sun.misc.Unsafe 類得到數組對象的內存地址(address_of_array)與數組基本偏移量(base_offset_of_array)相加的整型結果。這個4字節整型數值就是目標對象的內存地址。
  • 如果是64位JVM,可以通過 sun.misc.Unsafe 類得到數組對象的內存地址(address_of_array)與數組基本偏移量(base_offset_of_array)相加的長整型值結果。這個8字節長整型數值就是目標對象的內存地址。

32位JVM

1
2
3
4
Object helperArray[]    = new Object[ 1 ];
helperArray[ 0 ]      = targetObject;
long baseOffset     = unsafe.arrayBaseOffset(Object[]. class );
int addressOfObject = unsafe.getInt(helperArray, baseOffset);

64位JVM

1
2
3
4
Object helperArray[]    = new Object[ 1 ];
helperArray[ 0 ]      = targetObject;
long baseOffset     = unsafe.arrayBaseOffset(Object[]. class );
long addressOfObject    = unsafe.getLong(helperArray, baseOffset);

可以認為這段代碼中的 targetObject 是上文中 SampleClass 的某個實例。但請記住,這段代碼適用於任何類的任何實例。

類的內存布局

32位JVM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[header                ] 4  byte
[klass pointer         ] 4  byte (pointer)
[C++ vtbl ptr          ] 4  byte (pointer)
[layout_helper         ] 4  byte
[super check offset    ] 4  byte
[name                  ] 4  byte (pointer)
[secondary super cache ] 4  byte (pointer)
[secondary supers      ] 4  byte (pointer)
[primary supers        ] 32 byte (8 length array of pointer)
[java mirror           ] 4  byte (pointer)
[super                 ] 4  byte (pointer)
[first subklass        ] 4  byte (pointer)
[next sibling          ] 4  byte (pointer)
[modifier flags        ] 4  byte
  4  byte

64位JVM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[header                ] 8  byte
[klass pointer         ] 8  byte (4 byte for compressed-oops)
[C++ vtbl ptr          ] 8  byte (4 byte for compressed-oops)
[layout_helper         ] 4  byte
[super check offset    ] 4  byte
[name                  ] 8  byte (4 byte for compressed-oops)
[secondary super cache ] 8  byte (4 byte for compressed-oops)
[secondary supers      ] 8  byte (4 byte for compressed-oops)
[primary supers        ] 64 byte (32 byte for compressed-oops)
                                      {8 length array of pointer}
[java mirror           ] 8  byte (4 byte for compressed-oops)
[super                ] 8  byte (4 byte for compressed-oops)
[first subklass         ] 8  byte (4 byte for compressed-oops)
[next sibling          ] 8  byte (4 byte for compressed-oops)
[modifier flags        ] 4  byte
  4  byte

上面內容可參見源碼 klass.hpp :http://hg.openjdk.java.net/jdk7/hotspot/hotspot/file/9b0ca45cd756/src/share/vm/oops/klass.hpp

下圖展示了 SampleClass 在32位JVM中的內存布局,列出了自起始地址起的前128個字節:

這是 SampleBaseClass 在32位JVM中的內存布局,列出了自起始地址起的前128個字節:

我們只對重要的字段進行說明,數字的顏色對應着相應字段的顏色。

header 是一個常量:0×00000001。

klass pointer 指向 java.lang.Class 類在內存中的定義(即 java.lang.Class 類的內存地址,這兩個類的klass pointer字段都指向0x38970v8a8),表明這是一個類的內存結構。

C++ vtbl ptr 指向其對應類的虛函數表。虛函數表用於實現運行時虛函數調用的多態性。

layout helper 用於記錄該實例的淺尺寸(Shallow size)。淺尺寸是根據JVM的字段對齊機制計算出來的。在我們的環境中,對象按8字節對齊。

super 指向其父類的定義,在我們的演示代碼中,SampleBaseClass 是 SampleClass 的父類。在 SampleClass 類的內存布局中,可以看到SampleBaseClass 的內存地址為0x34104b70。而在 SampleBaseClass 類的內存布局中, 父類的內存地址為0×38970000,這是  java.lang.Object 類的地址。因為在Java中,每一個類都是 Object 類的子類。

modifier flags 即類的修飾符標志位。在Java中類的修飾符包括:public、protected、private、abstract、static、final以及strictfp。modifier flags 字段的值是對目標類的所有修飾符進行按位或運算得到的。在我們的示例代碼中,SampleClass 類的修飾符有 public 和 final 。因此它的 modifier flags 字段的值為“0×00000001 | 0×00000010 =  0×00000011 ”。而 SampleBaseClass 類的修飾符只有 public,所以它的modifier flags 字段的值為0×00000001 。各修飾符的對應取值如下所示:

字段布局和對齊

和C/C++不同,Java沒有 sizeOf 運算符計算基本數據類型類型或對象所占用的內存空間,sizeOf 運算符在IO操作和內存管理中非常實用。事實上,由於基本數據類型的大小在語言規范中預先定義,Java中也不會出現指針拷貝內存和指針運算(因為沒有指針)。因此,sizeOf 運算符並沒有存在的必要。

有兩種方法能夠確定一個類及其屬性共占用了多少內存空間。分別是淺尺寸(shallow size)和深尺寸(deep size)。淺尺寸就是對象本身各個字段所占用內存的大小,不包含該類中所引用的其他對象;於是引入了深尺寸概念,深尺寸在淺尺寸的基礎上增加了該類引用的其他對象的淺尺寸。

Sun Java虛擬機規定:除數組外的所有對象都包含一個長度為雙字(two-word,一個字由兩個字節組成)的 header,一個長度為單字(one-word)flags,以及一個指向對應類引用的單字(one-word)的字段。當我們用 new Object() 方法創建一個對象時,堆上就會為其分配8個字節的內存空間。

對於一個繼承了 Object 的類來說,情況會變得復雜而有趣。在這8個字節后面,類的各個屬性在堆內存上會按照一定的規則對齊,但並不按照他們的聲明順序進行對齊。

基本數據類型按照下列順序進行對齊:

  • double、long
  • int、float
  • short、char
  • boolean、byte

接下來,該類中所引用其他類的對象也會在堆上進行對齊。JVM會把對象的大小調整為8字節的倍數(http://www.codeinstructions.com/2008/12/java-objects-memory-structure.html)。

請看下面的示例:

1
2
3
class BooleanClass {
byte a;
}

這里會自動填充7個字節,整個對象的大小被擴大到16個字節。

1
2
3
Headers (include flags and ref to class ) : 8 bytes
value of byte : 1 byte
padding : 7 bytes

關於OOP的更多信息

有關OOP的一些基本信息已經在 OOP 和 壓縮OOP 章節介紹過了。我們假定你已經對JVM中 OOP 相關的術語有一定的了解了,下面就讓我們對它作進一步的了解。

OOP由兩個機器字長度的字段組成(32位JVM上機器字長為4字節,64位JVM上機器字長為8字節),這兩個字段分別是 Mark 和 Klass。這兩個字段出現在該實例的所有成員字段之前(譯注:這兩個字段應該就是對應上面字段的布局和對齊章節中 headers 所占用的8個字節)。但對於數組對象來說,在這兩個字段之前會有一個額外的字段用於記錄數組的長度。Mark 字段用於垃圾回收(在mark-and-sweep回收算法中使用),Klass 字段用於指向其對應類的元數據。所有的基本數據類型字段和引用字段都排在OOP(Mark 和 Klass 字段)的后面,包括引用其他對象的字段,甚至是引用OOP的字段( http://www.infoq.com/articles/Introduction-to-HotSpot )。

KlassOOPs

Klass 字段是一個指向對應類的元數據(包括字段的定義以及類似C++的虛函數表)的指針。每一個實例都攜帶一份類的元數據是一種非常低效的方式,KlassOOPs能讓所有對象共享同一份元數據從而減少不必要的開銷。需要注意的是 KlassOOP 和類加載器所產生的 Class object(java.lang.class 類型的對象)並不相同。下面是兩者之間的區別:

  • Class objects 只是普通的Java對象。Class objects和其他的Java對象一樣可以用OOP(InstanceOOPs)表示。其表現行為也與其他的Java對象相同,還可以存放到Java變量里。
  • KlassOOPs 是類的元數據在JVM中的表現形式,例如類的虛函數表就存放在 KlassOOPs 中。由於 KlassOOPs 生存在堆的永久區里(Permgen space),因此在Java代碼中無法直接獲得 KlassOOPs 的引用。你也可以簡單地認為 KlassOOP 是對應類的 Class object 在虛擬機級別上的鏡像。

MarkOOPs

Mark字段指向一個維護着OOP相關管理信息的數據結構。在32位JVM中,mark字段的數據結構為(http://hg.openjdk.java.net/jdk7/hotspot/hotspot/file/9b0ca45cd756/src/share/vm/oops/markOop.hpp for more details ):

  • 哈希值(25 bits):記錄着該對象的 HashCode() 方法的返回值。
  • 年齡(4 bits):對象的年齡(即這個對象所經歷過的垃圾回收的次數)。
  • 偏向鎖(1 bit)+ 鎖(2 bits):用於表示該對象的同步狀態。

Java 5引入了一種全新的對象同步方式,叫做偏向鎖(Java 6中默認使用偏向鎖 Biased-Lock)。經過觀察發現,在多數情況下對象在運行時往往只被一個線程鎖住,因此引入了偏向鎖的概念。處於偏向鎖狀態中的對象會優先朝向第一個鎖住它的線程,這個線程也會獲得到更好的鎖性能。Mark字段中會記錄獲取到該對象偏向鎖的線程:

  • Java線程指針:23 bits
  • 歷元時間戳(Epoch):2 bits
  • 年齡:4 bits
  • 偏向鎖狀態:1 bit
  • 鎖狀態:2 bits

如果另一個線程嘗試鎖定該對象,偏向鎖就會失效(無法重新獲取)。這樣,所有的線程都必須通過顯式調用lock、unlock方法來鎖定和解鎖對象。

下面是對象可能出現的狀態:

  • 未鎖定狀態(Unlocked)
  • 偏向鎖狀態(Biased)
  • 輕量級鎖定(Lightweight Locked)
  • 重量級鎖定(Heavyweight Locked)
  • 標記狀態(Marked,僅會在垃圾回收期間出現)

32位JVM

1
2
3
[mark]         ] 8  byte
[klass pointer ] 8  byte (pointer)
[fields        ] values of all fields including fields from super classe

64位JVM

1
2
3
[mark]         ] 8  byte
[klass pointer ] 8  byte (4 byte for compressed-oops)
[fields        ] values of all fields including fields from super classes

請參考:http://hg.openjdk.java.net/jdk7/hotspot/hotspot/file/9b0ca45cd756/src/share/vm/oops/oop.hpp

下面我們來聊一聊深尺寸的計算,並考慮繼承關系的影響。我們繼續在32位的JVM上以 SampleClass 和 SampleBaseClass 為例。下面是 SampleClass 對象的內存布局。請再仔細看看這兩個類的代碼和各個字段,以便於更好的理解后續內容。


mark字段是內存布局中的第一個字(0x69e34e01),字段中包含有該對象的哈希值,還有鎖狀態、對象年齡之類的標志位。

klass 字段指向 SampleClass 類的定義,即0x34104cc0。

字段 s 的值是20(0×0014),s是父類 SampleBaseClass 中的字段。父類字段排在內存布局的最前面,並且不會和子類的字段交叉排列。內存布局中的父類字段會以完整的系統字長結束。在字段的結尾,如果字長為4字節會按4字節進行自動補齊;如果字長為8字節,也會按4字節進行自動補齊。兩個填充字節(0×0000)可用於填充長度為4字節(一個字長)的空隙。

字段 i 的值為5(0×00000005)。字段 l 排在字段 i 的后面,它的值為10(0x000000000000000a)。

正如上文提到的類屬性順序:首先是long和double,其次是int和float,然后是char和short,再然后是byte和boolean,最后是引用類型。屬性按照各自的粒度進行對齊。當子類的第一個字段是double或者long類型,而父類並不滿足8字節對齊,JVM為了填補這個空隙會破例嘗試在子類的最前面擺放一個相對較短的字段。JVM會依次嘗試擺放int、short和byte類型的字段,最后嘗試引用類型的字段。

因此整型字段排在長整型字段的前面,字段 i 排在字段 l 的前面。

本文到此結束,我們還會帶來更多的驚喜!

我們希望你喜歡本文對Java中非常酷的底層機制所做的深入探討,希望能從中有所收獲。現在你已經知道了如何利用不安全的后門直接訪問內存和線程並完成一些底層操作,能夠通過一個類的對象輕松的獲取到這個類和這個實例的內存地址,或者是根據預定義的偏移量計算出類的內存地址。

現在你還知道如何了解一個類的內存布局,知道如何通過字段的完整對齊來最小化內存的占用。本文通篇都以 SampleClass 類為例,在保持示例一致的同時也有助於讀者更好的理解本文的內容。我們還詳細介紹了所有32位JVM和64位JVM相關的例子,希望本文能覆蓋到更多的讀者。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM