这次说的是64bit的jvm,暂不谈32bit的jvm,两者的对象大小是不一样的。
java对象的存储布局分为两种情况:普通对象 和 数据。
先说普通对象吧:如下图
那为啥对齐单元的大小是8个字节呢?
因为是64bit位的jvm,8个字节刚好是64个比特位,这样读取的效率高。
那这个东西我们在代码里面看得到吗? 可以看到的。
我们在pom.xml里面引入依赖:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
<scope>provided</scope>
<dependency>
但是我的报错啊:java.lang.ClassNotFoundException: org.openjdk.jol.info.ClassLayout,我也不知道为啥,解决办法如下:
点开如下链接:
,选择一个版本,我选择的是0.10/这个版本,亲测可用:
然后选择格式符合: jol-cli-.-full.jar的包
然后选中jar,将其添加为依赖,并且删除原来的maven依赖。
这样就可以用了。
我们在代码里运行下:
1.0 情形:
public class Test { } public class RunTest { public static void main( String[] args ) { Test test = new Test(); System.out.println(ClassLayout.parseInstance(test).toPrintable()); } }
运行的结果如下:
奇怪了,怎么只有header和padding呢,实例数据那一部分呢?
实例数据是指成员变量,而Test.java里面没有成员变量。所以总共16个字节。
那如果是Object类型的对象呢,Object里面没有成员变量。不管压缩与否都是16个字节,为啥??
如果是32位的jvm呢,32位的jvm不能压缩指针哦,Object对象是8个字节,如下图:
1.1情形:
public class Test { int a; } public class RunTest { public static void main( String[] args ) { Test test = new Test(); System.out.println(ClassLayout.parseInstance(test).toPrintable()); } }
这种情况应该是多少个字节呢? int类型占4个字节。如下图:
header部分不变,依然是12个字节,成员变量a占4个字节,刚好16个字节。
那为什么这次没有填充数据呢?注意了,只有在对象大小不能被8整除的时候才需要填充数据。
那如果是这样呢:
public class Test { int a; int b; } 多少个字节? header部分不变12 bytes,两个int类型成员变量8 bytes,这个时候需要填充4 bytes, 加起来共24个字节。
那如果是这样呢:
public class Test { int a; int b; boolean c; } 多少个字节? header部分不变12 bytes,两个int类型成员变量8 bytes,注意了boolean类型只占1个字节,所以需要填充3个字节,凑成24个字节。
那如果是这样呢:
public class Test { int a; int b; boolean c; String str = "hello"; } 多少个字节?header部分不变12 bytes,两个int类型成员变量8 bytes,注意了boolean类型只占1个字节。字符串类型的是怎么算呢? 我们要搞清楚,str才是成员变量,"hello"是一个字符串对象,是两回事。首先啊,字符串是一个特殊的对象,它是存放在字符串常量池中,那Test对象是多少字节呢?那str的大小为零吗 或者是5个字节吗??str是句柄,引用类型的,是存储对象的地址,也就是普通对象的指针,这个指针占用多少个字节呢?
12 + 4 + 4 + 1 + 3 + 4 + 4 = 32 bytes。有两个问题哦:
1. 为什么编译器会将boolean类型转为int类呢?
2. 64位的计算机,指针应该是8个字节啊。64位的jvm,对象的指针为什么是4个字节呢?
首先回答问题1:
因为虚拟机规范只有 4字节 和 8字节, 大部分指令都没有支持 byte、char 和 short 类型,甚至没有任何指令支持 boolean 类型。编译器会在编译器或运行期将 byte 和 short 类型带符号扩展为 int 类型, boolean 和 char 类型零位扩展为相应的 int 类型。与之类似,在处理 boolean、byte、char 和 short 类型的数组时,也会转为使用相应的 int 类型的字节码来处理指令。 因此,大多数对于 boolean、byte、char 和 short 类型数据的操作,实际都是使用 int 类型作为运算类型。另外还有第二点原因,在设计虚拟机时,主要考虑的是 32位体系,32位系统使用 4 字节是最节省,因为 CPU 只能 32位32位的寻址。
都说 byte、boolen 类型占 1字节,但上面又提到, byte 会被提升为 int 类型,那么就应该占了 4字节类型(long、float), boolean、char、short 都是占了 4字节。
再来回答问题2:
如下图,执行命令:java -XX:+PrintCommandLineFlags -version
我们可以配置关闭这两个压缩:
将 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops 改为: -XX:-UseCompressedClassPointers -XX:-UseCompressedOops
关闭压缩后,再运行,就成了 32(关闭前) + 4 + 4 = 40bytes,如下图:
最后再掰饬下一个问题啊,header里面包含了markword和class pointer这两部分,类型指针就是你属于哪个class对象就指向哪个class对象,上面已经说过了。现在重点这个header里面的markword里面到底装了些啥呢?markword装了三部分:1. 记录锁的状态,是无锁还是轻量级锁还是重量级锁,如果有锁,还得指向哪个线程持有这个锁等信息;2. GC标记信息(例如CMS用到三色标记法,需要记录三种颜色。还有有一个很重要的信息就是对象的年龄,在from和to去来回拷贝,每拷贝一次年龄就加1,这个年龄啊就记录在markword里面);3. 记录对象的hashcode信息,注意啊,只有调用了对象的hashcode()方法才会记录,记录之后,下次调用hashcode()方法,就不用重新计算了(对象的hashcode不会变),直接去头里面拿就可以了。
markword里面存hashcode有什么用呢? 会经常被用到吗?有用,HashMap和HashSet等等数据结构不都是用到了hashcode吗,你存储数据的时候,不经常用到这些吗? 所以说有用。
下面演示一下,只有调用了hashcode()方法,才会在markword里面记录hashclode信息。
public class RunTest { public static void main( String[] args ) { Test test = new Test(); System.out.println(ClassLayout.parseInstance(test).toPrintable()); test.hashCode(); System.out.println(ClassLayout.parseInstance(test).toPrintable()); } }
如上图所示,对比调用hashcode()之前和之后的markword信息。
下面演示一下,无锁,持有锁,释放锁这三种状态,对比下在markword里面记录的信息。
public class RunTest { public static void main( String[] args ) { Test test = new Test(); System.err.println("无锁状态:"); System.err.println(ClassLayout.parseInstance(test).toPrintable()); synchronized (test) { System.out.println("持有锁状态:"); System.out.println(ClassLayout.parseInstance(test).toPrintable()); } System.err.println("释放锁状态:"); System.err.println(ClassLayout.parseInstance(test).toPrintable()); } }
对比就知道,对象作为锁之后,锁的状态信息会记录在markword里面,释放锁之后和无锁两种状态的记录信息是一样的。
本次就分享这么多,欢迎大佬们指正!