1. 程式人生 > >Java 記憶體分配及容量擴充

Java 記憶體分配及容量擴充

一、Java 程序的記憶體使用

當執行一個Java應用程式時,Java 執行時會建立一個作業系統程序,作為作業系統程序,Java 執行時面臨著與其他程序完全相同的記憶體限制

架構提供的記憶體定址能力依賴於處理器的位數,舉例來說,32位或者64位程序能夠處理的位數決定了處理器能定址的記憶體範圍:32位提供了 2^32 的可定址範圍,也就是 4,294,967,296 位,或者說 4GB。而 64 位處理器的可定址範圍明顯增大:2^64,也就是 18,446,744,073,709,551,616,或者說 16 exabyte(百億億位元組)。
OS和C執行時佔用的記憶體數量取決於所用的OS,但通常數量較大:Windows 預設佔用的記憶體是2GB。剩餘的可定址空間就是可供執行的實際程序使用的記憶體。

對於Java應用程式,使用者空間是 Java 程序佔用的記憶體,實際上包含兩個池:Java 堆和本機(非 Java)堆。Java 堆的大小由 JVM 的Java堆設定控制:-Xms 和 -Xmx 分別設定最小和最大 Java 堆。在按照最大的大小設定分配了Java堆之後,剩下的使用者空間就是本機堆。下面展示了一個 32 位 Java 程序的記憶體佈局

一個 32 位 Java 程序的記憶體佈局示例檢視

可定址範圍總共有 4GB,OS 和 C 執行時大約佔用了其中的 1GB,Java 堆佔用了將近 2GB,本機堆佔用了其他部分。請注意,JVM 本身也要佔用記憶體,就像 OS 核心和 C 執行時一樣,而 JVM 佔用的記憶體是本機堆的子集。

二、Java 物件記憶體使用情況

Java程式碼使用new操作符建立一個Java物件的例項時,實際上分配的資料要比您想的多得多。例如,一個int值與一個Integer物件的大小比率是 1:4.額外的開銷源於 JVM 用於描述 Java 物件的元資料.其中通常包括:
類:一個指向類資訊的指標,描述了物件型別。舉例來說,對於 java.lang.Integer 物件,這是 java.lang.Integer 類的一個指標。
標記:一組標記,描述了物件的狀態,包括物件的雜湊碼(如果有),以及物件的形狀(也就是說,物件是否是陣列)。
鎖:物件的同步資訊,也就是說,物件目前是否正在同步。
物件元資料後緊跟著物件資料本身,包括物件例項中儲存的欄位。對於 java.lang.Integer 物件,這就是一個 int。
如果您正在執行一個 32 位 JVM,那麼在建立 java.lang.Integer 物件例項時,物件的佈局可能如圖

一個 32 位 Java 程序的 java.lang.Integer 物件的佈局示例 

有128位的資料被佔用,其中用於儲存int值的為32位,而物件元資料佔用了其餘的96位。

三、Java 陣列物件記憶體使用情況

陣列物件(例如一個 int 值陣列)的形狀和結構與標準 Java 物件相似。主要差別在於陣列物件包含說明陣列大小的額外元資料。
因此,資料物件的元資料包括:
類:一個指向類資訊的指標,描述了物件型別。舉例來說,對於 int 欄位陣列,這是 int[] 類的一個指標。
標記:一組標記,描述了物件的狀態,包括物件的雜湊碼(如果有),以及物件的形狀(也就是說,物件是否是陣列)。
鎖:物件的同步資訊,也就是說,物件目前是否正在同步。
大小:陣列的大小。
int 陣列物件的佈局示例

一個 32 位程序的 int 陣列物件的佈局示例 

有160位的資料用於儲存int值內的32位資料,而陣列元資料佔用了其餘160位。

四、比較32位和64位Java物件記憶體使用情況
64 位處理器的記憶體可定址能力比 32 位處理器高得多。對於 64 位程序,Java 物件中的某些資料欄位的大小(特別是物件元資料)也需要增加到 64 位。其他資料欄位型別(例如 int、byte 和 long )的大小不會更改。
一個 64 位程序的 java.lang.Integer 物件和 int 陣列的佈局示例

一個 64 位 Java 程序的 java.lang.Integer 物件和 int 陣列的佈局示例 

對於一個 64 位 Integer 物件,現在有 224 位的資料用於儲存 int 欄位所用的 32 位,開銷比例是 7:1。對於一個 64 位單元素 int 陣列,有 288 位的資料用於儲存 32 位 int 條目,開銷比例是 9:1。在 32 位 Java 執行時中執行的應用程式若遷移到 64 位 Java 執行時,其 Java 堆記憶體使用量會顯著增加。通常情況下,增加的數量是原始堆大小的 70% 左右。
,一個在 32 位 Java 執行時中使用 1GB Java 堆的 Java 應用程式在遷移到 64 位 Java 執行時之後,通常需要使用 1.7GB 的 Java 堆。請注意,這種記憶體增加並非僅限於 Java 堆。本機堆記憶體區使用量也會增加,有時甚至要增加 90% 之多。

五、Java 集合的記憶體使用

大量資料都是使用核心 Java API 提供的標準 Java Collections 類來儲存和管理的。非常有必要了解各集合提供的功能以及相關的記憶體開銷。總體而言,集合功能的級別越高,記憶體開銷就越高,
其中部分最常用的集合如下:
HashSet
HashMap
Hashtable
LinkedList
ArrayList
(HashSet 是包圍一個 HashMap 物件的包裝器,它提供的功能比 HashMap 少,同時容量稍微小一些。)

屬性彙總,記憶體開銷

集合 效能 預設容量 空時的大小 10K 條目的開銷 準確設定大小? 擴充套件演算法
HashSet O(1) 16 144 360K x2
HashMap O(1) 16 128 360K x2
Hashtable O(1) 11 104 360K x2+1
LinkedList O(n) 1 48 240K +1
ArrayList O(n) 10 88 40K x1.5
StringBuffer O(1) 16 72 24 x2
Hash 集合的效能比任何 List 的效能都要高,但每條目的成本也要更高。對於並不那麼注重訪問效能的較小集合而言,List 則是合理的選擇。ArrayList 和 LinkedList 集合的效能大體相同,但其記憶體佔用完全不同:ArrayList 的每條目大小要比 LinkedList 小得多,但它不是準確設定大小的。List 要使用的正確實現是 ArrayList 還是 LinkedList 取決於 List 長度的可預測性。如果長度未知,那麼正確的選擇可能是 LinkedList,因為集合包含的空白空間更少。如果大小已知,那麼 ArrayList 的記憶體開銷會更低一些。

六、ArrayList和HashMap容量增長方式

(1)、ArrayList 容量增長的方式

public boolean add(E e) { 
  ensureCapacity(size + 1); // 容量增長
  elementData[size++] = e; 
  return true; 
  }


public void ensureCapacity(int minCapacity) { 
  modCount++; 
  int ldCapacity = elementData.length; 
  if (minCapacity > oldCapacity) { 
  Object oldData[] = elementData; 
  int newCapacity = (oldCapacity * 3)/2 + 1; 
  if (newCapacity < minCapacity) {
  newCapacity = minCapacity; 
}
    elementData = Arrays.copyOf(elementData, newCapacity); //依次拷貝
  } 
} 


int newCapacity = (oldCapacity * 3)/2 + 1;  也就是原有容量的1.5倍+1。然後通過底層的複製方法將原有資料複製過來
如果資料量很大,那麼造成陣列重新分配的次數會增加,但對於一般的資料量下,
1千需要分配 11次
1萬一級需要分配17次
10萬 需要分配23次
100萬需要分配28次
所以,大家根據實際情況,大致分配一個初始化的容量還是有必要的。但是如果你初始容量太大,而資料增長很慢,那麼就在浪費記憶體了。
如何取捨,還是看具體的應用場景了。


(2)、HashMap 容量增長方式
HashMap主要是用陣列來儲存資料的,我們都知道它會對key進行雜湊運算,哈系運算會有重複的雜湊值,對於雜湊值的衝突,HashMap採用連結串列來解決的。
transient Entry[] table;
Entry 就是HashMap儲存資料所用的類,它擁有的屬性如下
final K key;
V value;
final int hash; 
Entry<K,V> next; //next就是為了雜湊衝突而存在的。


當put一個元素時,如果達到了容量限制,HashMap就會擴容,新的容量永遠是原來的2倍。
if (size++ >= threshold){
   resize(2 * table.length);
}
達到 threshold指定的值時就開始擴容, threshold=最大容量*載入因子。
如果資料大小是固定的,那麼最好給HashMap設定一個合理的容量值
根據上面的分析,HashMap的初始預設容量是16,預設載入因子是0.75,也就是說,如果採用HashMap的預設建構函式,當增加資料時,資料實際容量超過16*0.75=12時,HashMap就擴容,擴容帶來一系列的運算,新建一個是原來容量2倍的陣列,對原有元素全部重新雜湊,如果你的資料有幾千幾萬個,而用預設的HashMap建構函式,那結果是非常悲劇的,因為HashMap不斷擴容,不斷雜湊,在使用HashMap的場景裡,不會是多個執行緒共享一個HashMap,除非對HashMap包裝並同步,由此產生的記憶體開銷和cpu開銷在某些情況下可能是致命的。