測量Java對象所占內(nèi)存大小方式
背景:
相信大部分人都不會關(guān)注這個問題吧,只有一些偏執(zhí)狂才會抓著這些不放,我們平時寫代碼時經(jīng)常會new ArrayList<>(),new String()之類的,那么這些剛new出來的對象在內(nèi)存中占用多大空間呢?
方法一
設(shè)置-Xms和-Xmx的大小,然后在程序中循環(huán)new對象,直到發(fā)生OOM異常,記錄下此時new了多少個對象,大家覺得這種方法可靠不?
下面放上設(shè)置參數(shù)以及測試代碼。
/** * 研究new出的對象大小 * -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); } } } 運行結(jié)果: 14053 java.lang.OutOfMemoryError: Java heap space
根據(jù)上面的結(jié)果可以算出每個Object對象大小1024*1024/14053=74.61Byte,這個結(jié)果好像挺意外,個人覺得有點大了,下面來分析一下,由于設(shè)置了-XX:+PrintGCDetails參數(shù),控制臺實時輸出GC情況,
如下圖:
可以發(fā)現(xiàn),在new對象的過程中執(zhí)行了六次GC(Minor GC),也就是說JVM在青年代區(qū)回收了6次垃圾,執(zhí)行了N次FullGC,F(xiàn)ullGC會回收PSYoungGen、ParOldGen、Metaspace,這么看來這個結(jié)果是不可可信的,因為回收的過程中,銷毀了很多對象,但是計數(shù)器一直是增加的,所以74.61Byte這個結(jié)果毫無疑問是偏大的。
【錯誤糾正】
經(jīng)過同事的糾正,回收過程中Object對象不會被銷毀,因為被放到了list中,list并不會銷毀,因此在執(zhí)行GC過程中,只是在不斷地搬家,從一個survivor到另一個survivor.
方法二
利用java.lang.instrument.Instrumentation這個interface的特有屬性,可以在JVM運行過程中實時測量對象大小。簡單介紹下這個類,利用 Java 代碼,即 java.lang.instrument 做動態(tài) Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能從本地代碼中解放出來,使之可以用 Java 代碼的方式解決問題。使用 Instrumentation,開發(fā)者可以構(gòu)建一個獨立于應(yīng)用程序的代理程序(Agent),用來監(jiān)測和協(xié)助運行在 JVM 上的程序,甚至能夠替換和修改某些類的定義。有了這樣的功能,開發(fā)者就可以實現(xiàn)更為靈活的運行時虛擬機監(jiān)控和 Java 類操作了,這樣的特性實際上提供了一種虛擬機級別支持的 AOP 實現(xiàn)方式,使得開發(fā)者無需對 JDK 做任何升級和改動,就可以實現(xiàn)某些 AOP 的功能了。
在 Java SE 6 里面,instrumentation 包被賦予了更強大的功能:啟動后的 instrument、本地代碼(native code)instrument,以及動態(tài)改變 classpath 等等。這些改變,意味著 Java 具有了更強的動態(tài)控制、解釋能力,它使得 Java 語言變得更加靈活多變。
在 Java SE6 里面,最大的改變使運行時的 Instrumentation 成為可能。在 Java SE 5 中,Instrument 要求在運行前利用命令行參數(shù)或者系統(tǒng)參數(shù)來設(shè)置代理類,在實際的運行之中,虛擬機在初始化之時(在絕大多數(shù)的 Java 類庫被載入之前),instrumentation 的設(shè)置已經(jīng)啟動,并在虛擬機中設(shè)置了回調(diào)函數(shù),檢測特定類的加載情況,并完成實際工作。但是在實際的很多的情況下,我們沒有辦法在虛擬機啟動之時就為其設(shè)定代理,這樣實際上限制了 instrument 的應(yīng)用。而 Java SE 6 的新特性改變了這種情況,通過 Java Tool API 中的 attach 方式,我們可以很方便地在運行過程中動態(tài)地設(shè)置加載代理類,以達到 instrumentation 的目的。
另外,對 native 的 Instrumentation 也是 Java SE 6 的一個嶄新的功能,這使以前無法完成的功能 —— 對 native 接口的 instrumentation 可以在 Java SE 6 中,通過一個或者一系列的 prefix 添加而得以完成。
最后,Java SE 6 里的 Instrumentation 也增加了動態(tài)添加 class path 的功能。所有這些新的功能,都使得 instrument 包的功能更加豐富,從而使 Java 語言本身更加強大。
上面這段摘自IBM Developers,讓我自己解釋,有點不好解釋,大家看權(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 */
知道了這個類可以在JVM運行過程中測量對象大小,下面談?wù)勅绾问褂?,這個Instrumentation接口不太友好,筆者跟蹤了下這個接口,發(fā)現(xiàn)它的實現(xiàn)類是在rt.jar包里的,簡單介紹下rt.jar,大家都知道是極為重要的一個文件,rt是runtime的縮寫,即運行時的意思。
是java程序在運行時必不可少的文件。里面包含了java程序員常用的包,如java.lang,java.util,java.io,java.net, java.applet等。
也就是說,我們想得到Instrumentation的實例,必須得在JVM運行過程中才能取得,翻開源碼構(gòu)造方法是private類型,沒有任何getInstance的方法,寫這個類干嘛?
看來這個只能被JVM自己給初始化了,那么怎么將它自己初始化的東西取出來用呢,唯一能想到的就是javaagent代理,我說這是代理技術(shù),不知道準不準確,若不正確請大家指正。
Step1:先創(chuàng)建一個用于測試對象大小的處理類(代理類)
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的代碼,此時我們要將上面這個類編譯后打包為一個jar文件,并且在其包內(nèi)部的META-INF/MANIFEST.MF文件中增加一行:Premain-Class: MySizeOf代表執(zhí)行代理的全名,這里的類名稱是沒有package的,如果你有package,那么就寫全名,假設(shè)打包完的jar包名稱為agent.jar,打包的過程簡單說一下,jar命令參考文章最后的Extra部分。
- jar cf agent.jar MySizeOf.java //將MySizeOf.java打成jar包
- 修改jar包中的META-INF/MANIFEST.MF文件,增加一行:Premain-Class: MySizeOf,這里的類名稱是沒有package的,如果你有package,那么就寫全名。
Step2:編寫測試類TestSize
這里我打算測一下Interger、String、ArrayList、Object以及Long類型的對象大小。
代碼如下:
public class TestSize { public static void main(String []args) { System.out.println("一個Interger對象大小為:"+MySizeOf.sizeOf(new Integer(1))); System.out.println("一個String對象大小為:"+MySizeOf.sizeOf(new String("a"))); System.out.println("一個String對象大?。P(guān)閉指針壓縮)為:"+MySizeOf.fullSizeOf(new String("a"))); System.out.println("一個char對象大小為:"+MySizeOf.sizeOf(new char[1])); System.out.println("一個ArrayList對象大小為:"+MySizeOf.sizeOf(new ArrayList<>())); System.out.println("一個Object對象大小為:"+MySizeOf.sizeOf(new Object())); System.out.println("一個Long對象大小為:"+MySizeOf.sizeOf(new Long(10000000000L))); } }
Step3:執(zhí)行TestSize,進行測試
這里需要注意一點,這個Test方法不能在IDE環(huán)境中執(zhí)行,因為你無法使用Instrumentation的實例,必須采用古老的javac,java命令先編譯再執(zhí)行,請按照以下步驟執(zhí)行,
// step1,編譯TestSize.java,并將agent.jar包中的Instrumentation的實例引入ClassPath,這樣執(zhí)行TestSize.java時才能引用Instrumentation實例。 javac -classpath agent.jar TestSize.java //step2,執(zhí)行TestSize,使用-javaagent:agent.jar,意思是使用agent.jar作為代理,用到agent技術(shù) java -javaagent:agent.jar TestSize
運行結(jié)果:
一個Interger對象大小為:16
一個String對象大小為:24
一個String對象大?。P(guān)閉指針壓縮)為:48
一個char對象大小為:24
一個ArrayList對象大小為:24
一個Object對象大小為:16
一個Long對象大小為:24
在網(wǎng)上看到一個更全面的Agent測試類,里面提供了不少測量方法,提供給大家,
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); } /** * 遞歸計算當前對象占用空間總大小,包括當前類和超類的實例字段大小以及實例字段引用對象大小 */ public static long fullSizeOf(Object obj) {//深入檢索對象,并計算大小 Map<Object, Object> visited = new IdentityHashMap<Object, Object>(); Stack<Object> stack = new Stack<Object>(); long result = internalSizeOf(obj, stack, visited); while (!stack.isEmpty()) {//通過棧進行遍歷 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)) {//跳過常量池對象、跳過已經(jīng)訪問過的對象 return 0; } visited.put(obj, null);//將當前對象放入棧中 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); } //這個方法獲取非數(shù)組對象自身的大小,并且可以向父類進行向上搜索 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)鍵字(因為基本關(guān)鍵字在調(diào)用java默認提供的方法就已經(jīng)計算過了) continue; }else { field.setAccessible(true); try { Object objectToAdd = field.get(obj); if (objectToAdd != null) { stack.add(objectToAdd);//將對象放入棧中,一遍彈出后繼續(xù)檢索 } } catch (IllegalAccessException ex) { assert false; } } } } clazz = clazz.getSuperclass();//找父類class,直到?jīng)]有父類 } return result; } }
到這里就結(jié)束了,有幾個疑問,大家有興趣可以關(guān)注下:
1.修改MANIFEST文件應(yīng)該還有其他方法,好像是jar cmf manifest-addition jar-file input-file(s),大家有興趣可以關(guān)注下
2.如何在IDE中實現(xiàn)以上這些步驟,javac和java畢竟是遠古時代的東西了。
3.Instrumentation 的最大作用再說一說,就是類定義動態(tài)改變和操作。在 Java SE 5 及其后續(xù)版本當中,開發(fā)者可以在一個普通 Java 程序(帶有 main 函數(shù)的 Java 類)運行時,通過 – javaagent參數(shù)指定一個特定的 jar 文件(包含 Instrumentation 代理)來啟動 Instrumentation 的代理程序,然后利用Instrumentation的實現(xiàn)類做一列測量,這個有點類似AOP
4.先想這么多,想到更多問題再補充。
Extra:
jar操作指令
操作 | 命令 | Cool |
---|---|---|
創(chuàng)建一個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) | |
運行JAR文件中的應(yīng)用 | jre -cp app.jar MainClass | |
運行用JAR格式打包的應(yīng)用 | java -jar app.jar | |
調(diào)用一個打包成JAR的applet |
方法三
使用JOL 工具來查看一個對象的大小和分布
這個和前兩個方法不太一樣,是用來測一個JVM中對象的內(nèi)部分布情況的,而且測得是引用的大小,Object obj中,這個引用obj也是占大小的。
這個方法作為拓展即可。
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包后保持如下的相對路徑 和你要測試的類在一起.
編寫測試對象的類:VolatileLong
public final class VolatileLong { /** * 大小計算: * long 8 字節(jié) * [1]java對象頭: 32位 :8 byte 64位:12 byte * 所以總共: 6 個填充 * 8 byte + 8 (value) + 8 (對象頭) = 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 * 不知道為啥是這樣實現(xiàn):因為父類還有一個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í)行,因為此命令只接受class方式,若在IDE中測試,去掉報名,不然會報找不到類的錯誤,如下
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)
運行結(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é)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java詳解如何將excel數(shù)據(jù)轉(zhuǎn)為樹形
在平常的辦公工作中,excel數(shù)據(jù)的操作是最常見的需求,今天就來看一下通過Java如何來實現(xiàn)將excel數(shù)據(jù)轉(zhuǎn)為樹形,感興趣的朋友可以了解下2022-08-08MyBatis實現(xiàn)多表聯(lián)查的詳細代碼
這篇文章主要介紹了MyBatis如何實現(xiàn)多表聯(lián)查,通過實例代碼給大家介紹使用映射配置文件實現(xiàn)多表聯(lián)查,使用注解的方式實現(xiàn)多表聯(lián)查,需要的朋友可以參考下2022-08-08JAVA簡單工廠模式(從現(xiàn)實生活角度理解代碼原理)
本文主要介紹了JAVA簡單工廠模式(從現(xiàn)實生活角度理解代碼原理)的相關(guān)知識。具有很好的參考價值。下面跟著小編一起來看下吧2017-03-03在IDEA中配置tomcat并創(chuàng)建tomcat項目的圖文教程
這篇文章主要介紹了在IDEA中配置tomcat并創(chuàng)建tomcat項目的圖文教程,需要的朋友可以參考下2020-07-07MybatisPlus自動填充創(chuàng)建(更新)時間問題
在開發(fā)數(shù)據(jù)庫相關(guān)應(yīng)用時,手動設(shè)置創(chuàng)建和更新時間會導(dǎo)致代碼冗余,MybatisPlus提供了自動填充功能,通過實現(xiàn)MetaObjectHandler接口并重寫insertFill、updateFill方法,可以自動維護創(chuàng)建時間、更新時間等字段,極大簡化了代碼,這不僅提高了開發(fā)效率,也保證了數(shù)據(jù)的可追溯性2024-09-09Java實現(xiàn)DFA算法對敏感詞、廣告詞過濾功能示例
本篇文章主要介紹了Java實現(xiàn)DFA算法對敏感詞、廣告詞過濾功能示例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11