測(cè)量Java對(duì)象所占內(nèi)存大小方式
背景:
相信大部分人都不會(huì)關(guān)注這個(gè)問題吧,只有一些偏執(zhí)狂才會(huì)抓著這些不放,我們平時(shí)寫代碼時(shí)經(jīng)常會(huì)new ArrayList<>(),new String()之類的,那么這些剛new出來的對(duì)象在內(nèi)存中占用多大空間呢?
方法一
設(shè)置-Xms和-Xmx的大小,然后在程序中循環(huán)new對(duì)象,直到發(fā)生OOM異常,記錄下此時(shí)new了多少個(gè)對(duì)象,大家覺得這種方法可靠不?
下面放上設(shè)置參數(shù)以及測(cè)試代碼。
/**
* 研究new出的對(duì)象大小
* -Xms1m -Xmx1m -XX:+PrintGCDetails
*/
public class TestObjectSize {
int i = 0;
@Test
public void testObjectSize() {
List<Object> list = new ArrayList<>();
try {
while (true) {
list.add(new Object());
i++;
}
} catch (Exception e) {
System.out.println(i);
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
運(yùn)行結(jié)果:
14053
java.lang.OutOfMemoryError: Java heap space根據(jù)上面的結(jié)果可以算出每個(gè)Object對(duì)象大小1024*1024/14053=74.61Byte,這個(gè)結(jié)果好像挺意外,個(gè)人覺得有點(diǎn)大了,下面來分析一下,由于設(shè)置了-XX:+PrintGCDetails參數(shù),控制臺(tái)實(shí)時(shí)輸出GC情況,
如下圖:

可以發(fā)現(xiàn),在new對(duì)象的過程中執(zhí)行了六次GC(Minor GC),也就是說JVM在青年代區(qū)回收了6次垃圾,執(zhí)行了N次FullGC,F(xiàn)ullGC會(huì)回收PSYoungGen、ParOldGen、Metaspace,這么看來這個(gè)結(jié)果是不可可信的,因?yàn)榛厥盏倪^程中,銷毀了很多對(duì)象,但是計(jì)數(shù)器一直是增加的,所以74.61Byte這個(gè)結(jié)果毫無疑問是偏大的。
【錯(cuò)誤糾正】
經(jīng)過同事的糾正,回收過程中Object對(duì)象不會(huì)被銷毀,因?yàn)楸环诺搅薼ist中,list并不會(huì)銷毀,因此在執(zhí)行GC過程中,只是在不斷地搬家,從一個(gè)survivor到另一個(gè)survivor.
方法二
利用java.lang.instrument.Instrumentation這個(gè)interface的特有屬性,可以在JVM運(yùn)行過程中實(shí)時(shí)測(cè)量對(duì)象大小。簡(jiǎn)單介紹下這個(gè)類,利用 Java 代碼,即 java.lang.instrument 做動(dòng)態(tài) Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能從本地代碼中解放出來,使之可以用 Java 代碼的方式解決問題。使用 Instrumentation,開發(fā)者可以構(gòu)建一個(gè)獨(dú)立于應(yīng)用程序的代理程序(Agent),用來監(jiān)測(cè)和協(xié)助運(yùn)行在 JVM 上的程序,甚至能夠替換和修改某些類的定義。有了這樣的功能,開發(fā)者就可以實(shí)現(xiàn)更為靈活的運(yùn)行時(shí)虛擬機(jī)監(jiān)控和 Java 類操作了,這樣的特性實(shí)際上提供了一種虛擬機(jī)級(jí)別支持的 AOP 實(shí)現(xiàn)方式,使得開發(fā)者無需對(duì) JDK 做任何升級(jí)和改動(dòng),就可以實(shí)現(xiàn)某些 AOP 的功能了。
在 Java SE 6 里面,instrumentation 包被賦予了更強(qiáng)大的功能:?jiǎn)?dòng)后的 instrument、本地代碼(native code)instrument,以及動(dòng)態(tài)改變 classpath 等等。這些改變,意味著 Java 具有了更強(qiáng)的動(dòng)態(tài)控制、解釋能力,它使得 Java 語言變得更加靈活多變。
在 Java SE6 里面,最大的改變使運(yùn)行時(shí)的 Instrumentation 成為可能。在 Java SE 5 中,Instrument 要求在運(yùn)行前利用命令行參數(shù)或者系統(tǒng)參數(shù)來設(shè)置代理類,在實(shí)際的運(yùn)行之中,虛擬機(jī)在初始化之時(shí)(在絕大多數(shù)的 Java 類庫(kù)被載入之前),instrumentation 的設(shè)置已經(jīng)啟動(dòng),并在虛擬機(jī)中設(shè)置了回調(diào)函數(shù),檢測(cè)特定類的加載情況,并完成實(shí)際工作。但是在實(shí)際的很多的情況下,我們沒有辦法在虛擬機(jī)啟動(dòng)之時(shí)就為其設(shè)定代理,這樣實(shí)際上限制了 instrument 的應(yīng)用。而 Java SE 6 的新特性改變了這種情況,通過 Java Tool API 中的 attach 方式,我們可以很方便地在運(yùn)行過程中動(dòng)態(tài)地設(shè)置加載代理類,以達(dá)到 instrumentation 的目的。
另外,對(duì) native 的 Instrumentation 也是 Java SE 6 的一個(gè)嶄新的功能,這使以前無法完成的功能 —— 對(duì) native 接口的 instrumentation 可以在 Java SE 6 中,通過一個(gè)或者一系列的 prefix 添加而得以完成。
最后,Java SE 6 里的 Instrumentation 也增加了動(dòng)態(tài)添加 class path 的功能。所有這些新的功能,都使得 instrument 包的功能更加豐富,從而使 Java 語言本身更加強(qiáng)大。
上面這段摘自IBM Developers,讓我自己解釋,有點(diǎn)不好解釋,大家看權(quán)威解答就好。
下面這段是來自Java8 API,英文好的可以讀一下。
/**
* This class provides services needed to instrument Java
* programming language code.
* Instrumentation is the addition of byte-codes to methods for the
* purpose of gathering data to be utilized by tools.
* Since the changes are purely additive, these tools do not modify
* application state or behavior.
* Examples of such benign tools include monitoring agents, profilers,
* coverage analyzers, and event loggers.
*
* <P>
* There are two ways to obtain an instance of the
* <code>Instrumentation</code> interface:
*
* <ol>
* <li><p> When a JVM is launched in a way that indicates an agent
* class. In that case an <code>Instrumentation</code> instance
* is passed to the <code>premain</code> method of the agent class.
* </p></li>
* <li><p> When a JVM provides a mechanism to start agents sometime
* after the JVM is launched. In that case an <code>Instrumentation</code>
* instance is passed to the <code>agentmain</code> method of the
* agent code. </p> </li>
* </ol>
* <p>
* These mechanisms are described in the
* {@linkplain java.lang.instrument package specification}.
* <p>
* Once an agent acquires an <code>Instrumentation</code> instance,
* the agent may call methods on the instance at any time.
*
* @since 1.5
*/知道了這個(gè)類可以在JVM運(yùn)行過程中測(cè)量對(duì)象大小,下面談?wù)勅绾问褂茫@個(gè)Instrumentation接口不太友好,筆者跟蹤了下這個(gè)接口,發(fā)現(xiàn)它的實(shí)現(xiàn)類是在rt.jar包里的,簡(jiǎn)單介紹下rt.jar,大家都知道是極為重要的一個(gè)文件,rt是runtime的縮寫,即運(yùn)行時(shí)的意思。
是java程序在運(yùn)行時(shí)必不可少的文件。里面包含了java程序員常用的包,如java.lang,java.util,java.io,java.net, java.applet等。
也就是說,我們想得到Instrumentation的實(shí)例,必須得在JVM運(yùn)行過程中才能取得,翻開源碼構(gòu)造方法是private類型,沒有任何getInstance的方法,寫這個(gè)類干嘛?
看來這個(gè)只能被JVM自己給初始化了,那么怎么將它自己初始化的東西取出來用呢,唯一能想到的就是javaagent代理,我說這是代理技術(shù),不知道準(zhǔn)不準(zhǔn)確,若不正確請(qǐng)大家指正。
Step1:先創(chuàng)建一個(gè)用于測(cè)試對(duì)象大小的處理類(代理類)
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;
public class MySizeOf {
static Instrumentation inst;
public static void premain(String agentArgs, Instrumentation instP) {
inst = instP;
}
public static long sizeOf(Object o) {
if(inst == null) {
throw new IllegalStateException("Can not access instrumentation environment.\n" +
"Please check if jar file containing SizeOfAgent class is \n" +
"specified in the java's \"-javaagent\" command line argument.");
}
return inst.getObjectSize(o);
}
}這就是agent的代碼,此時(shí)我們要將上面這個(gè)類編譯后打包為一個(gè)jar文件,并且在其包內(nèi)部的META-INF/MANIFEST.MF文件中增加一行:Premain-Class: MySizeOf代表執(zhí)行代理的全名,這里的類名稱是沒有package的,如果你有package,那么就寫全名,假設(shè)打包完的jar包名稱為agent.jar,打包的過程簡(jiǎn)單說一下,jar命令參考文章最后的Extra部分。
- jar cf agent.jar MySizeOf.java //將MySizeOf.java打成jar包
- 修改jar包中的META-INF/MANIFEST.MF文件,增加一行:Premain-Class: MySizeOf,這里的類名稱是沒有package的,如果你有package,那么就寫全名。
Step2:編寫測(cè)試類TestSize
這里我打算測(cè)一下Interger、String、ArrayList、Object以及Long類型的對(duì)象大小。
代碼如下:
public class TestSize {
public static void main(String []args) {
System.out.println("一個(gè)Interger對(duì)象大小為:"+MySizeOf.sizeOf(new Integer(1)));
System.out.println("一個(gè)String對(duì)象大小為:"+MySizeOf.sizeOf(new String("a")));
System.out.println("一個(gè)String對(duì)象大?。P(guān)閉指針壓縮)為:"+MySizeOf.fullSizeOf(new String("a")));
System.out.println("一個(gè)char對(duì)象大小為:"+MySizeOf.sizeOf(new char[1]));
System.out.println("一個(gè)ArrayList對(duì)象大小為:"+MySizeOf.sizeOf(new ArrayList<>()));
System.out.println("一個(gè)Object對(duì)象大小為:"+MySizeOf.sizeOf(new Object()));
System.out.println("一個(gè)Long對(duì)象大小為:"+MySizeOf.sizeOf(new Long(10000000000L)));
}
}Step3:執(zhí)行TestSize,進(jìn)行測(cè)試
這里需要注意一點(diǎn),這個(gè)Test方法不能在IDE環(huán)境中執(zhí)行,因?yàn)槟銦o法使用Instrumentation的實(shí)例,必須采用古老的javac,java命令先編譯再執(zhí)行,請(qǐng)按照以下步驟執(zhí)行,
// step1,編譯TestSize.java,并將agent.jar包中的Instrumentation的實(shí)例引入ClassPath,這樣執(zhí)行TestSize.java時(shí)才能引用Instrumentation實(shí)例。 javac -classpath agent.jar TestSize.java //step2,執(zhí)行TestSize,使用-javaagent:agent.jar,意思是使用agent.jar作為代理,用到agent技術(shù) java -javaagent:agent.jar TestSize
運(yùn)行結(jié)果:
一個(gè)Interger對(duì)象大小為:16
一個(gè)String對(duì)象大小為:24
一個(gè)String對(duì)象大小(關(guān)閉指針壓縮)為:48
一個(gè)char對(duì)象大小為:24
一個(gè)ArrayList對(duì)象大小為:24
一個(gè)Object對(duì)象大小為:16
一個(gè)Long對(duì)象大小為:24
在網(wǎng)上看到一個(gè)更全面的Agent測(cè)試類,里面提供了不少測(cè)量方法,提供給大家,
public class MySizeOf {
static Instrumentation inst;
public static void premain(String agentArgs, Instrumentation instP) {
inst = instP;
}
public static long sizeOf(Object o) {
if(inst == null) {
throw new IllegalStateException("Can not access instrumentation environment.\n" +
"Please check if jar file containing SizeOfAgent class is \n" +
"specified in the java's \"-javaagent\" command line argument.");
}
return inst.getObjectSize(o);
}
/**
* 遞歸計(jì)算當(dāng)前對(duì)象占用空間總大小,包括當(dāng)前類和超類的實(shí)例字段大小以及實(shí)例字段引用對(duì)象大小
*/
public static long fullSizeOf(Object obj) {//深入檢索對(duì)象,并計(jì)算大小
Map<Object, Object> visited = new IdentityHashMap<Object, Object>();
Stack<Object> stack = new Stack<Object>();
long result = internalSizeOf(obj, stack, visited);
while (!stack.isEmpty()) {//通過棧進(jìn)行遍歷
result += internalSizeOf(stack.pop(), stack, visited);
}
visited.clear();
return result;
}
//判定哪些是需要跳過的
private static boolean skipObject(Object obj, Map<Object, Object> visited) {
if (obj instanceof String) {
if (obj == ((String) obj).intern()) {
return true;
}
}
return (obj == null) || visited.containsKey(obj);
}
private static long internalSizeOf(Object obj, Stack<Object> stack, Map<Object, Object> visited) {
if (skipObject(obj, visited)) {//跳過常量池對(duì)象、跳過已經(jīng)訪問過的對(duì)象
return 0;
}
visited.put(obj, null);//將當(dāng)前對(duì)象放入棧中
long result = 0;
result += sizeOf(obj);
Class <?>clazz = obj.getClass();
if (clazz.isArray()) {//如果數(shù)組
if(clazz.getName().length() != 2) {// skip primitive type array
int length = Array.getLength(obj);
for (int i = 0; i < length; i++) {
stack.add(Array.get(obj, i));
}
}
return result;
}
return getNodeSize(clazz , result , obj , stack);
}
//這個(gè)方法獲取非數(shù)組對(duì)象自身的大小,并且可以向父類進(jìn)行向上搜索
private static long getNodeSize(Class <?>clazz , long result , Object obj , Stack<Object> stack) {
while (clazz != null) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (!Modifier.isStatic(field.getModifiers())) {//這里拋開靜態(tài)屬性
if (field.getType().isPrimitive()) {//這里拋開基本關(guān)鍵字(因?yàn)榛娟P(guān)鍵字在調(diào)用java默認(rèn)提供的方法就已經(jīng)計(jì)算過了)
continue;
}else {
field.setAccessible(true);
try {
Object objectToAdd = field.get(obj);
if (objectToAdd != null) {
stack.add(objectToAdd);//將對(duì)象放入棧中,一遍彈出后繼續(xù)檢索
}
} catch (IllegalAccessException ex) {
assert false;
}
}
}
}
clazz = clazz.getSuperclass();//找父類class,直到?jīng)]有父類
}
return result;
}
}到這里就結(jié)束了,有幾個(gè)疑問,大家有興趣可以關(guān)注下:
1.修改MANIFEST文件應(yīng)該還有其他方法,好像是jar cmf manifest-addition jar-file input-file(s),大家有興趣可以關(guān)注下
2.如何在IDE中實(shí)現(xiàn)以上這些步驟,javac和java畢竟是遠(yuǎn)古時(shí)代的東西了。
3.Instrumentation 的最大作用再說一說,就是類定義動(dòng)態(tài)改變和操作。在 Java SE 5 及其后續(xù)版本當(dāng)中,開發(fā)者可以在一個(gè)普通 Java 程序(帶有 main 函數(shù)的 Java 類)運(yùn)行時(shí),通過 – javaagent參數(shù)指定一個(gè)特定的 jar 文件(包含 Instrumentation 代理)來啟動(dòng) Instrumentation 的代理程序,然后利用Instrumentation的實(shí)現(xiàn)類做一列測(cè)量,這個(gè)有點(diǎn)類似AOP
4.先想這么多,想到更多問題再補(bǔ)充。
Extra:
jar操作指令
| 操作 | 命令 | Cool |
|---|---|---|
| 創(chuàng)建一個(gè)JAR文件 | jar cf jar-file input-file(s) | |
| 查看JAR文件的內(nèi)容 | jar tf jar-file | |
| 導(dǎo)出JAR文件 | jar xf jar-file | |
| 導(dǎo)出JAR文件中制定的文件包 | jar xf jar-file archived-file(s) | |
| 運(yùn)行JAR文件中的應(yīng)用 | jre -cp app.jar MainClass | |
| 運(yùn)行用JAR格式打包的應(yīng)用 | java -jar app.jar | |
| 調(diào)用一個(gè)打包成JAR的applet |
方法三
使用JOL 工具來查看一個(gè)對(duì)象的大小和分布
這個(gè)和前兩個(gè)方法不太一樣,是用來測(cè)一個(gè)JVM中對(duì)象的內(nèi)部分布情況的,而且測(cè)得是引用的大小,Object obj中,這個(gè)引用obj也是占大小的。
這個(gè)方法作為拓展即可。
JOL (Java Object Layout) is the tiny toolbox to analyze object layout schemes in JVMs.
These tools are using Unsafe, JVMTI, and Serviceability Agent (SA) heavily to decoder the actual object layout, footprint, and references.
This makes JOL much more accurate than other tools relying on heap dumps, specification assumptions, etc.
該工具官網(wǎng):
http://openjdk.java.NET/projects/code-tools/jol/
下載jar包后保持如下的相對(duì)路徑 和你要測(cè)試的類在一起.
編寫測(cè)試對(duì)象的類:VolatileLong
public final class VolatileLong {
/**
* 大小計(jì)算:
* long 8 字節(jié)
* [1]java對(duì)象頭: 32位 :8 byte 64位:12 byte
* 所以總共: 6 個(gè)填充 * 8 byte + 8 (value) + 8 (對(duì)象頭) = 64 byte
* 還有采用如下方式的
* long p1, p2, p3, p4, p5, p6, p7; // cache line padding -> 7 *8 = 56 字節(jié)
* long value; -> 8 字節(jié)
* long p8, p9, p10, p11, p12, p13, p14; // cache line padding -> 7*8 = 56 字節(jié)
*
* java.util.concurrent.Exchanger.Slot<V>
* // Improve likelihood of isolation on <= 64 byte cache lines
* long q0, q1, q2, q3, q4, q5, q6, q7, q8, q9, qa, qb, qc, qd, qe; 15 * 8
* 不知道為啥是這樣實(shí)現(xiàn):因?yàn)楦割愡€有一個(gè)Long值 所以總的來說已經(jīng)超過128了
*
*/
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; // comment out
//objectsize = 6*8 + 8 + 4 =
}執(zhí)行下面指令:
java -jar jol-cli-0.9-full.jar internals -cp . VolatileLong
//jar包必須帶上全路徑,先把class文件編譯出來,然后再執(zhí)行,因?yàn)榇嗣钪唤邮躢lass方式,若在IDE中測(cè)試,去掉報(bào)名,不然會(huì)報(bào)找不到類的錯(cuò)誤,如下
java.lang.NoClassDefFoundError: ObjectLoc (wrong name: com/pingan/jvm/objectsize/ObjectLoc)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at org.openjdk.jol.util.ClassUtils.loadClass(ClassUtils.java:70)
at org.openjdk.jol.operations.ClasspathedOperation.run(ClasspathedOperation.java:76)
at org.openjdk.jol.Main.main(Main.java:60)
運(yùn)行結(jié)果:
VolatileLong object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 85 07 02 f8 (10000101 00000111 00000010 11111000) (-134084731)
12 4 (alignment/padding gap)
16 8 long VolatileLong.value 0
24 8 long VolatileLong.p1 0
32 8 long VolatileLong.p2 0
40 8 long VolatileLong.p3 0
48 8 long VolatileLong.p4 0
56 8 long VolatileLong.p5 0
64 8 long VolatileLong.p6 0
Instance size: 72 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java不解壓直接讀取壓縮包中文件的實(shí)現(xiàn)方法
這篇文章主要介紹了java不解壓直接讀取壓縮包中文件的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
intellij idea旗艦版解決學(xué)生無法注冊(cè)問題詳解
這篇文章主要介紹了intellij idea旗艦版解決學(xué)生無法注冊(cè)問題詳解,文中通過圖文示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
Spring Boot使用模板freemarker的示例代碼
本篇文章主要介紹了Spring Boot使用模板freemarker的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10
SpringMVC處理數(shù)據(jù)輸出的實(shí)例代碼
這篇文章主要給大家介紹了關(guān)于SpringMVC處理數(shù)據(jù)輸出的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05
springboot application無法使用$獲取pom變量的問題及解決
這篇文章主要介紹了springboot application無法使用$獲取pom變量的問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02

