【jvm系列-06】深入理解对象的实例化、内存布局和访问定位

简介: 【jvm系列-06】深入理解对象的实例化、内存布局和访问定位

一,对象实例化、内存布局和访问定位

1,对象的实例化

创建对象的方式和创建对象的步骤主要有以下几种方式

1.1,创建对象的几种方式

在日常开发中,创建对象的方式主要有以下几种:

  • 最常见的方式:new 加构造器,如果构造器私有,可以通过静态访问,如单例模式,或者通过工厂加载
//new 构造器 创建对象
Object object = new Object();
//构造器静态私有,如典型的单例模式
Object object = Object.getObject();
//工厂加载,SpringBean,SqlSessionBean
Object object = ObjectFactory.getObject();
  • 反射的方式:类的newInstance或者构造器的newInstance·
public class Invoke {
    public static void main(String[] args) {
        try {
            Class<?> clazz1 = Class.forName("com.tky.jvm.Invoke");
            //通过类构造器获取对象
            Constructor<?> constructor = clazz1.getConstructor();
            Invoke invoke1 = (Invoke)constructor.newInstance();
            //通过类名获取
            Class<Invoke> clazz2 = Invoke.class;
            Invoke invoke2 = clazz2.newInstance();
            //通过对象获取
            Invoke in = new Invoke();
            Class<? extends Invoke> clazz3 = in.getClass();
            Invoke invoke3 = clazz3.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 克隆的方式:clone的方式,不调用任何构造器,当前类需要实现Cloneable接口以及clone方法
/**
 * @author zhenghuisheng
 * @date : 2023/4/10
 */
@Data
public class Clone implements Cloneable {
    private Long id;
    private String username;
    private String password;
    @Override
    protected Clone clone() throws CloneNotSupportedException {
        return (Clone)super.clone();
    }
}
class TestClone{
    public static void main(String[] args) {
        Clone clone1 = new Clone();
        clone1.setId(1L);
        clone1.setUsername("zhenghuisheng");
        clone1.setUsername("123456");
        try {
            Clone clone2 = clone1.clone();
            System.out.println(clone2.getId());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 反序列化的方式:从文件或者网络中获取二进制流,将二进制流转换成对象
//对象序列化
Student s = new Student("1","zhenghuisheng","18");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/a.txt"));
objectOutputStream.writeObject(s);
objectOutputStream.close();
//对象反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));
Student student = (Student) inputStream.readObject();
  • 第三方库Objenesis
//构建 Objenesis 对象  Objenesis需要对应的pom依赖对象
Objenesis objenesis = new ObjenesisStd();
ObjectInstantiator<Student> instantiator = objenesis.getInstantiatorOf(Student.class);
Student student = instantiator.newInstance();

1.2,对象创建的步骤

这里主要从执行的角度来分析这个对象创建的步骤,如上图所示,主要分为六个步骤来创建对象

1.2.1,判断对象对应的类是否加载,验证,准备,解析和初始化

虚拟机在遇到一条new指令的时候,首先会去检查这个指令的参数是否能在元空间的常量池中定位到一个类的符号,并且检查这个类是否经历过了加载、验证、准备、解析和初始化这个几个步骤。如果没有,那么类加载器还在双亲委派的模式下,使用当前类加载器的以 ClassLoader + package + class 为key进行查找对应的.class文件,如果没有找到对应的文件,则会抛出 ClassNotFoundException 异常,如果找到,则进行类加载,并生成对应的Class对象


1.2.2,为对象开辟空间,分配内存

首先需要计算对象占用空间的大小,接着在堆中划分一块内存给新对象,如果实例成员变量时引用变量,那么仅分配引用变量空间即可,即四个字节大小。如根据不同的基本数据类型其所占用的字节数,从而得知每个变量占多大的空间,最后将这些变量所需要的空间全部叠加在一起,得到的就是这个总空间的字节数。


而内存如果是规整的,那么虚拟机将采用的是 指针碰撞 的方式来为对象分配内存。如下图,就是将用过的内存放在一边,空闲的内存放在另外一边,中间放着一个分界点的指示器,内存分配就是将指针向空闲那边挪动,挪动的距离就是对象所需要的大小 ,而指针碰撞这种方式,取决于虚拟机的垃圾回收算法是否具有压缩功能。

017ee21b74344e54b63d1f9af2251bae.png



如果内存内部不是规整的,虚拟机内部就得维护一个列表来管理已使用的内存和未使用的内存,其方式被称为 空闲列表 。如下图,虚拟机内部维护了一张表,记录的是哪块内存时可用的,哪块内存时不可用的,然后在分配的时候,就从列表中找到一块足够打的空间划分给对象实例,并更新表上的内容。

b1916c2044b340fa83969c251065c4b9.png



1.2.3,处理并发问题

由于对象是在堆中创建,而堆又是共享区域,因此避免不了会出现这个并发的问题,而在堆内部,主要采用了两种方式来保证实例的安全性。


一种是采用CAS比较与交换的方式,失败则重试,区域加锁来保证更新的原子性,另一种是 每个线程预先分配一个 TLAB。主要是通过这两种方式来解决并发安全的问题。


1.2.4,对象初始赋值

这里进行一个默认的初始化。这样所有属性都有一个默认值,保证对象实例字段在不赋值时就可以使用。因此在方法内部,静态变量在准备阶段就进行了初始赋值,实例变量在分配空间的时候也进行了初始赋值,因此这两个可以直接使用变量,其他的变量如果没有进行显示的初始化,那么会出现直接编译失败的情况。


1.2.5,设置对象的对象头

将对象所属的类、对象的hashCode、GC信息、年龄、锁信息等存储在对象的对象头中。


1.2.6,执行init方法进行初始化

这里就行一个显示初始化,初始化工作才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆对象的首地址赋值给引用对象。因此一般来说,new 指令之后会接着就是执行方法,将对象按照程序员的意愿进行初始化,这样真正可用的对象才算完整的创建出来。


2,对象的内存布局

对象的内存布局中,主要包括对象头、实例数据和对其填充



df9b44e43c4549aa86fb1f2324d39fb7.png

2.1,对象头(Header)

在对象头中,又可以分为两部分,一部分是运行时的元数据,另一部分就是类型指针。


运行时元数据包括哈希码、GC年龄分代、线程持有的锁、持有锁标志、线程id、线程时间戳。


由下图可知,在对象的年龄分代为4bit,因此最大为1111,即15,又由于是从0开始,因此其最大年龄为15,所以在设置这个年龄的时候,只能往小设置。锁的标志位对应的字节码用01、10、11表示,其值分别对应着1、2、3,并且这段锁升级的过程是不可逆的。


7636bd8f31ed4ab78823641089617809.png


而类型指针指向的是元数据InstanceKlass,确定该对象所属的类型。如果是数组,还需要记录数组的长度


2.2,实例数据(Instance Data)

对象真正存储的有效信息,包括代码中定义的各种类型的字段,以及父类继承下来的和本身拥有的字段 。并且在这些对象中,父类定义的变量会出现在子类之前,并且相同的字段总是会被分配在一起,如果CompactFields参数为true,子类的窄变量可能插入到父类变量的空隙。


2.3,代码示例

接下来分析一下以下这段代码

/**
 * @author zhenghuisheng
 * @date : 2023/4/7
 */
public class Customer {
    Integer id = 1001;
    String name = "zhenghuisheng";
    public Customer(){
        Account account = new Account();
    }
}
public class Test{
    public static void main(String[] args){
        Customer cust = new Customer(); 
    }
}

然后其对应的内存结构如下图所示,在这个main方法中,由于是静态方法,因此局部变量表的第一个slot不是this,而局部变量表中的第二个cust是引用着堆中 new Customer()的实例地址,该实例对象中,主要就是上面的运行时元数据、类型指针、对其填充等组成。运行时数据区就包括唯一地址哈希值、结果多次GC后的年龄、是否获得锁等标志;类型指针对应的就是Customer的Klass类元信息;实例数据就包括自身的属性以及父类属性



816b540d02f0488f89d970b5c2ea1947.png

3,对象的访问定位

创建对象主要是为了更好的去使用他,JVM内部主要是通过两种方式实现对象引用访问到内部对象的,一种是直接指针,一种是句柄访问。


如下代码所示,一个创建对象需要涉及到堆,栈和方法区,因此对象的定位以及访问也需要设计这三个地方


//第一个User存在方法区,主要是存储类信息和运行时常量池
//第二个user在栈中,作为变量存储
//最后的 new User存储在堆中
User user = new User();

句柄访问 的方式如下,在Java堆中有一个句柄池,然后句柄池中保存指向堆中的实例的地址和指向方法区中保存类信息的地址,而在栈中只需保存句柄池的地址即可。


7e1505ed5e2e403faae59098ce3c3861.png


直接指针 就是不需要使用句柄池,在栈的局部变量表中直接保存堆中实例的地址,而在堆中会有一个指针去指向方法区中保留类信息的地址。在Hotspot虚拟机中,主要采用的是这种方式

aebe5354418d43feb7ffb7c354fbf853.png



句柄指针需要在堆空间中开辟空间存储句柄池,因此会有一定的空间浪费,并且效率相对较低,但是如果出现对象的位置发生改变,如出现垃圾回收的情况,或者使用标记整理算法的时候 ,这个栈中指向堆中的句柄池的指针可以不用发生改变,只需改变句柄池只向实例数据和方法区的指针。而这个直接指针的优缺点就是就和句柄指针相反。


4,直接内存初体验(了解)

在JDK8中,方法区的具体实现从永久代变成了元空间,而元空间使用的是本地内存,又名直接内存,这部分不属于运行时数据区的一部分,也不是《java虚拟机规范》中定义的内存区域

6791e10180d64e9e921ce02b3f938339.png



在java代码中,可以直接通过这个 ByteBuffer.allocateDirect() 来进行本地内存空间的分配, 即直接通过这个NIO来进行操作,并且在通常直接内存的速度会直接优于Java堆,其读写性能相对较高。因此处于性能考虑,读写频繁的场合可以使用直接内存,并且Java的NIO库,也允许Java程序使用直接内存。


也可能会出现 OutOfMemoryError 异常,由于直接内存在Java堆之外,因此其大小不会受限于 -Xmx 指定的最大堆大小,但又由于系统的内存始终是有限的,因此堆和直接内存的总和依然受限于操作系统给出的最大内存,但是在直接内存中,也存在一定的缺点:分配回收成本较高,并且不受JVM内存回收管理。


因此可以直接内存可以通过 MaxDirectMemorySize 进行大小的设置,如果未指定,那么默认和堆的最大值 -Xmx 参数值一致

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100 * 1024);
相关文章
|
1月前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
181 77
|
22天前
|
缓存 监控 算法
Python内存管理:掌握对象的生命周期与垃圾回收机制####
本文深入探讨了Python中的内存管理机制,特别是对象的生命周期和垃圾回收过程。通过理解引用计数、标记-清除及分代收集等核心概念,帮助开发者优化程序性能,避免内存泄漏。 ####
31 3
|
27天前
|
传感器 人工智能 物联网
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
45 6
|
9天前
|
存储 缓存 数据安全/隐私保护
DMA(Direct Memory Access):直接内存访问
DMA(Direct Memory Access)是一种允许外设直接与内存进行数据传输的技术,无需 CPU 干预。它通过减轻 CPU 负担、提高数据传输效率来提升系统性能。DMA 的工作模式包括直接模式和 FIFO 模式,数据传输方式有单字传送和块传送,寻址模式有增量寻址和非增量寻址。通过缓存一致性协议、同步机制、数据校验和合理的内存管理,DMA 确保了数据在内存中的一致性和完整性。
39 0
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
108 4
|
2月前
|
缓存 算法 数据处理
如何选择合适的内存访问模式
【10月更文挑战第20天】如何选择合适的内存访问模式
57 1
|
1月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
280 1
|
2月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
44 4
|
21天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
1天前
|
存储 Java 程序员
【JVM】——JVM运行机制、类加载机制、内存划分
JVM运行机制,堆栈,程序计数器,元数据区,JVM加载机制,双亲委派模型