在了解之前,我们先看看一个面试过程
- 在面试的时候面试官经常会出现一道题:“Object o = new Object() 占用了多少内存?”
- A童鞋回答:“这个我知道,16个字节”,
- 这时候面试官再问了:“为什么是16个字节呢?”
- A童鞋:“啊~~ 唉。。。。不知道唉, 我只是来面试增删改查的,要知道这么多吗?”
那么今天呢,为了解决这道题我们就需要了解对象的内存布局!
什么是java对象的内存布局
java对象的内存布局主要的作用用来表示一个对象在计算机中占用了多少内存;内存布局主要分为四个部分:markword 、类型指针、 实例数据、对齐(补位)
mark word
对象头的组成部分,mark 是标记的意思,所以markword就是用来标记对象信息的,占用的内存空间根据系统来决定,32位系统占用4个字节,64位系统占用8个字节,markword里面主要标记对象哈希码、分代年龄、GC标记等等
类型指针(class point)
class point 也是对象头的一部分,固定长度4byte, 指定该对象的class类对象。(JVM默认使用-XX:+UseCompressedClassPointers 参数进行压缩。如不使用可设置-XX:-UseCompressedClassPointers关闭,则该字段在64位jvm下占用8个字节; jvm默认压缩后占用4字节, 不压缩占用8字节, 可使用java -XX:+PrintCommandLineFlags -version 命令查看默认的或已设置的jvm参数)
实例数据(instance data)
主要存储基本变量和引用变量
-
基本数据类型变量:用于存放java八种基本类型成员变量,以4byte步长进行补齐,使用内存重排序优化空间,8个基本数据类型占用内存情况如下:
基本数据类型占用的字节数 数据类型
占用字节
int 4 short 2 long 8 float 4 double 8 byte 1 boolean 1 char 2 -
引用变量:存放对象地址,如String,Object;都占用4个字节,64位jvm上默认使用-XX:+UseCompressedOops进行压缩, 可使用-XX:-UseCompressedOops进行关闭,则在64位jvm上会占用8个字节;
对齐(paddind)
对齐是用来补齐对象占用字节数的,每个对象的占用字节数必须是8的整数倍,如果不足8的整数倍,就会在对齐的位置补上剩余的字节,比如 Object o = new Object(); 实际上只占用了12个字节,但是12不能被8整除,所以在padding的位置补上了 4 个字节,凑成16字节;
测试
接下来我们运行一段代码,看看 new 一个对象后占用的内存空间;
想要查看内存使用情况,首先得引入一个叫做 JOL 的maven依赖,
- <!-- jol 可查看对象内存使用情况-->
- <dependency>
- <groupId>org.openjdk.jol</groupId>
- <artifactId>jol-core</artifactId>
- <version>0.10</version>
- </dependency>
java代码
- package com.test;
- import org.openjdk.jol.info.ClassLayout;
- public class JolTest {
-
- public static class TT{
-
- }
- public static void main(String[] args) {
- TT o = new TT();
- System.out.println(ClassLayout.parseInstance(o).toPrintable());
- }
- }
运行结果
通过这个实例我们可以看到,new 出的 TT 对象实际上只占用了12个字节,但是12不能被8整除,所以在padding的位置补上了 4 个字节,凑成16字节;
数据类型
我们看看如果加上8个基本的数据类型会占用多少个字节呢?
-
- public class JolTest {
-
- public static class TT{
- int i;
- long l;
- char c;
- short s;
- double d;
- float f;
- boolean b;
- byte byte1;
-
- String ss;
-
- }
-
- public static void main(String[] args) {
- TT o = new TT();
- System.out.println(ClassLayout.parseInstance(o).toPrintable());
- }
-
- }
打印结果
加锁
接下来我们在原基础上加上synchronized,首先我们先看看锁的标志位图
我们看锁信息的时候看以下的标志位就行了: 01表示偏向锁,也叫无锁,00 表示轻量级锁,10表示 重量级锁;
我们演示一把锁竞争时的标志位,首先我们把线程数量设置为 1,代码: int threadNum = 1; // 竞争锁的线程数量
-
- public class JolTest {
-
- // 未加锁对象
- public static class TT{
-
- }
- // 加锁对象
- public static class BB {
- public BB(Object obj) throws InterruptedException {
- synchronized (obj) {
- System.out.println(ClassLayout.parseInstance(obj).toPrintable());
- }
- }
- }
-
- public static void main(String[] args) {
- TT o = new TT();
- System.out.println(ClassLayout.parseInstance(o).toPrintable());
-
-
- int threadNum = 1; // 竞争锁的线程数量
-
- for (int i = threadNum;i>0;i--){
- // new 出多个线程来竞争锁
- new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- BB b = new BB(o);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }).start();
- }
-
- }
-
- }
打印结果可以看到,这时候升级为了自旋锁
接下来我们把竞争线程数量设置为2 ,代码: int threadNum =2; // 竞争锁的线程数量 ,其余代码一样,我们看看打印出的结果,2个以上的线程竞争时会直接升级为重量级锁