摘要:不幸的是,在里,由于數組元素的類型的限制,你操作起內存來會比較麻煩。這和的工作方式類似,不過它拷貝的是字節而不是數組元素。這個頭的長度可以通過方法來獲取到,這里是數組元素的類型。注意分配出來的內存是無法進行垃圾回收的。
本文主要介紹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 = 0sun.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里分配的何種內存,你都可以隨意讀寫任意類型的數據。
原文 Various types of memory allocation in Java
轉自 Java譯站
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/64063.html
摘要:編譯參見深入理解虛擬機節走進之一自己編譯源碼內存模型運行時數據區域根據虛擬機規范的規定,的內存包括以下幾個運運行時數據區域程序計數器程序計數器是一塊較小的內存空間,他可以看作是當前線程所執行的字節碼的行號指示器。 點擊進入我的博客 1.1 基礎知識 1.1.1 一些基本概念 JDK(Java Development Kit):Java語言、Java虛擬機、Java API類庫JRE(...
摘要:概要要理解的內存管理策略,首先就要熟悉的運行時數據區,如上圖所示,在執行程序的時候,虛擬機會把它所管理的內存劃分為多個不同的數據區,稱為運行時數據區。 這是一篇有關JVM內存管理的文章。這里將會簡單的分析一下Java如何使用從物理內存上申請下來的內存,以及如何來劃分它們,后面還會介紹JVM的核心技術:如何分配和回收內存。 JMM ( Java Memory Model )概要 show...
摘要:內存溢出的情況就是從類加載器加載的時候開始出現的,內存溢出分為兩大類和。以下舉出個內存溢出的情況,并通過實例代碼的方式講解了是如何出現內存溢出的。內存溢出問題描述元空間的溢出,系統會拋出。這樣就會造成棧的內存溢出。 導言: 對于java程序員來說,在虛擬機自動內存管理機制的幫助下,不需要自己實現釋放內存,不容易出現內存泄漏和內存溢出的問題,由虛擬機管理內存這一切看起來非常美好,但是一旦...
摘要:堆內存主要作用是存放運行時創建的對象。堆內存用來存放由創建的對象和數組,在堆中分配的內存,由虛擬機的自動垃圾回收器來管理。這也是比較占內存的原因,實際上,棧中的變量指向堆內存中的變量,這就是中的指針 堆:(對象) 引用類型的變量,其內存分配在堆上或者常量池(字符串常量、基本數據類型常量),需要通過new等方式來創建。 堆內存主要作用是存放運行時創建(new)的對象。(主要用于存放對象,...
閱讀 578·2023-04-25 21:29
閱讀 1104·2023-04-25 21:27
閱讀 1044·2021-11-25 09:43
閱讀 1077·2021-09-29 09:43
閱讀 3614·2021-09-03 10:30
閱讀 2854·2019-08-29 15:26
閱讀 2803·2019-08-29 12:52
閱讀 1741·2019-08-29 11:10