【JVM】【第八章】【对象专题】对象创建、内存分布、访问定位、直接内存

大纲

在这里插入图片描述

1.创建对象的几种方式

① new

应用场景:

  • 普通的new;
  • 单例模式中通过类的静态方法来获得实例;
  • 工厂模式;

② Class对象的newInstance()方法 反射

  • 在JDK9里面被标记为过时的方法
  • 此方式只能调用无参构造器,而且构造器的权限要求必须是public

③ Constructor的newInstance(XXX)方法 反射

  • 此方式可以调用无参、含参构造器,且对构造器权限没有要求

④ 使用clone()

  • 不用任何构造器,只需要类实现Cloneable接口,实现clone()

⑤ 反序列化

从文件中、网络中获取一个对象的二进制流

⑥ 第三方库Objenesis

2.创建对象的过程

1. 字节码分析

(1)示例代码:

/**
 * @author shkstart  shkstart@126.com
 * @create 2020  17:16
 */
public class ObjectTest {
    public static void main(String[] args) {
        Object obj = new Object();
    }
}

(2)字节码指令解析:
在这里插入图片描述
0: new #2

  • new指令

当遇到new指令时,首先去检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经经过类加载初始化过(也就是找这个类的Class对象)。

如果没有就必须进行类加载,并且为对象分配内存空间、属性的默认初始化。

  • #2 是运行时常量池中的字面量。

3: dup

  • dup指令
    把操作数栈中的引用变量复制一份,此时就有两个引用指向堆空间对象实体,栈底的用于赋值操作,上面的用于句柄其他操作。

4: inovkespecial #1

  • 调用()方法对对象进行初始化

2. 具体过程

1.判断对象是否进行过类加载阶段

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

2.为对象分配内存空间

首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。

step1. 对象的内存空间大小计算
  • 如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小;
  • 对于基本数据类型,int short byte boolean 引用类型都占四个字节,所以对象所占堆空间能够确定下来,并且这些对象属性进行默认初始化;
step2. 分配内存空间

不同的内存分配策略:

1.指针碰撞法
  • 策略使用背景

如果内存是规整的,那么虚拟机将采用的是指针碰撞法(Bump The Point)来为对象分配内存。

规整的意思是所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器,为对象分配内存就仅仅是把指针往空闲内存那边挪动一段与对象大小相等的距离。

  • 策略实际应用

如果垃圾收集器选择的是Serial ,ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带Compact(整理)过程的收集器时,使用指针碰撞。

标记压缩(整理)算法会整理内存碎片,堆内存一存对象,另一边为空闲区域

2.空闲列表法
  • 策略使用背景

Java堆内存并不规整,空闲的和被使用的相互交错在一起,此时无法用指针碰撞,JVM需要维护一个空闲列表,记录可用内存块,在分配内存的时候,从空闲列表找一个足够大的空间划分给对象实例,并更新表记录。

意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为了 “空闲列表(Free List)”

3.两种分配策略的选择

选择哪种分配方式由Java堆是否规整所决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

标记清除算法清理过后的堆内存,就会存在很多内存碎片。

3.处理并发安全问题

对象在创建过程中,是修改指针所指向的位置,并发情况下该指针不是线程安全的。

解决方案:

(1)对分配内存空间的动作进行同步处理

  • 虚拟机采用CAS + 失败重试的方式保证更新操作的原子性。

(2)内存分配动作按照线程划分在不同的空间中进行

  • 每个线程在java堆中预先分配一小块线程私有内存,叫做本地线程分配缓冲区(TLAB),对象在TLAB中分配,TLAB用完了,分配新的缓存区时才需要同步锁定。

  • TLAB是否使用,通过-XX:+/-UseTLAB参数来决定。

4.初始化分配到的内存

将对象的所有属性设置默认值(不涉及对象头),保证对象实例字段在不赋值可以直接使用

5.设置对象的对象头

对象头存储: 对象所属的类(类的元数据)、对象的哈希值、对象的GC分代年龄信息、锁信息。

将这些数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。

6.执行init()方法,完成对象的初始化

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

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

  • init()方法包括:
    1.显示赋值和代码块赋值(按代码顺序)
    2.构造器赋值(最后)

总结
前五步骤相当于给捏小人,最后一步相当于上色。

3.对象内存布局(☆☆☆☆☆)

3.1 对象头

对象头包含两部分:运行时元数据(Mark Word)类型指针

(1)Mark Word

就是对象自身运行时所需要的元数据

  • 哈希值(HashCode),可以看作是堆中对象的地址
  • GC分代年龄(年龄计数器)
  • 锁状态标志
  • 线程持有的锁
  • 偏向线程ID
  • 偏向时间戳
  • 如果对象是一个数组,那么对象头中还必须存储数组长度。

Mark Word在32位和64位虚拟机中,长度分别为32bit和64bit

(2)类型指针

  • 对象指向该对象的类型元数据(存放在方法区中)的指针,此指针用于确定该对象是哪个类的实例
  • 并不是说要获取对象的类型信息都要经过对象本身(对象头),这要看对象是如何访问定位的。

3.2 实例数据

  • 对象真正存储的有效信息。包含在Java程序中定义的各种类型的字段 (包括从父类继承下来的,以及自身定义的字段)

存储规则:

  1. 相同宽度的字段被分配到一起存放;比如 8字节放一起,2字节放一起…
    double/long->int->shorts/chars->bytes/booleans->oops
  2. 在规则1的基础上,父类中定义的变量会出现在子类之前
  3. 如果JVM设置 +XX:CompactFields 参数值为true,子类中较窄的变量会允许插入到父类变量的缝隙中,以节省一些空间。

3.3 对齐填充

  • 无特别含义,作用仅仅是占位符。
  • HotSpot虚拟机自动内存管理系统要求对象起始地址必须是8字节的整数倍,对象的大小必须是8字节的整数倍,如果没对齐,就用这个填充对齐。

3.4 创建对象时的内存分布图解

public class Customer{
	int id = 1001;
	String name;
	Account acct;
	{name = "客户"}

	public Customer(){
		acct = Account();
	}
}

class Account{}
public class CustomerTest {
    public static void main(String[] args) {
        Customer cust = new Customer();
    }
}

在这里插入图片描述

4.对象访问定位

JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢?

在这里插入图片描述

①. 句柄访问

首先,java堆中划分出一块内存作为句柄池,栈上的引用存储的信息就是对象的句柄地址。一个对象对应的句柄中包含了 到对象实例数据的指针到对象类型数据的指针,一个指向堆中的对象实例,一个指向方法区中对象的类型信息存储地址。

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

②. 直接指针(HotSpot采用)

栈上的引用存储的是java堆中对象实例数据的地址,到对象的类型信息的指针 就存储在对象头中,这种对象访问定位方式 如果要获取对象数据类型数据就必须经过对象实例才可。
在这里插入图片描述
1.优点: 直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据
2.缺点: 对象被移动(垃圾收集时移动对象很普遍)时需要修改 reference 的值

5. 直接内存(Direct Memory)

①. 直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。

②.直接内存是java堆外内存,不在jvm内存中,直接向系统申请的内存区间。
③.Jdk8后元空间用的就是直接内存
④.通常,访问直接内存的速度要优于Java堆,因为读写性能高。因此读写频繁的场合会使用直接内存来提高性能。
⑤.直接内存来源于NIO,通过存在堆中的DirectByteBuffer对象来操作本地内存
⑥.Java的NIO库允许java程序使用直接内存,用于数据缓冲区。

⑦. 代码演示:

/**
 *  IO                  NIO (New IO / Non-Blocking IO)
 *  byte[] / char[]     Buffer
 *  Stream              Channel
 *
 * 查看直接内存的占用与释放
 */
public class BufferTest {
    private static final int BUFFER = 1024 * 1024 * 1024;//1GB

    public static void main(String[] args){
        //直接分配本地内存空间
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
        System.out.println("直接内存分配完毕,请求指示!");

        Scanner scanner = new Scanner(System.in);
        scanner.next();

        System.out.println("直接内存开始释放!");
        byteBuffer = null;
        System.gc();
        scanner.next();
    }
}
  • 创建DirectByteBuffer直接内存对象,该对象使用直接内存。参数是字节数组的长度,单位是字节。
  • 代码里的意思是创建一个大小为1GB的DirectByteBuffer对象,然后通过丢失引用,垃圾回收,回收此直接内存对象。会发现Java程序占用内存小了很多
    在这里插入图片描述
    释放后,Java程序的内存占用明显减少
    在这里插入图片描述

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

直接内存也会导致OutOfMemoryError异常

  • 直接内存在堆外,因此其大小不会受限于虚拟机内存的大小。

  • 但是系统内存是 有限的,java堆和直接内存的总和依然受限于操作系统可以给出的最大内存。

  • 直接内存溢出异常:
    java.lang.OutOfMemoryError: Direct buffer memory

  • JVM内存可视化工具是无法看见直接内存的情况的

  • 简单理解: java process memory = java heap + native memory

直接内存的缺点

1)分配回收成本高:直接内存的回收
2)不受JVM内存回收管理

直接内存的大小设置

直接内存大小可以通过MaxDirectMemorySize设置,如果不指定,默认与堆的最大值一Xmx参数值一致

在这里插入图片描述
jdk1.8之后JVM内存结构可以理解为:java运行时数据区 + 本地内存

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值