一、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(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 |
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開銷在某些情況下可能是致命的。