认识Java对象,内存布局,new对象的过程

2023年 9月 24日 47.5k 0

认识Java对象

先了解一下对象的组成

对象的组成

image-20230411224913415.png
可以看到,一个对象由:对象头,实例数据,对齐填充组成。

1、对象头

markword

涉及到synchronized底层实现原理,你可以暂时简单地理解为一些关键的额外信息,可以参考:synchronized的轻量级锁居然不会自旋?深度解析synchronized实现原理

锁状态 29 bit 或 61 bit 1 bit 是否是偏向锁? 2 bit 锁标志位
无锁 31bit的Hashcode 0 01
偏向锁 线程ID 1 01
轻量级锁 指向栈中锁记录的指针 此时这一位不用于标识偏向锁 00
重量级锁 指向互斥量(重量级锁)的指针 此时这一位不用于标识偏向锁 10
GC标记 此时这一位不用于标识偏向锁 11
Class指针

4字节的指针(指向类元信息地址,class),用于访问Class对象。

默认开启指针压缩,所以是4字节,关闭的话为8字节。

数组的length

这个好理解,数组的.length就是拿到这个信息

这里length四个字节,也解释了一个数组的最大长度,是max_int。

2、实例数据

就是类自己的类似private String name = "123"这样的信息。

像父类的private的属性,也是有的,但访问不了。

3、对齐填充

对齐填充使得对象实例的字节数是8的倍数。

64位JVM的寻址空间更大,但是会带来性能的损耗;大多数计算机都是高效的64位处理器,顾名思义,一次能处理64位的指令,即8个字节的数据,HotSpot VM的自动内存管理系统也就遵循了这个要求,这样子性能更高,处理更快。

因此,new一个对象,实际就是要处理上述三大部分。一般有:

  • markword
  • Class对象
  • 实例数据

new对象的过程

一、检查是否类加载

检查常量池中是否能定位到这个类的「符号引用」。

如果没有Class对象,会先执行类加载过程的1~5步骤。类加载的过程可以参考:JVM架构之类加载系统,深入理解类加载器

二、为实例分配内存

首先因为堆全局唯一,因此需要保证线程安全:

方案是:CAS+TLAB

TLAB:Thread Local Allocation Buffer。就是在Eden区直接划分一块给某个线程私有。后续的new对象都在这块私有区域分配。

划分区域也会线程不安全,也需要CAS或锁,但是一种锁粗化的原理,比单单分配一个对象效率要高得多。

在TLAB不够时,采用CAS的方式分配内存。

CAS:compare and swap,保证线程安全

然后开始具体地分配内存

对象所需的内存大小在类加载完成后便可确定,具体如何分配有两种方式:

没有内存碎片:指针碰撞

用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置

有内存碎片:空闲列表

虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。

有没有内存碎片取决于GC算法,清除有内存碎片,复制,整理没有内存碎片

三、初始化实例变量

将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值(一般是0)。

四、设置对象头

即初始化mark word,class指针等信息

对象头是下图绿色的部分

image-20230411224913415.png

五、初始化

先初始化父类再初始化子类。

初始化时先执行实例代码块然后是构造方法。

这样一个真正可用的对象才算完全产生出来

开发视角:代码块/构造方法/声明调用顺序

我们现在已经学了Class对象的加载,实例对象的加载,这里来梳理一下,父子类的静态(非静态)代码块,构造方法,静态(非静态)字段的调用顺序

Class加载完全早于实例对象的加载,因此:

  • 父类的:静态变量声明,静态代码块
  • 子类的:静态变量声明,静态代码块
  • 这里的1~2是类加载的(5)初始化阶段

  • 父类的:实例变量声明,普通语句块
  • 父类的:构造函数
  • 子类的:实例变量声明,普通语句块
  • 子类的:构造函数
  • 这里的3~6是new对象实例的(5)初始化阶段

    没有被声明,代码块,构造函数指定的变量就是默认值(一般为零值)

    • 对于静态的字段由类加载步骤(3)准备阶段保证
    • 对于普通的字段由new对象步骤(3)初始化实例变量阶段保证

    声明语句和代码块哪个先执行

    为什么把声明和代码块放在同一行内?它们之间的顺序如何?

    实际上,这取决于代码的位置。

    我们知道在初始化之前,无论是静态字段,还是普通字段,都会为它们分配空间并赋予默认值,一般为零值。

    因此:

    public static int a = 1;
    

    像这样的语句,「为a分配空间」 与 「声明a的值」 ,并不会被一起执行。

    完全可以这样写:

    static {  a = 2; }
    public static int a = 1;
    

    此时访问a为1

    交换这两行代码的位置:

    public static int a = 1;
    static {  a = 2; }
    

    此时访问a为2

    普通字段完全一样,多个代码块也是这样

    因此,变量声明与代码块的执行顺序取决于代码的位置顺序

    对象如何被访问

    句柄

    在堆中划分一块区域作为句柄池,指针指向句柄,句柄指向堆内存空间

    直接指针

    直接指针就是指向堆内存空间的指针

    对比

    直接指针显然访问速度更快。句柄的好处是对象被移动时(比如GC),只改变句柄中实例数据指针,reference本身不用改变。

    HotSpot VM采用直接指针

    引用类型

    直接指针提供的功能太少,因此JDK1.2之后Java提供了四个级别的引用,以下按引用强度排序:

    强引用(StrongReference)

    最普遍的引用,垃圾回收器绝不会回收它,宁愿抛出 OutOfMemoryError 错误,使程序异常终止。

    new出来的对象的引用都是强引用。像下面这行代码,obj就是个强引用,存储在栈帧的局部变量表中。

    Object obj = new Object();
    

    执行 obj=null;后,这个obj原本指向的Object实例才可能被GC

    软引用(SoftReference)

    如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。可有可无。可用来实现内存敏感的高速缓存。

    弱引用(WeakReference)

    值得被回收。一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

    ThreadLocal的key是弱引用

    虚引用(PhantomReference)

    虚引用:主要用来跟踪对象被垃圾回收的活动。虚引用不会决定GC机制对一个对象的回收权。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

    在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

    查看Java对象的内存布局

    JDK自带

    System.setProperty("java.vm.name","Java HotSpot(TM) ");
    System.out.println(ObjectSizeCalculator.getObjectSize(3L));
    

    jol工具

    
        org.openjdk.jol
        jol-core
        0.16
    
    
    // 基本使用
    Object o = new Object();
    // 实例信息
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    // class类信息
    System.out.println(ClassLayout.parseClass(String.class).toPrintable());
    

    Instrumentation

    获取Instrumentation对象,先引入依赖项

    
        net.bytebuddy
        byte-buddy-agent
        1.12.10
    
    
        net.bytebuddy
        byte-buddy
        1.12.10
    
    
    import net.bytebuddy.agent.ByteBuddyAgent;
    import java.lang.instrument.Instrumentation;
    	ByteBuddyAgent.install();
    	final Instrumentation instrumentation = ByteBuddyAgent.getInstrumentation();
        inst.getObjectSize(obj);
    

    最后来看看Object类,它是Java所有类的父类。

    浅析Object类

    Object有哪些方法

    hashcode,equals,toString,wait,notify,clone

    为什么wait/notify在Object内而不是Thread

    因为等待/通知机制设计是为了解决线程间通信的。

    线程通信是否可执行,是由资源决定的,而非线程。

    一个线程使用完毕某个资源,别的线程才能使用。

    线程知道自己是因为要获取哪个资源而被阻塞,关注的是资源,而不在乎是因为谁(具体哪个线程)。

    输出一个没有重写toString方法的对象会发生什么

    输出Person类得到 Person@3c1

    输出数组得到 [I@4554617c

    输出new Object的对象得到 java.lang.Object@4554617c

    一个类没有重写toString方法,也会有这个方法

    因为Object类下已经做了实现

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    

    也就是会输出 类名 + @ + hashcode的值(hashcode16进制)

    new 一个Object多少字节

    这跟硬件是有关系的。还有是否开启指针压缩等。所以没有确切的答案。但可以肯定的是:一定会是8的倍数。开启指针压缩的情况下,为16字节。8字节mark word,4字节指针,以及对齐填充。另外要区分堆与栈。在栈上也会生成一个4字节的指针。

    小结一下:new 一个Object,在本地栈生成一个4字节的指针,指向一块堆内存区域。这块堆内存区域存储了markword,实例数据,和一个指针指向方法区class对象(或者用句柄实现,但sunhotspot是指针实现),以及对齐填充

    参考文档

    Java内存区域详解(重点)

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论