您的位置:首页 > 编程语言 > Java开发

测量Java对象所占内存大小

2017-12-17 00:40 337 查看
背景: 相信大部分人都不会关注这个问题吧,只有一些偏执狂才会抓着这些不放,我们平时写代码时经常会new ArrayList<>(),new String()之类的,那么这些刚new出来的对象在内存中占用多大空间呢?随着作者一起去看看吧!

方法一

设置-Xms和-Xmx的大小,然后在程序中循环new对象,直到发生OOM异常,记录下此时new了多少个对象,大家觉得这种方法可靠不?下面放上设置参数以及测试代码。

/**
* 研究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);
}
}
}
运行结果:
14053
java.lang.OutOfMemoryError: Java heap space


根据上面的结果可以算出每个Object对象大小1024*1024/14053=74.61Byte,这个结果好像挺意外,个人觉得有点大了,下面来分析一下,由于设置了-XX:+PrintGCDetails参数,控制台实时输出GC情况,如下图:



可以发现,在new对象的过程中执行了六次GC(Minor GC),也就是说JVM在青年代区回收了6次垃圾,执行了N次FullGC,FullGC会回收PSYoungGen、ParOldGen、Metaspace,这么看来这个结果是不可可信的,因为回收的过程中,销毁了很多对象,但是计数器一直是增加的,所以74.61Byte这个结果毫无疑问是偏大的。

【错误纠正】

经过同事的纠正,回收过程中Object对象不会被销毁,因为被放到了list中,list并不会销毁,因此在执行GC过程中,只是在不断地搬家,从一个survivor到另一个survivor.

方法二

利用java.lang.instrument.Instrumentation这个interface的特有属性,可以在JVM运行过程中实时测量对象大小。简单介绍下这个类,利用 Java 代码,即 java.lang.instrument 做动态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。

在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。

在 Java SE6 里面,最大的改变使运行时的 Instrumentation 成为可能。在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。

另外,对 native 的 Instrumentation 也是 Java SE 6 的一个崭新的功能,这使以前无法完成的功能 —— 对 native 接口的 instrumentation 可以在 Java SE 6 中,通过一个或者一系列的 prefix 添加而得以完成。

最后,Java SE 6 里的 Instrumentation 也增加了动态添加 class path 的功能。所有这些新的功能,都使得 instrument 包的功能更加丰富,从而使 Java 语言本身更加强大。

上面这段摘自IBM Developers,让我自己解释,有点不好解释,大家看权威解答就好。下面这段是来自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运行过程中测量对象大小,下面谈谈如何使用,这个Instrumentation接口不太友好,笔者跟踪了下这个接口,发现它的实现类是在rt.jar包里的,简单介绍下rt.jar,大家都知道是极为重要的一个文件,rt是runtime的缩写,即运行时的意思。是java程序在运行时必不可少的文件。里面包含了java程序员常用的包,如java.lang,java.util,java.io,java.net, java.applet等。也就是说,我们想得到Instrumentation的实例,必须得在JVM运行过程中才能取得,翻开源码构造方法是private类型,没有任何getInstance的方法,写这个类干嘛?看来这个只能被JVM自己给初始化了,那么怎么将它自己初始化的东西取出来用呢,唯一能想到的就是javaagent代理,我说这是代理技术,不知道准不准确,若不正确请大家指正。

Step1:先创建一个用于测试对象大小的处理类(代理类):

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文件,并且在其包内部的META-INF/MANIFEST.MF文件中增加一行:Premain-Class: MySizeOf代表执行代理的全名,这里的类名称是没有package的,如果你有package,那么就写全名,假设打包完的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对象大小(关闭指针压缩)为:"+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,执行TestSize,进行测试

这里需要注意一点,这个Test方法不能在IDE环境中执行,因为你无法使用Instrumentation的实例,必须采用古老的javac,java命令先编译再执行,请按照以下步骤执行,

// step1,编译TestSize.java,并将agent.jar包中的Instrumentation的实例引入ClassPath,这样执行TestSize.java时才能引用Instrumentation实例。
javac -classpath agent.jar TestSize.java
//step2,执行TestSize,使用-javaagent:agent.jar,意思是使用agent.jar作为代理,用到agent技术
java -javaagent:agent.jar TestSize


运行结果:
一个Interger对象大小为:16
一个String对象大小为:24
一个String对象大小(关闭指针压缩)为:48
一个char对象大小为:24
一个ArrayList对象大小为:24
一个Object对象大小为:16
一个Long对象大小为:24


在网上看到一个更全面的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)) {//跳过常量池对象、跳过已经访问过的对象
return 0;
}
visited.put(obj, null);//将当前对象放入栈中
long result = 0;
result += sizeOf(obj);
Class <?>clazz = obj.getClass();
if (clazz.isArray()) {//如果数组
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);
}

//这个方法获取非数组对象自身的大小,并且可以向父类进行向上搜索
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())) {//这里抛开静态属性
if (field.getType().isPrimitive()) {//这里抛开基本关键字(因为基本关键字在调用java默认提供的方法就已经计算过了)
continue;
}else {
field.setAccessible(true);
try {
Object objectToAdd = field.get(obj);
if (objectToAdd != null) {
stack.add(objectToAdd);//将对象放入栈中,一遍弹出后继续检索
}
} catch (IllegalAccessException ex) {
assert false;
}
}
}
}
clazz = clazz.getSuperclass();//找父类class,直到没有父类
}
return result;
}
}


到这里就结束了,有几个疑问,大家有兴趣可以关注下,

1. 修改MANIFEST文件应该还有其他方法,好像是jar cmf manifest-addition jar-file input-file(s),大家有兴趣可以关注下

2. 如何在IDE中实现以上这些步骤,javac和java毕竟是远古时代的东西了。

3. Instrumentation 的最大作用再说一说,就是类定义动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 – javaagent参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序,然后利用Instrumentation的实现类做一列测量,这个有点类似AOP

4. 先想这么多,想到更多问题再补充。

Extra:

jar操作指令

操作命令Cool
创建一个JAR文件jar cf jar-file input-file(s)
查看JAR文件的内容jar tf jar-file
导出JAR文件jar xf jar-file
导出JAR文件中制定的文件包jar xf jar-file archived-file(s)
运行JAR文件中的应用jre -cp app.jar MainClass
运行用JAR格式打包的应用java -jar app.jar
调用一个打包成JAR的applet

方法三,使用JOL 工具来查看一个对象的大小和分布

这个和前两个方法不太一样,是用来测一个JVM中对象的内部分布情况的,而且测得是引用的大小,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.

该工具官网:

http://openjdk.java.NET/projects/code-tools/jol/

下载地址:

http://central.maven.org/maven2/org/openjdk/jol/jol-cli/

下载jar包后保持如下的相对路径 和你要测试的类在一起.

编写测试对象的类:VolatileLong

public final class VolatileLong {

/**
* 大小计算:
*  long 8 字节
* [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 字节
*  long value;  ->  8 字节
*  long p8, p9, p10, p11, p12, p13, p14; // cache line padding -> 7*8 = 56 字节
*
*  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
*  不知道为啥是这样实现:因为父类还有一个Long值 所以总的来说已经超过128了
*
*/
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; // comment out
//objectsize = 6*8 + 8 + 4 =

}


执行下面指令:

java -jar jol-cli-0.9-full.jar internals -cp . VolatileLong

//jar包必须带上全路径,先把class文件编译出来,然后再执行,因为此命令只接受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)


运行结果:

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


参考文章:

https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html

http://blog.csdn.net/biaobiaoqi/article/details/6886749

http://www.importnew.com/18614.html

http://www.importnew.com/25992.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  内存 对象 java