JVM内存区域(二)

JVM内存区域(二)

1. 对象的创建

1.1 对象创建过程

Java是一种面向对象的语言,在使用java的日常,我们常常和对象打交道。在JVM内存区域中也存在专门的堆内存来存储管理对象。因此就堆内存和对象之间的关系,做简单的阐述。

虚拟机遇到一条new指令后,虚拟机就会新建一个对象,整个过程虚拟机大致会做如下工作:

  • 检查对象对应的类是否被加载、解析和初始化过
  • 为对象在堆内存上分配内存
  • 初始化新分配的内存为零值
  • 虚拟机对对象进行必要的设置(信息存放对象头)

在上面的工作都完成之后,从虚拟机的角度来看,一个新的对象已经产生了。但从java程序角度来看,对象创建才刚刚开始——<init>方法还没执行,所有的字段都还为零。执行完new指令后接着执行init方法,把对象按照程序员的意图进行初始化,这样一个真正可用对象才算完全产生出来。

1.2 堆内存分配的问题

虚拟机遇到一条new指令后,首先会去检查该对象对应的类是否被加载、解析和初始化过(通常称为类加载过程),如果类没有加载进来,则首先进行类加载。类加载也是一个重要的过程,后面的博客会详细介绍。

在类加载通过虚拟机的检查后,虚拟机会首先在堆内存中为新生对象分配空间。值得注意的是,对象所需要的内存空间大小在类加载后是可以完全确定的(如何确定的本文第2节会介绍)。

在为对象分配堆上内存空间的时候,存在两个主要的问题:

  • a. 内存块连续性的问题
  • b. 多线程同步的问题

a. 线程运行过程中,需要经常向堆内存申请内存分配,但是通常来讲申请的内存需要是连续的(例如新建数组)。内存中有空闲区占用区。根据空闲区和占用区的位置之间的不同,可以分为两种内存分配方式:

  • 指针碰撞 bump the pointer: 堆内存是规整的,一边是占用区,另一边是空闲区;中间采用指针指示,需要申请新的内存时候,只需要将指示器指针移动一定的距离即可
  • 空闲列表:如果堆内存不是规整的,即占用区和空闲去交错分布;因此通操作系统的存储管理一样,虚拟机需要维护一个空闲去列表记录哪些空闲区可用;在申请内存分配的时候,从空闲区寻找到一个大小合适的区域分配出去。

采用何种方式进行内存分配取决于堆内存的规整性;而堆内存的规整性取决于虚拟机采用的垃圾回收算法是否带有压缩整理功能决定的。因此在使用SerialParNew等带Compact过程的垃圾回收器,虚拟机内存分配采用bump the pointer;而在使用CMS这种基于Mark-Sweep算法的垃圾回收器时,通常采用空闲列表这种方式。

b. 除了以何种方式从堆内存空间分配空间以外,还存在另一个线程安全的问题;当多个线程申请内存分配的时候,可能虚拟机正在使用内存指针给线程A分配内存的时候,线程B用采用原来的内存指针来进行内存分配了。这是典型的多线程问题,解决方法有两种:

  • 对分配内存空间进行同步处理,保证分配操作是线程安全的、原子性的;实际上虚拟机采用的是WCAS(Compare and Set)配上失败重试的方法保证指针更新操作的原子性;
  • 线程封闭的方式;把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在堆中预先分配一小块内存称为本地线程分配缓冲(Thread Local Allocation Buffer TLAB)。哪个线程需要分配内存,就在对应的TLAB上分配,只有TLAB用完并分配新的TLAB的时候,才需要同步锁定。同步需要额外的花销。

1.3 源码分析

以下代码片段来自于http://hg.openjdk.java.net/jdk7/jdk7/hotspot/archive/9b0ca45cd756.zip/src/上下载openjdk7的源代码中的bytecodeInterpreter.cpp文件。


  CASE(_new): {//遇到new指令
        u2 index = Bytes::get_Java_u2(pc+1);//根据下一个pc找到一个index,其实就是new SomeClass()中的SomeClass
        constantPoolOop constants = istate->method()->constants();//获取方法区的常量池
        if (!constants->tag_at(index).is_unresolved_klass()) {//如果需要新建对象的类被解析过
          // Make sure klass is initialized and doesn't have a finalizer
          oop entry = constants->slot_at(index).get_oop();//根据index在常量池中找到对应oop(ordinary object pointer),oop指向加载解析过的类对象的指针
          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();//确定对象的大小的;第2节会介绍;
            oop result = NULL;
            // If the TLAB isn't pre-zeroed then we'll have to do it
            bool need_zero = !ZeroTLAB;//是否需要将内存区域0值化
            if (UseTLAB) {//判断是否是TLAB的方式
              result = (oop) THREAD->tlab().allocate(obj_size);
            }

            //以下是内存分配多线程同步的CAS加失败重试的方案
            if (result == NULL) {//result为NULl,表示不是采用TLAB解决内存分配多线程的问题
              need_zero = true;
              // Try allocate in shared eden
              //直接在堆内存中的eden代中分配
        retry:
              HeapWord* compare_to = *Universe::heap()->top_addr();
              HeapWord* new_top = compare_to + obj_size;
              if (new_top <= *Universe::heap()->end_addr()) {
                //Atomic::cmpxchg_ptr是一个采用CAS的原子操作,用于将compare_to指针更新;
                if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {
                  goto retry;
                }
                result = (oop) compare_to;//内存分配成功
              }
            }
            if (result != NULL) {
              // Initialize object (if nonzero size and need) and then the header
              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) {//根据是否启用偏向锁来设置Mark Word
                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);//将result指针如虚拟机栈中,保存在局部变量表中;
              UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);//更新程序计数器,继续下一个操作;
            }
          }
        }
        //如果类没有被解析;则会进行一系列复杂的类加载、内存分配等操作;这是一种较慢的情况;
        // Slow case allocation
        CALL_VM(InterpreterRuntime::_new(THREAD, METHOD->constants(), index),
                handle_exception);
        SET_STACK_OBJECT(THREAD->vm_result(), 0);
        THREAD->set_vm_result(NULL);
        UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
      }

2. 对象的内存布局

HotSpot虚拟机中,对象在内存中存储的布局可以分为3个区域,对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头包含两种信息:

  • 第一部分用于存储对象本身的运行时数据。如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;这部分数据的长度在32bit(32位虚拟机)或者64bit(64位虚拟机),被称为”Mark Word”。根据对象所处的状态不同,Mark Word存储的内容也不同;

  • 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机可以通过这个来确定对象是那个类的实例。(并不是所有的虚拟机实现都必须在对象数据上保留类型指针,也就是查找对象的类元数据并非一定要经过对象本身,第3节介绍的基于句柄方式的对象定位,就不要通过对象而可以找到对应的类元数据)

另外如果对象是一个Java数组的话,还需要在对象头必须有一块用于记录数组长度的数据,因为虚拟机需要通过对象的元数据信息确定对象的大小,而从类元数据中却无法确定数组的大小。

实例数据存放了对象真正意义上的有效信息,也就是程序中定义的各类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。

对齐填充并不是必然存在的,由于HotSpot的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍,因此当对象实例数据部分没有对齐的时候,就需要对其填充来补全。

3. 对象的访问定位

java语言规范中,java需要通过虚拟机栈上的reference数据来操作堆上的具体对象。在虚拟机规范中,由于reference类型只是一个指向对象的引用,并没有定义如何通过这种引用去定位、方位堆上的对象的具体位置。目前主流的访问方式有使用句柄直接指针两种

  • 句柄方式;堆上会划分一块内存作为句柄池,引用存储的是对象在句柄池中的句柄地址,而句柄中包含了对象和其对应的类型数据各具体信息的地址信息;
  • 直接地址;引用存储的直接就是对象的堆内存地址,而采用这种方式的话,对象中必须要设置如何访问对应的类型数据的相关信息;

各自的优缺点如下:

句柄方式

  • 优点:句柄方式在对象被移动时(GC时移动对象是非常普遍的)只改变句柄中的实例数据指针,而引用本身不用改变
  • 缺点:中间加了一层,访问变慢;

直接指针:

  • 优点:访问速度快
  • 缺点:对象频繁移动,对引用的修改

参考:《深入理解Java虚拟机》

原文地址:https://www.cnblogs.com/Spground/p/9567903.html