• JVM_运行时数据区4-对象的实例化


    对象的实例化

    对象的实例化

    创建对象的方式

    • new
      最常见的方式变
      1 : Xxx的静态方法变形
      2 : XxBuilder/XxoxFactory的静态方法
    • Class的newInstance():反射的方式,只能调用空参的构造器,权限必须是public
    • Constructor的newInstance(Xxx):反射的方式,可以调用空参、带参的构造器,权限没有要求
    • 使用clone() :不调用任何构造器,当前类需要实现Cloneable接口,实现clone()
    • 使用反序列化:从文件中、从网络中获取一个对象的二进制流
    • 第三方库Objenesis

    创建对象的步骤

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

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

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

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

    给对象的属性赋值的操作:
    ① 属性的默认初始化
    ② 显式初始化
    ③ 代码块中初始化
    ④ 构造器中初始化
    
    1. 处理并发安全问题
      在分配内存空间时,另外一个问题是及时保证new对象时候的线程安全性:创建对象是非常频繁的操作,虚拟机需要解决并发问题。虚拟机采用 了两种方式解决并发问题:
      CAS ( Compare And Swap )失败重试、区域加锁:保证指针更新操作的原子性;
      TLAB把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区,(TLAB ,Thread Local Allocation Buffer) 虚拟机是否使用TLAB,可以通过一XX:+/一UseTLAB参数来 设定。

    2. 初始化分配到的空间
      内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。

    3. 设置对象的对象头

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

    • 考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,1个比特固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)[1]下对象的存储内容如下所示。

    HotSpot解释器代码片段

    // 确保常量池中存放的是已解释的类 
    if (!constants->tag_at(index).is_unresolved_klass()) {
    	// 断言确保是klassOop和instanceKlassOop(这部分下一节介绍) 
    	oop entry = (klassOop) *constants->obj_at_addr(index);
    	assert(entry->is_klass(), "Should be resolved klass");
    	klassOop k_entry = (klassOop) entry;
    	assert(k_entry->klass_part()->oop_is_instance(), "Should be instanceKlass");
    	instanceKlass* ik = (instanceKlass*) k_entry->klass_part();
    	// 确保对象所属类型已经经过初始化阶段 
    	if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
    		// 取对象长度 
    		size_t obj_size = ik->size_helper();
    		oop result = NULL;
    		// 记录是否需要将对象所有字段置零值 
    		bool need_zero = !ZeroTLAB;
    		// 是否在TLAB中分配对象 
    		if (UseTLAB) {
    			result = (oop) THREAD->tlab().allocate(obj_size);
    		}
    		if (result == NULL) {
    			need_zero = true;
    			// 直接在eden中分配对象 
    			retry: HeapWord* compare_to = *Universe::heap()->top_addr();
    			HeapWord* new_top = compare_to + obj_size;
    			// cmpxchg是x86中的CAS指令,这里是一个C++方法,通过CAS方式分配空间,并发失败的 话,转到retry中重试直至成功分配为止 
    			if (new_top <= *Universe::heap()->end_addr()) {
    				if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {
    					goto retry;
    				}
    				result = (oop) compare_to;
    			}
    		}
    		if (result != NULL) {
    			// 如果需要,为对象初始化零值 
    			if (need_zero ) {
    				HeapWord* to_zero = (HeapWord*) result + sizeof(oopDesc) / oopSize;
    				obj_size -= sizeof(oopDesc) / oopSize;
    				if (obj_size > 0 ) {
    					memset(to_zero, 0, obj_size * HeapWordSize);
    				}
    			}
    			// 根据是否启用偏向锁,设置对象头信息 
    			if (UseBiasedLocking) {
    				result->set_mark(ik->prototype_header());
    			} else {
    				result->set_mark(markOopDesc::prototype());
    			}
    			result->set_klass_gap(0);
    			result->set_klass(k_entry);
    			// 将对象引用入栈,继续执行下一条指令
    			SET_STACK_OBJECT(result, 0);
    			UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
    		}
    	}
    }
    
    1. 执行init方法进行初始化
      在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。 因此一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之 后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。
    • 代码实例
    /**
     * 测试对象实例化的过程
     *  ① 加载类元信息 - ② 为对象分配内存 - ③ 处理并发问题  - ④ 属性的默认初始化(零值初始化)
     *  - ⑤ 设置对象头的信息 - ⑥ 属性的显式初始化、代码块中初始化、构造器中初始化
     *
     *  给对象的属性赋值的操作:
     *  ① 属性的默认初始化 - ② 显式初始化 / ③ 代码块中初始化 - ④ 构造器中初始化
     * 
     */
    public class Customer{
    	int id = 1001;
    	String name;
    	Account acct;
    	{
    		name = "匿名客户";
    	}
    	public Customer(){
    		acct = new Account();
    	}
    }
    class Account{
    }
    

    对象的内存布局

    对象头

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

    实例数据

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

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

    对齐填充

    这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

    小结

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

    对象的访问定位

    JVM是如何通过栈帧中的对象引|用访问到其内部的对象实例的呢?-> 定位,通过栈上reference访问

    句柄访问

    如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如图所示。

    使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

    直接访问

    如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如图所示

    使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机HotSpot而言,它主要使用第二种方式进行对象访问

    直接内存

    相关链接:https://juejin.im/post/6844903822091878408

    概念

    • 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域
    • 直接内存是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();
    	}
    }
    
    • 来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存

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

    异常

    也可能导致OutOfMemoryError异常:OutOfMemoryError: Direct buffer memory

    /**
     * 本地内存的OOM:  OutOfMemoryError: Direct buffer memory
     */
    public class BufferTest2 {
    	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);
    		}
    	}
    }
    
    • 由于直接内存在Java堆外,因此它的大小不会直接受限于一Xmx指定的最大 堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
    • 缺点
      分配回收成本较高不受JVM内存回收管理
    • 直接内存大小可以通过MaxDirectMemorySize设置
    • 如果不指定,默认与堆的最大值一Xmx参数值一致
  • 相关阅读:
    300+值得收藏的设计师免费资源站
    Apache 隐藏入口文件 index.php
    Nginx 虚拟主机下支持Pathinfo并隐藏入口文件的完整配置
    Java多线程
    Java注解
    Java异常机制
    面向对象
    数组
    Java方法(函数)
    Java流程控制(Scanner)
  • 原文地址:https://www.cnblogs.com/suit000001/p/13608439.html
Copyright © 2020-2023  润新知