【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);
相关文章
|
15天前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
29 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
11天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
35 4
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
32 2
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
1月前
|
存储 Java
深入理解java对象的内存布局
这篇文章深入探讨了Java对象在HotSpot虚拟机中的内存布局,包括对象头、实例数据和对齐填充三个部分,以及对象头中包含的运行时数据和类型指针等详细信息。
28 0
深入理解java对象的内存布局
|
1月前
|
算法 Java
JVM进阶调优系列(3)堆内存的对象什么时候被回收?
堆对象的生命周期是咋样的?什么时候被回收,回收前又如何流转?具体又是被如何回收?今天重点讲对象GC,看完这篇就全都明白了。
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
10天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
8天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
8 1