一、數(shù)組分配的上限
Java里數(shù)組的大小是受限制的,因為它使用的是int類型作為數(shù)組下標。這意味著你無法申請超過Integer.MAX_VALUE(2^31-1)大小的數(shù)組。這并不是說你申請內(nèi)存的上限就是2G。你可以申請一個大一點的類型的數(shù)組。比如:
復制代碼代碼如下:
finallong[]ar=newlong[Integer.MAX_VALUE];
這個會分配16G-8字節(jié),如果你設置的-Xmx參數(shù)足夠大的話(通常你的堆至少得保留50%以上的空間,也就是說分配16G的內(nèi)存,你得設置成-Xmx24G。這只是一般的規(guī)則,具體分配多大要看實際情況)。
不幸的是在Java里,由于數(shù)組元素的類型的限制,你操作起內(nèi)存來會比較麻煩。在操作數(shù)組方面,ByteBuffer應該是最有用的一個類了,它提供了讀寫不同的Java類型的方法。它的缺點是目標數(shù)組類型必須是byte[],也就是說你分配的內(nèi)存緩存最大只能是2G。
二、把所有數(shù)組都當作byte數(shù)組來進行操作
假設現(xiàn)在2G內(nèi)存對我們來說遠遠不夠,如果是16G的話還算可以。我們已經(jīng)分配了一個long[],不過我們希望把它當作byte數(shù)組來進行操作。在Java里我們得求助下C程序員的好幫手了——sun.misc.Unsafe。這個類有兩組方法:getN(object,offset),這個方法是要從object偏移量為offset的位置獲取一個指定類型的值并返回它,N在這里就是代表著那個要返回值的類型,而putN(Object,offset,value)方法就是要把一個值寫到Object的offset的那個位置。
不幸的是這些方法只能獲取或者設置某個類型的值。如果你從數(shù)組里拷貝數(shù)據(jù),你還需要unsafe的另一個方法,copyMemory(srcObject,srcOffset,destObject,destOffet,count)。這和System.arraycopy的工作方式類似,不過它拷貝的是字節(jié)而不是數(shù)組元素。
想通過sun.misc.Unsafe來訪問數(shù)組的數(shù)據(jù),你需要兩個東西:
1.數(shù)組對象里數(shù)據(jù)的偏移量
2.拷貝的元素在數(shù)組數(shù)據(jù)里的偏移量
Arrays和Java別的對象一樣,都有一個對象頭,它是存儲在實際的數(shù)據(jù)前面的。這個頭的長度可以通過unsafe.arrayBaseOffset(T[].class)方法來獲取到,這里T是數(shù)組元素的類型。數(shù)組元素的大小可以通過unsafe.arrayIndexScale(T[].class)方法獲取到。這也就是說要訪問類型為T的第N個元素的話,你的偏移量offset應該是arrayOffset+N*arrayScale。
我們來寫個簡單的例子吧。我們分配一個long數(shù)組,然后更新它里面的幾個字節(jié)。我們把最后一個元素更新成-1(16進制的話是0xFFFFFFFFFFFFFFFF),然再逐個清除這個元素的所有字節(jié)。
復制代碼代碼如下:
finallong[]ar=newlong[1000];
finalintindex=ar.length-1;
ar[index]=-1;//FFFFFFFFFFFFFFFF
System.out.println(Beforechange=+Long.toHexString(ar[index]));
for(longi=0;i<8;++i)
{
unsafe.putByte(ar,longArrayOffset+8L*index+i,(byte)0);
System.out.println(Afterchange:i=+i+,val=+Long.toHexString(ar[index]));
}
想運行上面這個例子的話,得在你的測試類里加上下面的靜態(tài)代碼塊:
復制代碼代碼如下:
privatestaticfinalUnsafeunsafe;
static
{
try
{
Fieldfield=Unsafe.class.getDeclaredField(theUnsafe);
field.setAccessible(true);
unsafe=(Unsafe)field.get(null);
}
catch(Exceptione)
{
thrownewRuntimeException(e);
}
}
privatestaticfinallonglongArrayOffset=unsafe.arrayBaseOffset(long[].class);
輸出的結果是:
復制代碼代碼如下:
Beforechange=ffffffffffffffff
Afterchange:i=0,val=ffffffffffffff00
Afterchange:i=1,val=ffffffffffff0000
Afterchange:i=2,val=ffffffffff000000
Afterchange:i=3,val=ffffffff00000000
Afterchange:i=4,val=ffffff0000000000
Afterchange:i=5,val=ffff000000000000
Afterchange:i=6,val=ff00000000000000
Afterchange:i=7,val=0
三、sun.misc.Unsafe的內(nèi)存分配
上面也說過了,在純Java里我們的能分配的內(nèi)存大小是有限的。這個限制在Java的最初版本里就已經(jīng)定下來了,那個時候人們都不敢相像分配好幾個G的內(nèi)存是什么情況。不過現(xiàn)在已經(jīng)是大數(shù)據(jù)的時代了,我們需要更多的內(nèi)存。在Java里,想獲取更多的內(nèi)存有兩個方法:
1.分配許多小塊的內(nèi)存,然后邏輯上把它們當作一塊連續(xù)的大內(nèi)存來使用。
2.使用sun.misc.Unsafe.allcateMemory(long)來進行內(nèi)存分配。
第一個方法只是從算法的角度來看比較有意思一點,所以我們還是來看下第二個方法。
sun.misc.Unsafe提供了一組方法來進行內(nèi)存的分配,重新分配,以及釋放。它們和C的malloc/free方法很像:
1.longUnsafe.allocateMemory(longsize)——分配一塊內(nèi)存空間。這塊內(nèi)存可能會包含垃圾數(shù)據(jù)(沒有自動清零)。如果分配失敗的話會拋一個java.lang.OutOfMemoryError的異常。它會返回一個非零的內(nèi)存地址(看下面的描述)。
2.Unsafe.reallocateMemory(longaddress,longsize)——重新分配一塊內(nèi)存,把數(shù)據(jù)從舊的內(nèi)存緩沖區(qū)(address指向的地方)中拷貝到的新分配的內(nèi)存塊中。如果地址等于0,這個方法和allocateMemory的效果是一樣的。它返回的是新的內(nèi)存緩沖區(qū)的地址。
3.Unsafe.freeMemory(longaddress)——釋放一個由前面那兩方法生成的內(nèi)存緩沖區(qū)。如果address為0什么也不干。
這些方法分配的內(nèi)存應該在一個被稱為單寄存器地址的模式下使用:Unsafe提供了一組只接受一個地址參數(shù)的方法(不像雙寄存器模式,它們需要一個Object還有一個偏移量offset)。通過這種方式分配的內(nèi)存可以比你在-Xmx的Java參數(shù)里配置的還要大。
注意:Unsafe分配出來的內(nèi)存是無法進行垃圾回收的。你得把它當成一種正常的資源,自己去進行管理。
下面是使用Unsafe.allocateMemory分配內(nèi)存的一個例子,同時它還檢查了整個內(nèi)存緩沖區(qū)是不是可讀寫的:
復制代碼代碼如下:
finalintsize=Integer.MAX_VALUE/2;
finallongaddr=unsafe.allocateMemory(size);
try
{
System.out.println(Unsafeaddress=+addr);
for(inti=0;i<size;++i)
{
unsafe.putByte(addr+i,(byte)123);
if(unsafe.getByte(addr+i)!=123)
System.out.println(Failedatoffset=+i);
}
}
finally
{
unsafe.freeMemory(addr);
}
正如你所看見的,使用sun.misc.Unsafe你可以寫出非常通用的內(nèi)存訪問的代碼:不管是Java里分配的何種內(nèi)存,你都可以隨意讀寫任意類型的數(shù)據(jù)。