天天看點

如何在Java中配置設定超過-Xmx限制的記憶體

本文主要介紹Java中幾種配置設定記憶體的方法。我們會看到如何使用sun.misc.Unsafe來統一操作任意類型的記憶體。以前用C語言開發的同學通常都希望能在Java中通過較底層的接口來操作記憶體,他們一定會對本文中要講的内容感興趣。

如果你對Java記憶體優化比較感興趣,可以看下

這篇文章 ,以及它的姊妹篇:

數組配置設定的上限

Java裡數組的大小是受限制的,因為它使用的是int類型作為數組下标。這意味着你無法申請超過Integer.MAX_VALUE(2^31-1)大小的數組。這并不是說你申請記憶體的上限就是2G。你可以申請一個大一點的類型的數組。比如:

final long[] ar = new long[ Integer.MAX_VALUE ];

這個會配置設定16G -8位元組,如果你設定的-Xmx參數足夠大的話(通常你的堆至少得保留50%以上的空間,也就是說配置設定16G的記憶體,你得設定成-Xmx24G。這隻是一般的規則,具體配置設定多大要看實際情況)。

不幸的是,在Java裡,由于數組元素的類型的限制,你操作起記憶體來會比較麻煩。在操作數組方面,ByteBuffer應該是最有用的一個類了,它提供了讀寫不同的Java類型的方法。它的缺點是,目标數組類型必須是byte[],也就是說你配置設定的記憶體緩存最大隻能是2G。

把所有數組都當作byte數組來進行操作

假設現在2G記憶體對我們來說遠遠不夠,如果是16G的話還算可以。我們已經配置設定了一個long[],不過我們希望把它當作byte數組來進行操作。在Java裡我們得求助下C程式員的好幫手了——

sun.misc.Unsafe

。這個類有兩組方法:

getN(object, offset),

這個方法是要從object偏移量為offset的位置擷取一個指定類型的值并傳回它,N在這裡就是代表着那個要傳回值的類型,而

putN(Object,offset,value)

方法就是要把一個值寫到Object的offset的那個位置。

不幸的是,這些方法隻能擷取或者設定某個類型的值。如果你從數組裡拷貝資料,你還需要unsafe的另一個方法,

copyMemory(srcObject, srcOffset, destObject,destOffet,count)

。這和

System.arraycopy

的工作方式類似,不過它拷貝的是位元組而不是數組元素。

想通過sun.misc.Unsafe來通路數組的資料,你需要兩個東西:

  • 數組對象裡資料的偏移量
  • 拷貝的元素在數組資料裡的偏移量

Arrays和Java别的對象一樣,都有一個對象頭,它是存儲在實際的資料前面的。這個頭的長度可以通過

unsafe.arrayBaseOffset(T[].class)

方法來擷取到,這裡T是數組元素的類型。數組元素的大小可以通過

unsafe.arrayIndexScale(T[].class)

方法擷取到。這也就是說要通路類型為T的第N個元素的話,你的偏移量offset應該是

arrayOffset+N*arrayScale

你可以看下這篇文章裡,UnsafeMemory類使用Unsafe通路記憶體的那個例子。

我們來寫個簡單的例子吧。我們配置設定一個long數組,然後更新它裡面的幾個位元組。我們把最後一個元素更新成-1(16進制的話是

0xFFFF FFFF FFFF FFFF

),然再逐個清除這個元素的所有位元組。

final long[] ar = new long[ 1000 ];

final int index = ar.length - 1;

ar[ index ] = -1; //FFFF FFFF FFFF FFFF

System.out.println( "Before change = " + Long.toHexString( ar[ index ] ));

for ( long i = 0; i < 8; ++i )

{

    unsafe.putByte( ar, longArrayOffset + 8L * index + i, (byte) 0);

    System.out.println( "After change: i = " + i + ", val = "  +  Long.toHexString( ar[ index ] ));

}

想運作上面 這個例子的話,得在你的測試類裡加上下面的靜态代碼塊:

private static final Unsafe unsafe;

static

    try

    {

        Field field = Unsafe.class.getDeclaredField("theUnsafe");

        field.setAccessible(true);

        unsafe = (Unsafe)field.get(null);

    }

    catch (Exception e)

        throw new RuntimeException(e);

private static final long longArrayOffset = unsafe.arrayBaseOffset(long[].class);

輸出的結果是:

Before change = ffffffffffffffff

After change: i = 0, val = ffffffffffffff00

After change: i = 1, val = ffffffffffff0000

After change: i = 2, val = ffffffffff000000

After change: i = 3, val = ffffffff00000000

After change: i = 4, val = ffffff0000000000

After change: i = 5, val = ffff000000000000

After change: i = 6, val = ff00000000000000

After change: i = 7, val = 0

sun.misc.Unsafe

的記憶體配置設定

上面也說過了,在純Java裡我們的能配置設定的記憶體大小是有限的。這個限制在Java的最初版本裡就已經定下來了,那個時候人們都不敢相像配置設定好幾個G的記憶體是什麼情況。不過現在已經是大資料的時代了,我們需要更多的記憶體。在Java裡,想擷取更多的記憶體有兩個方法:

  • 配置設定許多小塊的記憶體,然後邏輯上把它們當作一塊連續的大記憶體來使用。
  • 使用

    sun.misc.Unsafe.allcateMemory(long)

    來進行記憶體配置設定。

第一個方法隻是從算法的角度來看比較有意思一點,是以我們還是來看下第二個方法。

sun.misc.Unsafe提供了一組方法來進行記憶體的配置設定,重新配置設定,以及釋放。它們和C的malloc/free方法很像:

  • long Unsafe.allocateMemory(long size)

    ——配置設定一塊記憶體空間。這塊記憶體可能會包含垃圾資料(沒有自動清零)。如果配置設定失敗的話會抛一個java.lang.OutOfMemoryError的異常。它會傳回一個非零的記憶體位址(看下面的描述)。
  • Unsafe.reallocateMemory(long address, long size)

    ——重新配置設定一塊記憶體,把資料從舊的記憶體緩沖區(address指向的地方)中拷貝到的新配置設定的記憶體塊中。如果位址等于0,這個方法和allocateMemory的效果是一樣的。它傳回的是新的記憶體緩沖區的位址。
  • Unsafe.freeMemory(long address)

    ——釋放一個由前面那兩方法生成的記憶體緩沖區。如果address為0什麼也不幹 。

這些方法配置設定的記憶體應該在一個被稱為單寄存器位址的模式下使用:Unsafe提供了一組隻接受一個位址參數的方法(不像雙寄存器模式,它們需要一個Object還有一個偏移量offset)。通過這種方式配置設定的記憶體可以比你在-Xmx的Java參數裡配置的還要大。

注意:Unsafe配置設定出來的記憶體是無法進行垃圾回收的。你得把它當成一種正常的資源,自己去進行管理。

下面是使用

Unsafe.allocateMemory

配置設定記憶體的一個例子,同時它還檢查了整個記憶體緩沖區是不是可讀寫的:

final int size = Integer.MAX_VALUE / 2;

final long addr = unsafe.allocateMemory( size );

try

    System.out.println( "Unsafe address = " + addr );

    for ( int i = 0; i < size; ++i )

        unsafe.putByte( addr + i, (byte) 123);

        if ( unsafe.getByte( addr + i ) != 123 )

            System.out.println( "Failed at offset = " + i );

finally

    unsafe.freeMemory( addr );

正如你所看見的,使用

sun.misc.Unsafe

你可以寫出非常通用的記憶體通路的代碼:不管是Java裡配置設定的何種記憶體,你都可以随意讀寫任意類型的資料。