对象的实例化内存布局与访问定位、直接内存知识概括

本文详细探讨了Java对象的实例化过程,包括类加载、内存分配、并发控制以及初始化步骤。同时,讲解了对象内存布局,包括对象头、实例数据和对齐填充。介绍了对象访问定位的两种方式:句柄访问和直接指针。此外,还阐述了直接内存的概念,以及其在NIO中的应用和与堆内存的区别。最后,提到了直接内存可能导致的OOM问题及其配置。
摘要由CSDN通过智能技术生成

对象的实例化

对象实例化:在这里插入图片描述

对象创建的方式:

  • new:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法
  • Class的newInstance方法:在JDK9里面被标记为过时的方法,因为只能调用空参构造器,并且权限必须为 public
  • Constructor的newInstance(Xxxx):反射的方式,可以调用空参的,或者带参的构造器
  • 使用clone():不调用任何的构造器,要求当前的类需要实现Cloneable接口中的clone方法
  • 使用序列化:序列化一般用于Socket的网络传输
  • 第三方库 Objenesis

创建对象的步骤:

1、判断对象对应的类是否加载、链接、初始化:

  • 虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。(即判断类元信息是否存在)。
  • 如果该类没有加载,那么在双亲委派模式下,使用当前类加载器以ClassLoader + 包名 +
    类名为key进行查找对应的.class文件,如果没有找到文件,则抛ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class对象。

2、为对象分配内存:

  • 首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小

  • 如果内存规整:采用指针碰撞分配内存
    ①如果内存是规整的,那么虚拟机将采用的是指针碰撞法(Bump The Point)来为对象分配内存。
    ②意思是所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针往空闲内存那边挪动一段与对象大小相等的距离罢了。
    ③如果垃圾收集器选择的是Serial ,ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带Compact(整理)过程的收集器时,使用指针碰撞。
    ④标记压缩(整理)算法会整理内存碎片,堆内存一存对象,另一边为空闲区域

  • 如果内存不规整
    ①如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表来为对象分配内存。
    ②意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为了 “空闲列表(Free List)”
    ③选择哪种分配方式由Java堆是否规整所决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
    ④标记清除算法清理过后的堆内存,就会存在很多内存碎片。

3、处理并发问题:

  • 采用CAS+失败重试保证更新的原子性
  • 每个线程预先分配TLAB - 通过设置 -XX:+UseTLAB参数来设置(区域加锁机制)
  • 在Eden区给每个线程分配一块区域

4、初始化分配到的内存:

  • 所有属性设置默认值,保证对象实例字段在不赋值可以直接使用。

5、设置对象的对象头:

  • 将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。

6、执行init方法进行初始化:

  • 在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量

  • 因此一般来说(由字节码中跟随invokespecial指令所决定),new指令之后会接着就是执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完成创建出来。

对象的内存布局

对象内存布局:在这里插入图片描述
对象头:

  • 对象头包含两部分:运行时元数据(Mark Word)和类型指针
  • 运行时元数据
    ①哈希值(HashCode),可以看作是堆中对象的地址
    ②GC分代年龄(年龄计数器)
    ③锁状态标志
    ④线程持有的锁
    ⑤偏向线程ID
    ⑥偏向时间戳
  • 类型指针
    ①指向类元数据InstanceKlass,确定该对象所属的类型。指向的其实是方法区中存放的类元信息
  • 说明:如果对象是数组,还需要记录数组的长度

实例数据(Instance Data):

  • 说明:
    它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)

  • 规则:
    ①相同宽度的字段总是被分配在一起
    ②父类中定义的变量会出现在子类之前(父类在子类之前加载)
    ③如果CompactFields参数为true(默认为true):子类的窄变量可能插入到父类变量的空隙

对齐填充:

  • 不是必须的,也没特别含义,仅仅起到占位符的作用

总结:
在这里插入图片描述

对象的访问定位

JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢?
在这里插入图片描述在这里插入图片描述
对象的两种访问方式:句柄访问和直接指针:

句柄访问:

  • 缺点:在堆空间中开辟了一块空间作为句柄池,句柄池本身也会占用空间;通过两次指针访问才能访问到堆中的对象,效率低
  • 优点:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改在这里插入图片描述

直接指针(HotSpot采用):

  • 优点:直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据
  • 缺点:对象被移动(垃圾收集时移动对象很普遍)时需要修改 reference 的值
    在这里插入图片描述

Java引用与C语言指针区别:

  • Java中的引用可以理解为被封装过的指针,与指针相比引用是可控的,所以更加安全。
  • 引用不可以计算而指针可以计算,C语言指针更加灵活,也容易产生内存泄漏问题。
  • Java中的引用由于受到封装可以不用关心细节,而C语言的指针本质上就是一个int变量。
  • Java引用类型的初始值是null,而C语言指针的初始值是未知的,不可控的。
  • Java引用作为函数参数时,传递的是引用值的copy,在函数内部可以改变引用指向的对象,但是不会改变引用值本身。比如,在函数内部将引用指向对象的属性改变在函数外面会改变,而将引用本身赋值null在函数外面该引用不会受到影响。
  • 引用传递的核心概念就是:堆内存的一个对象被多个栈内存的数据引用。将引用作为函数参数,就是引用传递,因为形参就是指向堆内存的另一个栈内存中的数据。
  • 在JAVA中,实际上每一个new 语句返回的都是一个指针的引用。引用本身存放在栈中,但是引用指向的对象存放在堆内存中。

直接内存概述

直接内存:

  • 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
  • 直接内存是在Java堆外的、直接向系统申请的内存区间。
  • 来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
  • 通常,访问直接内存的速度会优于Java堆。即读写性能高。
  • 因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。
  • Java的NIO库允许Java程序使用直接内存,用于数据缓冲区

为什么直接缓冲区相对于堆缓冲区,分配和释放都比较昂贵?

  • 可以这么理解 这上面提到的直接缓冲区是可以通过类似于调unsafe.allocateMemory(size);取得的,内部是调用了类似于c语言的malloc临时分配的进程堆中的内存,不受gc管控,需要手动释放(sun.misc 有对应的phathom reference 的 cleaner可以用来释放这部分内存)。而java堆的内存是jvm启动时通过mmap已经分配好的内存区域,受gc监管回收。一个是每次都要调用操作系统函数分配内存空间,一个是使用提前分配好了的空间。

虚拟内存

虚拟内存是怎么工作的:

  • 当每个进程创建的时候,内核会为进程分配4G的虚拟内存,当进程还没有开始运行时,这只是一个内存布局。实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射)。这个时候数据和代码还是在磁盘上的。当运行到对应的程序时,进程去寻找页表,发现页表中地址没有存放在物理内存上,而是在磁盘上,于是发生缺页异常,于是将磁盘上的数据拷贝到物理内存中。
  • 另外在进程运行过程中,要通过malloc来动态分配内存时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
  • 可以认为虚拟空间都被映射到了磁盘空间中(事实上也是按需要映射到磁盘空间上,通过mmap,mmap是用来建立虚拟空间和磁盘空间的映射关系的)

利用虚拟内存机制的优点:

  • 既然每个进程的内存空间都是一致而且固定的(32位平台下都是4G),所以链接器在链接可执行文件时,可以设定内存地址,而不用去管这些数据最终实际内存地址,这交给内核来完成映射关系
  • 当不同的进程使用同一段代码时,比如库文件的代码,在物理内存中可以只存储一份这样的代码,不同进程只要将自己的虚拟内存映射过去就好了,这样可以节省物理内存
  • 在程序需要分配连续空间的时候,只需要在虚拟内存分配连续空间,而不需要物理内存时连续的,实际上,往往物理内存都是断断续续的内存碎片。这样就可以有效地利用我们的物理内存

BIO 与 NIO

非直接缓存区(BIO):

  • 原来采用BIO的架构,在读写本地文件时,我们需要从用户态切换成内核态在这里插入图片描述

直接缓冲区(NIO):

  • NIO 直接操作物理磁盘,省去了中间商赚差价在这里插入图片描述

深入 ByteBuffer 源码:

  • ByteBuffer.allocateDirect() 方法
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}
  • DirectByteBuffer 类的构造器用到了 Unsafe 类分配本地内存
DirectByteBuffer(int cap) {                   // package-private

    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

直接内存与 OOM

直接内存与 OOM:

  • 直接内存也可能导致OutofMemoryError异常

  • 由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

  • 直接内存的缺点为:
    ①分配回收成本较高
    ②不受JVM内存回收管理

  • 直接内存大小可以通过MaxDirectMemorySize设置

  • 如果不指定,默认与堆的最大值-Xmx参数值一致

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值