哪個更快:Java堆還是本地內存

TristaMurph 8年前發布 | 6K 次閱讀 Java Java開發

使用Java的一個好處就是你可以不用親自來管理內存的分配和釋放。當你用 new 關鍵字來實例化一個對象時,它所需的內存會自動的在Java堆中分配。堆會被垃圾回收器進行管理,并且它會在對象超出作用域時進行內存回收。但是在JVM中有一個‘后門’可以讓你訪問不在堆中的本地內存(native memory)。在這篇文章中,我會給你演示一個對象是怎樣以連續的字節碼的方式在內存中進行存儲,并且告訴你是應該怎樣存儲這些字節,是在Java堆中還是在本地內存中。最后我會就怎樣從JVM中訪問內存更快給一些結論:是用Java堆還是本地內存。

使用 Unsafe 來分配和回收內存

sun.misc.Unsafe 可以讓你在Java中分配和回收本地內存,就像C語言中的 malloc 和 free 。通過它分配的內存不在Java堆中,并且不受垃圾回收器的管理,因此在它被使用完的時候你需要自己來負責釋放和回收。下面是我寫的一個使用 Unsafe 來管理本地內存的一個工具類:

public class Direct implements Memory {

    private static Unsafe unsafe;
    private static boolean AVAILABLE = false;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe)field.get(null);
            AVAILABLE = true;
        } catch(Exception e) {
            // NOOP: throw exception later when allocating memory
        }
    }

    public static boolean isAvailable() {
        return AVAILABLE;
    }

    private static Direct INSTANCE = null;

    public static Memory getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Direct();
        }
        return INSTANCE;
    }

    private Direct() {

    }

    @Override
    public long alloc(long size) {
        if (!AVAILABLE) {
            throw new IllegalStateException("sun.misc.Unsafe is not accessible!");
        }
        return unsafe.allocateMemory(size);
    }

    @Override
    public void free(long address) {
        unsafe.freeMemory(address);
    }

    @Override
    public final long getLong(long address) {
        return unsafe.getLong(address);
    }

    @Override
    public final void putLong(long address, long value) {
        unsafe.putLong(address, value);
    }

    @Override
    public final int getInt(long address) {
        return unsafe.getInt(address);
    }

    @Override
    public final void putInt(long address, int value) {
        unsafe.putInt(address, value);
    }
}

在本地內存中分配一個對象

讓我們來將下面的Java對象放到本地內存中:

public class SomeObject {

    private long someLong;
    private int someInt;

    public long getSomeLong() {
        return someLong;
    }
    public void setSomeLong(long someLong) {
        this.someLong = someLong;
    }
    public int getSomeInt() {
        return someInt;
    }
    public void setSomeInt(int someInt) {
        this.someInt = someInt;
    }
}

我們所做的僅僅是把對象的屬性放入到 Memory 中:

public class SomeMemoryObject {

    private final static int someLong_OFFSET = 0;
    private final static int someInt_OFFSET = 8;
    private final static int SIZE = 8 + 4; // one long + one int

    private long address;
    private final Memory memory;

    public SomeMemoryObject(Memory memory) {
        this.memory = memory;
        this.address = memory.alloc(SIZE);
    }

    @Override
    public void finalize() {
        memory.free(address);
    }

    public final void setSomeLong(long someLong) {
        memory.putLong(address + someLong_OFFSET, someLong);
    }

    public final long getSomeLong() {
        return memory.getLong(address + someLong_OFFSET);
    }

    public final void setSomeInt(int someInt) {
        memory.putInt(address + someInt_OFFSET, someInt);
    }

    public final int getSomeInt() {
        return memory.getInt(address + someInt_OFFSET);
    }
}

現在我們來看看對兩個數組的讀寫性能:其中一個含有數百萬的 SomeObject 對象,另外一個含有數百萬的 SomeMemoryObject 對象。

// with JIT:
Number of Objects:  1,000     1,000,000     10,000,000    60,000,000
Heap Avg Write:      107         2.30          2.51         2.58       
Native Avg Write:    305         6.65          5.94         5.26
Heap Avg Read:       61          0.31          0.28         0.28
Native Avg Read:     309         3.50          2.96         2.16

// without JIT: (-Xint)
Number of Objects:  1,000     1,000,000     10,000,000    60,000,000
Heap Avg Write:      104         107           105         102       
Native Avg Write:    292         293           300         297
Heap Avg Read:       59          63            60          58
Native Avg Read:     297         298           302         299

結論:跨越JVM的屏障來讀本地內存大約會比直接讀Java堆中的內存慢10倍,而對于寫操作會慢大約2倍。 但是需要注意的是,由于每一個SomeMemoryObject對象所管理的本地內存空間都是獨立的,因此讀寫操作都不是連續的。 那么我們接下來就來對比下讀寫連續的內存空間的性能。

訪問一大塊的連續內存空間

這個測試分別在堆中和一大塊連續本地內存中包含了相同的測試數據。然后我們來做多次的讀寫操作看看哪個更快。并且我們會做一些隨機地址的訪問來對比結果。

// with JIT and sequential access:
Number of Objects:  1,000     1,000,000     1,000,000,000
Heap Avg Write:      12          0.34           0.35 
Native Avg Write:    102         0.71           0.69 
Heap Avg Read:       12          0.29           0.28 
Native Avg Read:     110         0.32           0.32

// without JIT and sequential access: (-Xint)
Number of Objects:  1,000     1,000,000      10,000,000
Heap Avg Write:      8           8              8
Native Avg Write:    91          92             94
Heap Avg Read:       10          10             10
Native Avg Read:     91          90             94

// with JIT and random access:
Number of Objects:  1,000     1,000,000     1,000,000,000
Heap Avg Write:      61          1.01           1.12
Native Avg Write:    151         0.89           0.90 
Heap Avg Read:       59          0.89           0.92 
Native Avg Read:     156         0.78           0.84

// without JIT and random access: (-Xint)
Number of Objects:  1,000     1,000,000      10,000,000
Heap Avg Write:      55          55              55
Native Avg Write:    141         142             140
Heap Avg Read:       55          55              55 
Native Avg Read:     138         140             138

結論:在做連續訪問的時候,Java堆內存通常都比本地內存要快。對于隨機地址訪問,堆內存僅僅比本地內存慢一點點,并且是針對大塊連續數據的時候,而且沒有慢很多。

最后的結論

在Java中使用本地內存有它的意義,比如當你要操作大塊的數據時(>2G)并且不想使用垃圾回收器(GC)的時候。從延遲的角度來說,直接訪問本地內存不會比訪問Java堆快。這個結論其實是有道理的,因為跨越JVM屏障肯定是有開銷的。這樣的結論對使用本地還是堆的 ByteBuffer 同樣適用。使用本地ByteBuffer的速度提升不在于訪問這些內存,而是它可以直接與操作系統提供的本地IO進行操作。

 

來自:http://shenzhang.github.io/blog/2014/04/13/which-one-is-faster-java-heap-or-native-memory/

 

 本文由用戶 TristaMurph 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!