JVM运行时数据区篇(有关对象的扩展知识)

Posted ProChick

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM运行时数据区篇(有关对象的扩展知识)相关的知识,希望对你有一定的参考价值。

1.对象的实例化

  • 对象的创建方式

    • 使用new关键字:也是最常见的方式、包括单例模式创建的、工厂模式创建的、构造者模式创建的
    • 使用Class的newInstance方法:反射的方式,只能调用空参的构造器,权限必须是public
    • 使用Constructor的newInstance方法: 反射的方式,可以调用空参、带参的构造器,权限没有要求
    • 使用clone:不调用任何构造器,当前类需要实现Cloneable接口
    • 使用反序列化:从文件中或网络中获取一个对象的二进制流
    • 使用第三方库动态生成:例如Objenesis
  • 对象的创建步骤

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

      • 当虚拟机遇到一条new指令时,首先去检查这个指令的参数能否在 Metaspace 元空间的常量池中定位到一个类的符号引用,并且检查这个符号引用指向的类是否已经被加载、解析和初始化
      • 如果没有加载,那么在双亲委派模式下,使用当前类加载器查找对应的.class文件。 如果没有找到该文件,则抛出 ClassNotFoundException 异常。如果找到,则进行类加载,并生成对应的Class类对象
    • 为对象分配内存空间

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

      • 如果内存空间规整,则使用指针碰撞法进行分配

        意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带有compact (整理)过程的收集器时,使用指针碰撞。

      • 如果内存空间不规整,则虚拟机需要维护一个列表,然后使用空闲列表进行分配

        意思是已使用的内存和未使用的内存相互交错,那么虛拟机将采用的是空闲列表法来为对象分配内存。虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。

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

    • 处理并发安全问题

      • 在为对象分配内存空间时,另外一个问题是能够及时保证线程的安全性,因为创建对象是非常频繁的操作,所以虚拟机需要解决并发问题
      • 第一种方式:采用CAS失败重试、区域加锁,这可以保证指针更新操作的原子性
      • 第二种方式:采用TLAB,即每个线程在Java堆中预先分配一小块内存
    • 初始化分配的内存空间

      • 内存空间分配结束后,虚拟机将会为分配到的内存空间进行初始化,也就是属性的默认初始化
      • 这一步保证了对象的实例字段在Java代码中可以不用赋值就可以直接使用,程序能访问到这些字段的数据类型所对应的默认值
    • 设置对象的头信息

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

      • 在这一步中,JVM会将成员变量进行显式赋值、执行实例化代码块、调用类的构造方法,并把堆内该对象的首地址赋值给引用变量
      • 所以一般在执行new指令之后,紧接着就是执行初始化有关的方法

2.对象的内存布局

  • 对象头

    • 运行时元数据
      • 哈希值( HashCode:对象的地址 )
      • GC分代年龄
      • 锁状态标志
      • 线程持有的锁
      • 偏向线程ID
      • 偏向时间戳
    • 类型指针:指向类元数据的InstanceClass,以此确定该对象所属的类型
    • 如果是数组,还需记录数组的长度
  • 实例数据

    • 它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段,包括从父类继承下来的和本身所拥有的字段
    • 规则
      • 相同宽度的字段总被分配在一起
      • 父类中定义的变量会出现在子类之前
      • 如果CompactFields参数为true,子类的窄变量可能插入到父类变量的空隙
  • 对齐填充

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

程序示例

public class Account{

}

public class Customer{
    int id = 1001;
    String name;
    Account acct;

    {
        name = "匿名客户";
    }
    
    public Customer(){
        acct = new Account();
    }
}

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

3.对象的访问定位

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

  • 采用句柄访问的方式

    • 优点:当对象实例数据由于垃圾回收机制发生移动的时候,我们不需要更改栈中的引用地址
    • 缺点:需要单独开辟一块空间维护句柄池,而且查找对象实例的效率也比较低
  • 采用直接指针的方式( HotSpot虚拟机 )

    • 优点:查找效率高,直接指向了实例对象
    • 缺点:当对象实例数据由于垃圾回收机制发生移动的时候,我们同时需要更改栈中的引用地址

4.什么是直接内存

  • 基本概述

    • 首先直接内存不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域
    • 直接内存是Java堆外的、直接向系统申请的内存区间,也被叫做本地内存。在JDK8之后,元数据空间就存放在本地直接内存中
    • 我们可以理解为:Java整个进程内存空间 = Java堆的空间 + 本地内存
    • 这种方式在Java中主要来源于NIO,通过利用存在于堆当中的DirectByteBuffer方法操作Native内存

    程序示例

    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();
        }
    }
    


  • 优势缺点

    • 通常,访问直接内存的速度会优于访问Java堆的速度,即读写性能高。因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存,在Java中的NIO库允许Java程序使用直接内存,用于作数据缓冲区

    • 当然,使用直接内存也可能导致 OutOfMemoryError 异常。 由于直接内存在Java堆外,因此它的大小不会直接受限于JVM的分配,但是系统内存是有限的,如果申请的空间超过了系统的空闲空间,那么就会出现OOM异常

      private static final int BUFFER = 1024 * 1024 * 20;//20MB
      
      public static void main(String[] args) {
          ArrayList<ByteBuffer> list = new ArrayList<>();
      
          int count = 0;
          try {
              while(true){
                  ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
                  list.add(byteBuffer);
                  count++;
                  try {
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          } finally {
              System.out.println(count);
          }
      }
      

    • 直接内存的分配回收成本较高,它不受JVM内存回收管理

  • 相关参数设置

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

以上是关于JVM运行时数据区篇(有关对象的扩展知识)的主要内容,如果未能解决你的问题,请参考以下文章

JVM运行时数据区篇(本地方法栈)

JVM运行时数据区篇(堆空间进阶掌握)

JVM运行时数据区篇(程序计数器)

JVM运行时数据区篇(基础认知)

JVM运行时数据区篇(虚拟机栈)

JVM运行时数据区篇(虚拟机栈帧结构)