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 程序的記憶體佈局
可定址範圍總共有 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 物件例項時,物件的佈局可能如圖
有128位的資料被佔用,其中用於儲存int值的為32位,而物件元資料佔用了其餘的96位。
三、Java 陣列物件記憶體使用情況
陣列物件(例如一個 int 值陣列)的形狀和結構與標準 Java 物件相似。主要差別在於陣列物件包含說明陣列大小的額外元資料。
因此,資料物件的元資料包括:
類:一個指向類資訊的指標,描述了物件型別。舉例來說,對於 int 欄位陣列,這是 int[] 類的一個指標。
標記:一組標記,描述了物件的狀態,包括物件的雜湊碼(如果有),以及物件的形狀(也就是說,物件是否是陣列)。
鎖:物件的同步資訊,也就是說,物件目前是否正在同步。
大小:陣列的大小。
int 陣列物件的佈局示例
有160位的資料用於儲存int值內的32位資料,而陣列元資料佔用了其餘160位。
四、比較32位和64位Java物件記憶體使用情況
64 位處理器的記憶體可定址能力比 32 位處理器高得多。對於 64 位程序,Java 物件中的某些資料欄位的大小(特別是物件元資料)也需要增加到 64 位。其他資料欄位型別(例如 int、byte 和 long )的大小不會更改。
一個 64 位程序的 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 |
六、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開銷在某些情況下可能是致命的。