Java對象佔用內存大小

new Object()將佔用多少bytes的內存空間?

原生類型(primitive type)的內存佔用
Primitive Type             Memory Required(bytes)
—————————————————————
boolean                      1
byte                            1
short                           2
char                            2
int                               4
float                            4
long                            8
double                        8 html

 

對象在內存中存儲的佈局能夠分爲三塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。  
HotSpot虛擬機的對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據, 如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等等,這部分數據的長度在32位和64位的虛擬機(暫 不考慮開啓壓縮指針的場景)中分別爲32個和64個Bits,官方稱它爲「Mark Word」。對象須要存儲的運行時數據不少,其實已經超出了3二、64位Bitmap結構所能記錄的限度,可是對象頭信息是與對象自身定義的數據無關的額 外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲儘可能多的信息,它會根據對象的狀態複用本身的存儲空間。例如在32位的HotSpot虛擬機 中對象未被鎖定的狀態下,Mark Word的32個Bits空間中的25Bits用於存儲對象哈希碼(HashCode),4Bits用於存儲對象分代年齡,2Bits用於存儲鎖標誌 位,1Bit固定爲0,在其餘狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容以下表所示。  

表1 HotSpot虛擬機對象頭Mark Word java

存儲內容 標誌位 狀態
對象哈希碼、對象分代年齡 01 未鎖定
指向鎖記錄的指針 00 輕量級鎖定
指向重量級鎖的指針 10 膨脹(重量級鎖定)
空,不須要記錄信息 11 GC標記
偏向線程ID、偏向時間戳、對象分代年齡 01

可偏向 算法

 

 

1. 一個object header, 也稱object overhead, 保存當前實例的type信息和內置monitor信息等, 32位系統上佔用8bytes,64位系統上佔用16bytes;
2. 0到多個fields, reference類型在32位系統上每一個佔用4bytes, 在64位系統上每一個佔用8bytes; primitive類型參考上面;
3. padding, 對步驟一、2之和的補長。CPU從內存中讀取數據是以word爲基本單位, 32位的系統中word寬度爲32bits, 64位的系統中word寬度爲64bits, 將整個Java對象佔用內存補長爲word的整倍數大大提升了CPU存取數據的性能,參考維基百科關於數據alignment的說明。 就Hotspot而言,不論是32位系統仍是64位系統要求(步驟1 + 步驟2 + padding) % 8等於0且0 <= padding < 8。例如在64位系統上: 數組

public class Student {
    private int age;
}

如new Student()則其佔用內存: 16 + 4 = 20,按照3中的說明則padding爲4bytes,這樣整個內存佔用爲24bytes。 服務器

六. 一維原生數組的內存佔用
——————————————————————————————–
1. 在32位的系統中, 佔用內存爲: 型別佔用內存 * 數組長度 + 8(數組在JVM中被當成特殊的對象, object overhead佔用8bytes) + 4(數組長度) + padding。如:
byte[2], 型別佔用內存,即byte型別佔用1byte,數組長度爲2,這樣佔用的總內存爲1 * 2 + 8 + 4 = 14,padding上2bytes爲16bytes,因此byte[2]佔用內存爲16bytes。
2. 在64位的系統中, 佔用內存爲: 型別佔用內存 * 數組長度 + 16(object overhead佔用16bytes) + 8(數組長度) + padding。如:
byte[2], 型別佔用內存,即byte型別佔用1byte,數組長度爲2,這樣佔用的總內存爲1 * 2 + 16 + 8 = 26,padding上6bytes,26 + 6 = 32bytes,因此byte[2]佔用內存爲32bytes 數據結構

七. 多維數組和一維對象數組
——————————————————————————————–
1. 在32位的系統中, 佔用內存爲: reference佔用內存 * 數組第1維長度 +12(數組自己被當作reference佔8bytes,數組長度佔4bytes)。如:
byte[3][7], reference佔用內存4byte,數組第1維長度爲3,這樣佔用的總內存爲4 * 3 + 12 = 24,因此byte[3][7]佔用內存爲24bytes。再如byte[7][3], reference佔用內存4byte,數組第1維長度爲7,這樣佔用的總內存爲4 * 7 + 12 = 40,因此byte[7][3]佔用內存爲40bytes。再如new HashMap[7][6][4],reference佔用內存4byte,數組第1維長度爲7,這樣佔用的總內存爲4 * 7 + 12 = 40,因此HashMap[7][6][4]佔用內存爲40bytes。
2. 在64位的系統中, 佔用內存爲: reference佔用內存 * 數組第1維長度 +24(數組自己被當作reference佔16bytes,數組長度佔8bytes)。如:
byte[3][7], reference佔用內存8byte,數組第1維長度爲3,這樣佔用的總內存爲8 * 3 + 24 = 48,因此byte[3][7]佔用內存爲48bytes。 oracle

八. 編碼計算
——————————————————————————————–
1. java.lang.instrument.Instrumentation實例由JVM產生,咱們需實現一個代理(agent),根據java.lang.instrument的package specification說明,這個代理裏需有個public static void premain(String agentArgs, Instrumentation inst); 方法,這樣在JVM初始化後在調用應用程序main方法前,JVM將調用咱們agent裏的這個premain方法,這樣就注入了Instrumentation實例。
2. 計算實例的內存大小,經過Instrumentation#getObjectSize(Object objectToSize)得到。
3. 注意: 若是有field是常量(如, Boolean.FALSE),由於多實例共享,因此算其佔用內存爲0。
4. 如計算對象Deep範圍內存佔用的話則需遞歸計算引用對象佔用的內存,而後進行累加。
5. 代碼實現以下MemoryCalculator.java: jvm

/*
 * @(#)MemoryCalculator.java	1.0 2010-11-8
 *
 * Copyright 2010 Richard Chen(utopia_rabbi@sse.buaa.edu.cn) All Rights Reserved.
 * PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package charpter.memory;

import java.lang.instrument.Instrumentation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Stack;

/**
 * 提供實例佔用內存大小的計算功能. 內部藉助JVM的{@link Instrumentation}實現.
 *
 * @author Rich, 2010-11-8.
 * @version 1.0
 * @since 1.0
 */
public final class MemoryCalculator {
	/**
	 * JVM在初始化後在調用應用程序main方法前將調用本方法, 本方法中能夠寫任何main方法中可寫的代碼.
	 *
	 * @param agentArgs 命令行傳進行來的代理參數, 內部需自行解析.
	 * @param inst JVM注入的句柄.
	 */
	public static void premain(String agentArgs, Instrumentation inst) {
		instrumentation = inst;
	}
	/**
	 * 計算實例自己佔用的內存大小. 注意:
	 * 1. 屢次調用可能結果不同, 主要跟實例的狀態有關
	 * 2. 實例中成員變量若是是reference類型, 則reference所指向的實例佔用內存大小不統計在內
	 *
	 * @param obj 待計算內存佔用大小的實例.
	 * @return 內存佔用大小, 單位爲byte.
	 */
	public static long shallowSizeOf(Object obj) {
		if (instrumentation == null) {
			throw new IllegalStateException("Instrumentation initialize failed");
		}
		if (isSharedObj(obj)) {
			return 0;
		}
		return instrumentation.getObjectSize(obj);
	}
	/**
	 * 計算實例佔用的內存大小, 含其成員變量所引用的實例, 遞歸計算.
	 *
	 * @param obj 待計算內存佔用大小的實例.
	 * @return 內存佔用大小, 單位爲byte.
	 */
	public static long deepSizeOf(Object obj) {
		Map calculated = new IdentityHashMap();
		Stack unCalculated = new Stack();
		unCalculated.push(obj);
		long result = 0;
		do {
			result += doSizeOf(unCalculated, calculated);
		} while (!unCalculated.isEmpty());
		return result;
	}
	/**
	 * 判斷obj是不是共享對象. 有些對象, 如interned Strings, Boolean.FALSE和Integer#valueOf()等.
	 *
	 * @param obj 待判斷的對象.
	 * @return true, 是共享對象, 不然返回false.
	 */
	private static boolean isSharedObj(Object obj) {
		if (obj instanceof Comparable) {
			if (obj instanceof Enum) {
				return true;
			} else if (obj instanceof String) {
				return (obj == ((String) obj).intern());
			} else if (obj instanceof Boolean) {
				return (obj == Boolean.TRUE || obj == Boolean.FALSE);
			} else if (obj instanceof Integer) {
				return (obj == Integer.valueOf((Integer) obj));
			} else if (obj instanceof Short) {
				return (obj == Short.valueOf((Short) obj));
			} else if (obj instanceof Byte) {
				return (obj == Byte.valueOf((Byte) obj));
			} else if (obj instanceof Long) {
				return (obj == Long.valueOf((Long) obj));
			} else if (obj instanceof Character) {
				return (obj == Character.valueOf((Character) obj));
			}
		}
		return false;
	}
	/**
	 * 確認是否需計算obj的內存佔用, 部分狀況下無需計算.
	 *
	 * @param obj 待判斷的對象.
	 * @param calculated 已計算過的對象.
	 * @return true, 意指無需計算, 不然返回false.
	 */
	private static boolean isEscaped(Object obj, Map calculated) {
		return obj == null || calculated.containsKey(obj)
				|| isSharedObj(obj);
	}
	/**
	 * 計算棧頂對象自己的內存佔用.
	 *
	 * @param unCalculated 待計算內存佔用的對象棧.
	 * @param calculated 對象圖譜中已計算過的對象.
	 * @return 棧頂對象自己的內存佔用, 單位爲byte.
	 */
	private static long doSizeOf(Stack unCalculated, Map calculated) {
		Object obj = unCalculated.pop();
		if (isEscaped(obj, calculated)) {
			return 0;
		}
		Class clazz = obj.getClass();
		if (clazz.isArray()) {
			doArraySizeOf(clazz, obj, unCalculated);
		} else {
			while (clazz != null) {
				Field[] fields = clazz.getDeclaredFields();
				for (Field field : fields) {
					if (!Modifier.isStatic(field.getModifiers())
							&& !field.getType().isPrimitive()) {
						field.setAccessible(true);
						try {
							unCalculated.add(field.get(obj));
						} catch (IllegalAccessException ex) {
							throw new RuntimeException(ex);
						}
					}
				}
				clazz = clazz.getSuperclass();
			}
		}
		calculated.put(obj, null);
		return shallowSizeOf(obj);
	}
	/**
	 * 將數組中的全部元素加入到待計算內存佔用的棧中, 等待處理.
	 *
	 * @param arrayClazz 數組的型別.
	 * @param array 數組實例.
	 * @param unCalculated 待計算內存佔用的對象棧.
	 */
	private static void doArraySizeOf(Class arrayClazz, Object array,
			Stack unCalculated) {
		if (!arrayClazz.getComponentType().isPrimitive()) {
			int length = Array.getLength(array);
			for (int i = 0; i < length; i++) {
				unCalculated.add(Array.get(array, i));
			}
		}
	}
	/** JVM將在啓動時經過{@link #premain}初始化此成員變量. */
	private static Instrumentation instrumentation = null;
}

,從Java SE 6u23以後的64位版本就默認打開了對象指針壓縮。 ide

十. Compressed oops的內存佔用
注意,Compressed oops只在64位的JVM中才會有,另外,在Java SE 6u23以前的1.6版本中須要經過-XX:+UseCompressedOops參數開啓。壓縮算法對對象內存佔用計算的影響主要在於:
——————————————————————————————–
1. object header,未壓縮前由一個native-sized mark word 8bytes加上一個class word 8bytes組成,共16bytes。採用壓縮後,class word縮減爲4bytes,現共佔用12bytes;
2. reference類型,由8bytes縮減爲4bytes;
3. 數組長度,由8bytes縮減爲4bytes。 oop

因此,上述測試案例中:
——————————————————————————————–
1. 原生類型,內存佔用大小不變。
2. 對象類型,object header由16bytes變動爲12bytes,reference類型的fields由8bytes變動爲4bytes,primitive類型的fields保持不變,padding不變。
3. 一維原生數組,如new byte[2]佔用內存的計算公式由:型別佔用內存 * 數組長度 + 16 + 8 + padding變動爲: 型別佔用內存 * 數組長度 + 12 + 4 + padding,這樣獲得: 1byte * 2 + 12 + 4 = 18,padding上6bytes等於24bytes。
4. 多維數組和一維對象數組,如new byte[3][7],計算公式由: reference佔用內存 * 數組第1維長度 +24(數組自己被當作reference佔16bytes,數組長度佔8bytes) 變動爲: reference佔用內存 * 數組第1維長度 + 16(object header 12bytes,數組長度佔4bytes) + padding,這樣獲得:4bytes * 3 + 16 = 28,padding上4bytes等於32bytes。 再如new HashMap[7],7 * 4bytes + 16 = 44bytes,padding上4bytes爲48bytes。

十一. 總結
經過上述Java內存佔用大小的理論分析與實際測試,給咱們實際開發帶來幾點重要的啓發:
——————————————————————————————–
1. 一樣的程序在不一樣環境下運行,佔用的內存不同大小,64位系統上佔用的內存要比在32位系統上多1至1.5倍;
2. n個元素的數組要比n個單獨元素佔用更大的內存,特別是primitive類型的數組;
3. 定義多維數組時,要儘量把長度小的放在第1維,即int[9][1]要比int[1][9]佔用更多內存,Integer[1000][4][3]遠比Integer[3][4][1000]佔用的內存要多得多;
4. Java SE 6u23以後的64位版本要比以前的版本在對象內存佔用方面小得多。

 

  1. jvm對於對象會啓用對齊優化,咱們定義類時field的順序在運行期會被打亂

  2. 關閉了壓縮指針模式後,Person對象體偏移由 offset = 16變成了 offset = 12

 

因此開啓壓縮指針模式後,對象頭的_klass域獲得了壓縮,竟然變成了32位系統時的長度4字節了,咱們都知道32位的長度最多隻能表示4G的內存,那麼HostSpot 到底是如何處理的呢

咱們引用官方文檔:

Java HotSpot? Virtual Machine Performance Enhancements

這就是面對對象的好處,咱們面對的最小地址單元不是byte,而是object,也就是說在jvm的世界裏32位地址表示的不是4GB,而是4G個對象的指針,大概是32GB,解碼過程就是把對象指針乘以8加上GC堆的初始地址就能獲得操做系統本地64位地址了,編碼過程相反

其中啓用壓指得有操做系統底層的支持:GC堆從虛擬地址0開始分配

進而咱們能夠獲得壓指面對的全部場景:

  • 若是GC堆大小在4G如下,直接砍掉高32位,避免了編碼解碼過程

  • 若是GC堆大小在4G以上32G如下,則啓用UseCompressedOop

  • 若是GC堆大小大於32G,壓指失效(因此說服務器內存太大很差......

考慮到內存對齊,Person對象開壓指長度爲32字節,不開爲40字節

相關文章
相關標籤/搜索