在進行 JVM 調優時,咱們常常關注 JVM 各個區域大小以及相關參數,從而進行特定的優化,在一次排查內存溢出問題時我不由想到一個問題,一個 Java 對象到底佔用多大內存?下面咱們就來分析驗證下。java
在 JVM 中,Java 對象都是在堆內存上分配的,想要分析出 Java 對象內存佔用,首先要了解 Java 對象內存結構,一個 Java 對象內存佔用由三部分組成:對象頭(Header)
,實例數據(Instance Data)
和對齊填充(Padding)
。編程
虛擬機的對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據
,如 hashCode
、GC分代年齡
、鎖狀態標誌
、線程持有的鎖
、偏向線程ID
、偏向時間戳
等。這部分數據的長度在 32 位和 64 位的虛擬機(未開啓指針壓縮)中分別爲 4B 和 8B ,官方稱之爲 」Mark Word」
。數組
對象的另外一部分是類型指針(kclass)
,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是那個類的實例。另外若是對象是一個 Java 數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,由於虛擬機能夠經過普通 Java 對象的元數據信息肯定 Java 對象的大小,可是從數組的元數據中卻沒法肯定數組的大小。一樣,這部分數據的長度在 32 位和 64 的虛擬機(未開啓指針壓縮)中分別爲 4B 和 8B。bash
從 JDK 1.6 update14 開始,64 bit JVM 正式支持了 -XX:+UseCompressedOops
這個能夠壓縮指針,起到節約內存佔用的新參數。數據結構
若是 UseCompressedOops
是打開的,則如下對象的指針會被壓縮:架構
全部對象的 klass 屬性
全部對象指針實例的屬性
全部對象指針數組的元素(objArray)
複製代碼
由此咱們能夠計算出對象頭大小:編程語言
32位虛擬機對象頭大小= Mark Word(4B)+ kclass(4B) = 8B
64位虛擬機對象頭大小= Mark Word(8B)+ kclass(4B) = 12B
複製代碼
一個 Java 對象中的實例數據可能包括兩種,一是 8 種基本類型,二是實例數據也是一個對象,說到這裏不少人可能有個誤區:函數
基本類型?基本類型不是在棧上分配內存的嗎?怎麼要計算到分配在堆內存上對象的大小裏面去?工具
基本類型在棧上分配內存?其實並非,所謂「棧內存保存基本類型以及對象的引用(reference),堆內存保存對象」 只是一句不嚴謹的話,實際仔細研究起來,棧內存(更專業的術語叫作堆棧)做爲虛擬機做爲方法調用和方法執行的數據結構,可能保存五種信息:學習
局部變量表
操做數棧
動態連接
方法返回地址
附加信息
複製代碼
其中局部變量表中存儲了方法中的局部變量,可能爲 8 種基本類型或者 reference
也就是說,棧內存中保存的基本類型,都是方法中的局部變量,而若是基本類型做爲對象的實例變量,是在堆上分配空間的,此外,若是實例變量被final修飾,則既不在棧也不在堆上分配空間,而是分配到常量池裏面。
8 種基本類型和 reference 大小在虛擬機上都是固定的,見下表
Primitive Type | Memory Required(bytes) |
---|---|
boolean | 1 |
byte | 1 |
short | 2 |
char | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
Reference | 4 |
因爲虛擬機內存管理體系要求 Java 對象內存起始地址必須爲 8 的整數倍,換句話說,Java 對象大小必須爲 8 的整數倍,當對象頭+實例數據大小不爲 8 的整數倍時,將會使用Padding機制進行填充,譬如, 64 位虛擬機上 new Object() 實際大小爲:
Mark Word(8B)+ kclass(4B)[開啓指針壓縮] = 12B
但因爲Padding機制,實際佔用空間爲: Mark Word(8B)+ kclass(4B)[開啓指針壓縮]+Padding(4B) = 16B
Java 中數組也是一種對象,數組的大小與普通 Java 對象相比多了數組長度的信息(4B),即一個數組對象大小爲 Mark Word(8B)+ kclass(4B)[開啓指針壓縮] + 數組長度(4B) = 16B
如今咱們已經知道了一個 Java 對象的大小 = 對象頭 + 實例數據 + Padding ,如今,咱們驗證一下計算結果,google 到一個 Instrumentation
恰好能夠計算對象大小
Instrumentation
是 Java SE 5 引入的特性,使用 Instrumentation
,開發者能夠構建一個獨立於應用程序的代理程序(Agent),用來監測和協助運行在 JVM 上的程序,甚至可以實現字節碼修改技術。簡單的說,Instrumentation
實現了一個虛擬機層面的 AOP 。
本文不涉及 Instrumentation
的複雜應用,咱們只使用 Instrumentation
其中一個 getObjectSize()
方法獲取對象大小。
使用 Instrumentation
須要使用 javaagent 技術, 簡單說就是運行一個帶 main 函數的類時能夠經過 –javaagent 參數指定一個特定的 jar 文件(包含 Instrumentation
代理)來啓動 Instrumentation
的代理程序。具體分爲三步:
其中 premain 注入 Instrumentation ,sizeOf 用來計算對象佔用空間
ObjectShallowSize.java:
package sizeof;
import java.lang.instrument.Instrumentation;
public class ObjectShallowSize {
private static Instrumentation inst;
public static void premain(String agentArgs, Instrumentation instP){
inst = instP;
}
public static long sizeOf(Object obj){
return inst.getObjectSize(obj);
}
}
複製代碼
在 ObjectShallowSize.java 路徑下新建 /META-INF/MANIFEST.MF 指定 Premain-Class 內容爲:
Manifest-Version: 1.0
Premain-Class: sizeof.ObjectShallowSize
複製代碼
而後編譯,打包
javac -d . ObjectShallowSize.java
jar cvfm java-agent-sizeof.jar META-INF/MANIFEST.MF .
複製代碼
編寫一個測試模版類 ObjectSizeTest.java ,使用
java -javaagent:java-agent-sizeof.jar ObjectSizeTest
複製代碼
來運行程序
ObjectSizeTest.java 代碼以下:
package sizeof;
public class ObjectSizeTest {
public static void main(String[] args) {
System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest));
}
}
複製代碼
ObjectSizeTest 沒有實例變量,理論計算
ObjectSizeTest大小 = Mark Word(8B)+ kclass(4B) [開啓指針壓縮]+Padding(4B) = 16B
爲了方便,咱們在 IDEA 中驗證一下,導入剛纔的 ObjectSizeTest 類,指定 JVM 參數如圖
運行結果爲 16B,和咱們猜測一致
接下來咱們在模版類中添加幾個實例變量驗證下
package sizeof;
public class ObjectSizeTest {
private int i;
public static void main(String[] args) {
System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest()));
}
}
複製代碼
理論值:Mark Word(8B)+ kclass(4B) + i(4B) = 16B
實際值:16B
package sizeof;
public class ObjectSizeTest {
private int i;
private int j;
public static void main(String[] args) {
System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest()));
}
}
複製代碼
理論值:Mark Word(8B)+ kclass(4B) + i(4B) + j(4B)+Padding(4B) = 24B
實際值:24B
package sizeof;
public class ObjectSizeTest {
private int i;
private int j;
private String s;
private boolean aBoolean;
private char c;
public static void main(String[] args) {
System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest()));
}
}
複製代碼
理論值:Mark Word(8B)+ kclass(4B) + i(4B) + j(4B) + s(4B) + aBoolean(1B) + c(2B) + Paddding(5B) = 32B
實際值:32B
package sizeof;
public class ObjectSizeTest {
private String s; // 4
private int i1; // 4
private byte b1; // 1
private byte b2; // 1
private int i2;// 4
private Object obj; //4
private byte b3; // 1
public static void main(String[] args) {
System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest()));
}
}
複製代碼
理論值:Mark Word(8B)+ kclass(4B) + s(4B) + i1(4B) + b1(1B) + b2(1B) + 2(padding) + i2(4B) + obj(4B)+ b3(1B) + Paddding(7B) = 40B
實際值:32B
納尼?這裏爲何理論值和實際值不一致?
事實上,HotSpot建立的對象的字段會先按照給定順序排列一下,默認的順序以下,從長到短排列,引用排最後: long/double --> int/float --> short/char --> byte/boolean --> Reference
這個順序可使用JVM參數: -XX:FieldsAllocationSylte = 0
(默認是1)來改變。
按照這種方法咱們來從新計算下對象大小
Mark Word(8B)+ kclass(4B) + i1(4B) + i2(4B) + b1(1B) + b2(1B) + b3(1B) + Paddding(1B) + s(4B) + obj(4B) = 32B
與預期值一致。
前面咱們計算 Java 對象大小時,對於實例變量爲對象的,只計算了其reference的大小,實際應該也將實例變量自己計算在內,咱們能夠經過反射機制取出 Java 對象中實例變量,遞歸計算累加出實際大小。 yueyemaitian.iteye.com/blog/203304… 已經提供了現成的程序以下,使用fullSizeOf()方法便可計算出 Java 對象實際大小。
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
/**
* 對象佔用字節大小工具類
* <p>
*
* @author tianmai.fh
* @date 2014-03-18 11:29
*/
public class SizeOfObject {
static Instrumentation inst;
public static void premain(String args, Instrumentation instP) {
inst = instP;
}
/**
* 直接計算當前對象佔用空間大小,包括當前類及超類的基本類型實例字段大小、<br></br>
* 引用類型實例字段引用大小、實例基本類型數組總佔用空間、實例引用類型數組引用自己佔用空間大小;<br></br>
* 可是不包括超類繼承下來的和當前類聲明的實例引用字段的對象自己的大小、實例引用數組引用的對象自己的大小 <br></br>
*
* @param obj
* @return
*/
public static long sizeOf(Object obj) {
return inst.getObjectSize(obj);
}
/**
* 遞歸計算當前對象佔用空間總大小,包括當前類和超類的實例字段大小以及實例字段引用對象大小
*
* @param objP
* @return
* @throws IllegalAccessException
*/
public static long fullSizeOf(Object objP) throws IllegalAccessException {
Set<Object> visited = new HashSet<Object>();
Deque<Object> toBeQueue = new ArrayDeque<Object>();
toBeQueue.add(objP);
long size = 0L;
while (toBeQueue.size() > 0) {
Object obj = toBeQueue.poll();
//sizeOf的時候已經計基本類型和引用的長度,包括數組
size += skipObject(visited, obj) ? 0L : sizeOf(obj);
Class<?> tmpObjClass = obj.getClass();
if (tmpObjClass.isArray()) {
//[I , [F 基本類型名字長度是2
if (tmpObjClass.getName().length() > 2) {
for (int i = 0, len = Array.getLength(obj); i < len; i++) {
Object tmp = Array.get(obj, i);
if (tmp != null) {
//非基本類型須要深度遍歷其對象
toBeQueue.add(Array.get(obj, i));
}
}
}
} else {
while (tmpObjClass != null) {
Field[] fields = tmpObjClass.getDeclaredFields();
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers()) //靜態不計
|| field.getType().isPrimitive()) { //基本類型不重複計
continue;
}
field.setAccessible(true);
Object fieldValue = field.get(obj);
if (fieldValue == null) {
continue;
}
toBeQueue.add(fieldValue);
}
tmpObjClass = tmpObjClass.getSuperclass();
}
}
}
return size;
}
/**
* String.intern的對象不計;計算過的不計,也避免死循環
*
* @param visited
* @param obj
* @return
*/
static boolean skipObject(Set<Object> visited, Object obj) {
if (obj instanceof String && obj == ((String) obj).intern()) {
return true;
}
return visited.contains(obj);
}
}
複製代碼
參考資料:
感謝閱讀,原創不易,若有啓發,點個贊吧!這將是我寫做的最強動力!本文不一樣步發佈於不止於技術的技術公衆號
Nauyus
,主要分享一些編程語言,架構設計,思惟認知類文章, 2019年12月起開啓周更模式,歡迎關注,共同窗習成長!