一切要从new Object()说起

Object类介绍

java.lang.Object 类是所有类的父类, Java 的所有类都继承了 Object,所有类皆可以使用 Object 的方法。

Object类位于 java.lang 包中,编译时会自动导入,它可以显示继承,也可以隐式继承,如我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承 Object,成为 Object的子类。

通常我们需要实例化一个Object,需要通过new关键字去初始化。

Object的内存布局

在 JVM 中,一个Object对象通常由以下三部分所组成:

  • 对象头(Object Header)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

对象头(Object Header)

包括关于对象指向它的类元数据的指针、分代年龄、锁状态、同步状态和标识哈希码等基本信息。由两个词mark word和class pointer组成,如果是数组对象的话,还会有一个数组长度。

mark word
在这里插入图片描述 通常是一组位域,用于存储对象自身的运行时数据,如hashCode、GC分代年龄、锁同步信息等等。占用64个比特,8个字节。
class pointer

类指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。占用64个比特,8个字节。开启压缩类指针后,占用32个比特,4个字节。

数组长度

如果当对象是一个数组对象时,那么在对象头中有一个保存数组长度的空间,占用4字节(32bit)空间。

实例数据(Instance Data)

对象中所有属性的值,包括从父类继承下来的属性。如果对象未定义属性,则这里就不会有数据。根据属性类型的不同占不同的字节,例如boolean类型占1个字节,int类型占4个字节等等。为了提高存储空间的利用率,这部分数据的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。

属性类型分为以下两种:

  • 基本数据类型:

    类型占用字节数
    byte,boolean1
    char,short2
    int,float4
    long,double8
  • 引用数据类型:

    Object对象及其子类

对齐填充(Padding)

理论上计算机对于任何变量的访问都可以从任意位置开始,然而实际上系统会对这些变量的存放地址有限制,通常将变量首地址设为某个数N的倍数,这就是内存对齐填充。

默认情况下,Java虚拟机堆中对象的起始地址需要对齐至8的整数倍。如果一个对象的对象头和实例数据占用的总大小不到8字节的整数倍,则以此来填充对象大小至8字节的整数倍,否则就不需要进行内存对齐填充。

为什么要对齐填充?

内存对齐的其中一个原因,是让字段只出现在同一CPU的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。其实对其填充的最终目的是为了计算机高效寻址,提高CPU内存访问速度,一般处理器的内存存取粒度都是N的整数倍,假如访问M(M不能被N整除)大小的数据,没有进行内存对齐,有可能就需要两次访问才可以读取出数据,而进行内存对齐可以一次性把数据全部读取出来。

Object占用的内存大小

Java 对象标头(mark word)大小在 32 位和 64 位 jvm 上有所不同。确切的大小会因 JVM(尤其是处理器架构)而异。也有可能出于对齐目的,JVM 可以实现最小大小,但获得“前 4 个字节的字段空闲”策略。例如,在 64 位 JVM 上,类型指针占用 8 个字节,监视器信息占用 4 个字节。

至于方法,通常来说,对象中方法的数量对其大小没有影响。方法相关信息存放于方法区(jdk8叫元空间)通常通过在多个对象之间共享Class的单个实例来存储,然后让每个对象存储一个指向对应Class的指针。

下面默认都以Hotspot实现的64位虚拟机为例:

原本情况下,类指针应该占64个比特,也就是8个字节。但因为Hotspot实现的64位虚拟机,默认会开启压缩类指针(和压缩对象指针不一样),而类指针就在class Pointer中存储着,所以会把class Pointer压缩成4个字节。

new Object()占用大小分为两种情况:

  • 未开启压缩类指针

    8字节(mark word) + 8字节(class pointer) = 16字节

  • 开启压缩类指针(默认是开启的)

    8字节(mark word) + 4字节(class pointer) + 4字节(padding) = 16字节

假如是一个数组对象,则:

  • 未开启压缩类指针

    8字节(mark word) + 8字节(class pointer) + 4字节(padding) + 4字节(length)= 24字节

  • 开启压缩类指针(默认是开启的)

    8字节(mark word) + 4字节(class pointer) + 4字节(length)= 16字节

类指针压缩验证

使用 java -XX:+PrintCommandLineFlags -version命令,如下所示:
在这里插入图片描述

-XX:+UseCompressedClassPointers:使用压缩类指针(注意只有64位HotSpot VM才支持压缩)

我们可以使用java -XX:-UseCompressedClassPointers关闭指针(换成+号则是开启),我们可以通过以下代码进行验证

import org.openjdk.jol.info.ClassLayout;

public class ObjectDemo {

    static Object o = new Object();
    static Object[] objects = new Object[0];

    public static void main(String[] args) {

        //需要添加jol依赖
        //打印对象的布局信息
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        System.out.println(ClassLayout.parseInstance(objects).toPrintable());
    }
}

输出如下:

java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000355da25401 (hash: 0x355da254; age: 0)
  8   8        (object header: class)    0x000000001be81c00
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

[Ljava.lang.Object; object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000002626b41801 (hash: 0x2626b418; age: 0)
  8   8                    (object header: class)    0x000000001bf3b180
 16   4                    (array length)            0
 16   8                    (alignment/padding gap)   
 24   0   java.lang.Object Object;.<elements>        N/A
Instance size: 24 bytes
Space losses: 8 bytes internal + 0 bytes external = 8 bytes total

可以看到,数组对象总大小变成了24Byte

jol依赖
        <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.16</version>
            <scope>provided</scope>
        </dependency>

Object的内存分配

内存分配流程

在这里插入图片描述

java对象的分配策略主要有以下几步:

  1. JVM会先进行栈上分配
  2. 如果没有开启栈上分配或则不符合条件的则会进行TLAB分配
  3. 如果对象满足了直接进入老年代的条件,那就直接分配在老年代
  4. 尝试在eden区分配,如果eden区经过GC后空间仍然不够分配则也会分配到老年代(注意开启空间担保配置情况下只要老年代的连续空间大于新生代对象总大小或历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC)
  5. 分配在eden区的对象经过一次GC后存活则进入s区,s区空间不够则直接进入老年代
  6. s区对象达到一定年龄(默认15)则进入老年代
  7. 对象分配到老年代时,若空间不够则触发Full GC,若Full GC后内存仍然不够则抛出OOM异常

TLAB

JVM在堆上分配内存的时候为了保证Java对象的内存分配的安全性,需要对这个过程做同步操作(CAS机制),为了提高效率,每一个线程在Java堆中能够预先分配一小块内存,这部份内存称之为TLAB(Thread Local Allocation Buffer),这块内存的分配时线程独占的,读取、使用、回收是线程共享的。而且只有经过了内存逃逸分析的对象才会分配到TLAB中去。

能够经过设置-XX:+/-UseTLAB参数来指定是否开启TLAB分配。

内存分配的两种方法

为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

指针碰撞(如Serial、ParNew)

假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。

空闲列表(如CMS)

如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。

Object访问方式

句柄访问

  • 原理:在堆中建立句柄池,栈中对象引用 -> 句柄池句柄 ( 句柄信息1 -> 堆中对象实例; 句柄信息2 -> 方法区中对象对应的类数据)
  • 优势:GC时大量移动对象,只需要修改句柄池中引用,无需修改栈中对象引用,减少引用的维护。
  • 缺点:对象访问时多一步指针定位的时间开销,Java程序对象访问很多,效率有一定降低。

直接指针

这也是Hotspot默认的访问方式

  • 原理:栈中对象引用 -> 堆中对象实例 -> 方法区对象对应的类数据
  • 优势:减少对象访问的一次指针定位的时间开销,Java程序对象访问很多,效率有一定提升。
  • 缺点:GC时大量移动对象,需要修改所有关联的栈中对象引用

Object的加载

请参考:JVM双亲委派模型

Object的销毁

请参考:JVM垃圾回收机制深入解析

Object源码分析

Object的主要方法

Object 类在 java.lang 包中,是所有类的父类,所有类都间接或者直接继承 Object 类。

Object 类中方法主要有:

  • registerNatives():本地方法
  • getClass():返回对象对应的Class类
  • **hashCode():**返回对象的哈希码
  • equals():默认判断对象的地址是否相同
  • **clone():**实现拷贝必须实现 Cloneable 接口,并重写 clone()方法。
  • toString():显示的是类名 +"@"+ 对象哈希码的十六进制数
  • notify():唤醒同一个对象上调用 wait()方法的线程的其中一个
  • notifyAll():会唤醒同一个对象上所有调用 wait()方法的线程
  • wait():释放锁,并使当前线程进入阻塞
  • finalize():当垃圾回收器将要回收对象所占内存之前被调用

其中registerNatives()、getClass()、clone()、notify()、notifyAll()和 wait()等方法是本地方法,具体实现是通过c/c++代码实现的,因此子类不能重写。

Object的构造方法

源码中并没有Object的构造方法,但是,编译器在编译期间会给Object一个默认的空的构造方法(任意Java类,只要类中没有构造方法,编译器都会默认的给一个空构造方法,若已有构造方法,则不会添加):

public Object(){
}

乍一看啥内容也没有,我们用反编译手段探探究竟:

0 return

Object的构造方法实际上就执行了一条return的汇编指令,但是如果一个对象中存在其它属性的话,则JVM还会为其调用一个init方法,比如下面这个类:

public class MyObject(){
	Object o = new Object();
}

反汇编后发现它的构造器中的指令为:

0 aload_0
1 invokespecial #1 <java/lang/Object.<init>>
4 return

也就是说对象中的实例属性会通过Object中的init方法进行初始化,该方法由JVM创建,如果不存在实例变量,也就不会创建init方法了。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值