JMM——对象的创建、内存布局及访问

JMM——对象的创建、内存布局及访问

讲解HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程

以某大厂面试题展开对象的内存布局问题

1、请解释以下对象的创建过程?

2、对象在内存中的存储布局?

3、对象头具体包括什么?

4、对象怎么定位?

5、对象怎么分配?

6、Object o = new Object在内存中占用几个字节?

一、请解释以下对象的创建过程?

这个问题其实就是问:new了一个对象,在本质上是怎样一个过程

大致可分为以下步骤

1、检查符号引用

当JVM遇到字节码new指令的时候,将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否被加载、解析和初始化过

如果没有加载过则先执行相应的类加载过程,在前面我们详细讲过类加载过程,这里简单回忆:

1、Class Loading

类加载、类加载器、双亲委派模型

2、Class Linking

​ 1、验证——Verification

​ 2、准备——Preparation(半初始化,类变量赋零值(默认值))

​ 3、解析——Resolution

3、Class Initializing

初始化过程——对上面赋零值的类变量,按照程序员的设置赋初始值

上面的过程,是如果Class还没有Load到内存,上面三步就是完成类加载load过程。如果已经Load到内存,就不会再次执行上面的步骤。

2、申请对象内存

上面的类加载通过之后,接下来就是虚拟机为新生对象分配内存。

对象所需的内存大小在类加载完成之后便可以完全确定,为对象分配空间的任务实际上等同于把一块确定大小的内存块从Java堆(Heap)中划分出来

我们知道堆内存是由GC来管理的,根据GC垃圾收集器的不同,内存划分方式分为以下两种:

  • 指针碰撞

    Java堆中的内存绝对规整,只需根据对象大小,把指针挪动一段与对象大小相等的距离,分配内存即可

    对应的是Serial、ParNew等使用标记——整理算法的垃圾收集器

  • 空闲列表

    Java堆中的内存并不规整,无法根据偏移量划分内存。虚拟机必须维护一个列表,记录内存的使用情况,将空闲内存分配给对象实例

    对应的是CMS这种基于标记——清除算法的收集器,由于此算法会导致内存碎片的产生,所以内存并不规整

3、成员变量赋默认值

内存分配完成之后,虚拟机需要把内存空间都初始化为零值。这步操作保证对象的实例字段在Java代码中可以不赋初值就直接使用,使程序能够访问到字段对应的零值。

基本数据类型的零值

数据类型 零值 数据类型 零值
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
byte (byte)0 reference null
char ‘\u0000’

对于这个实例对象的成员变量赋默认值(零值),比如:

 int  m = 8;

在这一步就会把 m 赋值为 0

4、调用构造方法

调用构造方法,成员变量赋初始值

new指令之后执行()方法,这一步才会按照程序员的意愿对对象进行初始化,赋初始值

把成员变量按照顺序赋初始值

int  m = 8;

到达这一步 m 就被真正赋值为 8,一个真正可用的对象才算完全被构造出来

如果有父类,优先调用父类构造方法

二、对象在内存中的存储布局?

对象的内存和虚拟机的实现、虚拟机的配置有很大的关系

对象又分为普通对象、数组对象两种

观察虚拟机配置

java -XX:+PrintCommandLineFlags -version

可以显示出JVM运行时的详细配置

1、普通对象

1、对象头

对象头分为两类信息:

(1)运行时数据

​ markword 8字节64位

​ 标识对象的状态信息如哈希码、锁的状态标志、偏向锁ID、偏向时间戳、GC分代年龄等等

(2)ClassPointer指针

​ 即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。

在这里插入图片描述

比如一个个实例对象O1、O2…On,如何找到二进制指令数据的入口–Class对象?就是靠这个ClassPointer指针

2、实例数据

实例数据部分是对象真正存储的有效信息,即我们在代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段,都必须在这一部分记录下来。

其实就是我们的赋值操作的存储,m是几、n是几

3、Padding对齐填充

仅仅起到占位符的作用,由于JVM自动内存管理要求对象的起始位置必须是8字节的整数倍,即任何对象的大小都必须是8字节的整数倍

有些对象并不能被精确的设计成8字节的整数倍,此时就需要使用对齐填充来补全

2、数组对象

  1. 对象头(同上)
  2. 数组长度:4字节
  3. 数组数据
  4. Padding对齐填充

了解普通对象即可,面试最多到这个深度,数组对象了解即可

三、对象头具体包括什么?

在这里插入图片描述

《深入理解Java虚拟机》P51、P482

重点理解偏向锁的锁标记位

四、对象怎么定位?

了解即可,没什么意义——《深入理解Java虚拟机》P52

创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象

句柄池

​ 通过间接指针,效率稍低、但是GC的时候,效率比较高

直接指针

​ 节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是极为可观的执行成本,就HotSpot而言,就是使用直接指针的方式,效率高

五、对象怎么分配?

在这里插入图片描述

这一部分涉及到 JVM 的自动内存管理,在后面将会详细讲到,这里简单概述一下:

1、栈上分配

有一些小的对象,线程私有不会被其他线程共享,如果经过即时编译后能够被拆解为标量类型(能够直接参与运算,相当于看作是指令而不是对象),就会尝试先间接地在栈上分配,当栈弹出的时候对象随之消亡,不需要垃圾收集器来管理

2、线程本地分配(TLAB)

当对象无法标量替换,也就无法分配在栈上,此时就只能分配在Heap空间,一般分配在Eden区,而Eden区为了并发效率,让每一个线程保持一个线程独有的区域,称为TLAB,此时对象会优先分配在TLAB

由于所有的线程都会向Eden区分配对象,高并发情况下必然会产生线程的对同一内存的争用,所以JVM又设计了TLAB机制——每个线程占用Eden区 1% 的大小,这个内存区域线程独有,不会产生线程争用,提高内存分配效率

3、对象优先在 Eden 分配

对象的内存分配从概念上讲,应该都是在堆上分配,新生对象通常会分配在新生代中(E)。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,对Eden区进行内存回收

4、大对象直接进入老年代

当对象很大(需要大量连续内存空间的Java对象),最经典的就是那种很长的字符串(如JSON对象),或者数量很庞大的数组,总之只要大小超过一个阙值,就可能直接分配在老年代(O)

我们在编写程序时要注意避免大对象:在分配空间时,它容易导致内存明明还有不少空间时就提前触发GC,以获取足够的连续空间才能安置好大对象,而当复制对象时,大对象就意味着高额的内存复制开销

5、长期存活的对象进入老年代

多数垃圾收集器都采用了分代收集来管理内存,因此在对象内存分配时就必须能决策哪些存活对象应当放在新生代,哪些对象应该放在老年代

JVM通过存储在对象头部,给每一个对象定义了一个对象年龄(Age)计数器,当对象年龄达到阙值,就会进入到老年代,不同的垃圾收集器默认阙值不同:

对象何时进入老年代—经过的GC次数:

—Parallel Scavenge:15次

—CMS:6次

—G1:15次

6、动态对象年龄判定

为了能更好的适应不同程序的内存情况,JVM并不严格限制对象年龄达到阙值才能晋升老年代——如果在Survivor空间中相同年龄的所有对象大小的总和大于Sruvivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到年龄阙值

JVM自动管理、判定

7、空间分配担保

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,才可以确保这一次的Minor GC是安全的

新生代使用复制收集算法,为了提高内存利用率,只使用了一个Survivor空间作为备份,因此当出现大量对象在经过Minor GC之后仍然存活的情况——最极端情况下所有对象都存活,需要老年代进行分配担保,确保老年代可用空间能够容纳Survivor的所有对象

与贷款担保类似,贷款时需要有共同担保人,当贷款人无力偿还时,由担保人顶替还款义务,防止银行无法回收贷款

六、对象的大小问题

问题:Object o = new Object在内存中占用几个字节?

1、脑袋8个字节 + Class Pointer指针4个字节 = 12个字节

2、因为需要对齐为8的倍数,所以padding填充了4个字节

3、上面这个基本的 Object 对象 o 占用字节 :head 8 + Class Point 4 + padding 4 = 16个字节

这是基本的什么属性都没有的简单的对象,占用16字节

如果实例对象有各种属性,再加上各种属性占用字节即可,如下:

private static class P {
    
    
                           //8 _markword
                           //4 _oop指针
           int id;         //4
           String name;    //4
           int age;        //4

           byte b1;        //1
           byte b2;        //1
       
           Object o;       //4
           byte b3;        //1
       
}

猜你喜欢

转载自blog.csdn.net/qq_42583242/article/details/107826790