JVM中Java对象的创建

来源:互联网 发布:淘宝母婴店名字 编辑:程序博客网 时间:2024/05/18 01:22

Java是一门面向对象的编程语言,在Java程序运行过程中无时无刻都有对象被创建出来,在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个new关键字而已,而在虚拟机中,对象(仅限于普通Java对象,不包括数组和Class对象等)的创建又是怎样一个过程呢?

1.
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。(加载过程解析见http://blog.csdn.net/x_iya/article/details/78733427)

HotSpot虚拟机的处理流程:
如果已经加载,就从常量池中根据index->CPSlot,然后得到 Klass 的地址,如果该类已被加载,则地址最后一位为0。然后将 Klass 强转成 InstanceKlass ,继而从 InstanceKlass 中得到该类的类型元数据,如 size ;根据 size 从堆中分配相应的空间用于存储新创建的类型实例。
openjdk\hotspot\src\share\vm\interpreter\bytecodeInterpreter.cpp

      CASE(_new): {        u2 index = Bytes::get_Java_u2(pc+1);        ConstantPool* constants = istate->method()->constants();        // 确保常量池中存放的是已解释的类        if (!constants->tag_at(index).is_unresolved_klass()) {          // Make sure klass is initialized and doesn't have a finalizer          Klass* entry = constants->slot_at(index).get_klass();          // 断言确保是 Klass 和 InstanceKlass          assert(entry->is_klass(), "Should be resolved klass");          Klass* k_entry = (Klass*) entry;          assert(k_entry->oop_is_instance(), "Should be InstanceKlass");          InstanceKlass* ik = (InstanceKlass*) k_entry;          // 确保对象所属类型已经经过初始化阶段          if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {            // 取对象长度            size_t obj_size = ik->size_helper();            oop result = NULL;            // If the TLAB isn't pre-zeroed then we'll have to do it            // 记录是否需要将对象所有字段置零值            bool need_zero = !ZeroTLAB;            // 是否在TLAB中分配对象            if (UseTLAB) {              result = (oop) THREAD->tlab().allocate(obj_size);            }            if (result == NULL) {              need_zero = true;              // Try allocate in shared eden              // 直接在 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) {              // 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) {                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);            }          }        }        // 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);      }
CPSlot slot_at(int which) {assert(is_within_bounds(which), "index out of bounds");// Uses volatile because the klass slot changes without a lock.volatile intptr_t adr = (intptr_t)OrderAccess::load_ptr_acquire(obj_at_addr_raw(which));assert(adr != 0 || which == 0, "cp entry for klass should not be zero");return CPSlot(adr);}
class CPSlot VALUE_OBJ_CLASS_SPEC {intptr_t _ptr;public:CPSlot(intptr_t ptr): _ptr(ptr) {}CPSlot(Klass* ptr): _ptr((intptr_t)ptr) {}CPSlot(Symbol* ptr): _ptr((intptr_t)ptr | 1) {}intptr_t value()   { return _ptr; }bool is_resolved()   { return (_ptr & 1) == 0; }bool is_unresolved() { return (_ptr & 1) == 1; }Symbol* get_symbol() {assert(is_unresolved(), "bad call");return (Symbol*)(_ptr & ~1);}Klass* get_klass() {assert(is_resolved(), "bad call");return (Klass*)_ptr;}};

这里写图片描述
2.
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
分配内存的方式有两种:指针碰撞与空闲列表。
类加载的两个重要任务:在堆中生成Class对象;在方法区生成元数据

  1. 并发情况下为了保持线程安全需要对其进行处理。
    方法一:对分配内存空间的动作进行同步处理—-实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
    方法二:每个线程在Java堆中预先分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),内存分配在该线程自己的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。

  2. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。

  3. 虚拟机对对象进行必要的设置(设置对象头)。

上边的5步都完成之后,从虚拟机的角度来看,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚刚开始–<init> 方法还没有执行,所有的字段还是零。
<init>与<clinit>的区别
简单的来说<init> 方法是由JVM生成的一个方法并由JVM调用,完成对非静态变量解析初始化。

对于如下的简单测试程序

public class Demo {    public static void main(String[] args) {        Demo demo = new Demo();    }}
D:\N3verL4nd\Desktop>javac Demo.javaD:\N3verL4nd\Desktop>javap -v DemoClassfile /D:/N3verL4nd/Desktop/Demo.class  Last modified 2017-12-20; size 270 bytes  MD5 checksum 64db768d73abb9750bd198c6655b932c  Compiled from "Demo.java"public class Demo  minor version: 0  major version: 52  flags: ACC_PUBLIC, ACC_SUPERConstant pool:   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V   #2 = Class              #14            // Demo   #3 = Methodref          #2.#13         // Demo."<init>":()V   #4 = Class              #15            // java/lang/Object   #5 = Utf8               <init>   #6 = Utf8               ()V   #7 = Utf8               Code   #8 = Utf8               LineNumberTable   #9 = Utf8               main  #10 = Utf8               ([Ljava/lang/String;)V  #11 = Utf8               SourceFile  #12 = Utf8               Demo.java  #13 = NameAndType        #5:#6          // "<init>":()V  #14 = Utf8               Demo  #15 = Utf8               java/lang/Object{  public Demo();    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=1, locals=1, args_size=1         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."<init>":()V         4: return      LineNumberTable:        line 1: 0  public static void main(java.lang.String[]);    descriptor: ([Ljava/lang/String;)V    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=2, locals=2, args_size=1         0: new           #2                  // class Demo         3: dup         4: invokespecial #3                  // Method "<init>":()V         7: astore_1         8: return      LineNumberTable:        line 3: 0        line 4: 8}SourceFile: "Demo.java"

对于 new Demo();
在Java层面:它完成分配空间与调用构造器的任务。在Java中分配内存空间与初始化是绑定到一起的。
在JVM层面:

// 分配空间给新对象0: new           #2// class Demo3: dup// 调用构造器4: invokespecial #3// Method "<init>":()V

new字节码指令的作用是创建指定类型的对象实例、对其进行默认初始化,并且将指向该实例的一个引用压入操作数栈顶;

dup指令的作用:

复制之前分配的Demo空间的引用并压入栈顶。那么这里为什么需要这样么做呢?因为 invokespecial 指令通过 #3 这个常量池入口寻找到了 Demo() 构造方法,构造方法虽然找到了。但是必须还得知道是谁的构造方法,所以要将之前分配的空间的应用压入栈顶让 invokespecial 命令应用才知道原来这个构造方法是刚才创建的那个引用的,调用完成之后将栈顶的值弹出。astore_1:将此时的栈顶值弹出存入局部变量中去。

RednaxelaFX大牛的解释:

因为 invokespecial 会消耗掉操作数栈顶的引用作为传给构造器的“this”参数,所以如果我们希望在invokespecial调用后在操作数栈顶还维持有一个指向新建对象的引用,就得在 invokespecial 之前先“复制”一份引用——这就是这个 dup 的来源。

综上,new Demo() 有三个作用:
- 创建并默认初始化一个 Demo 类型的对象
- 调用 Demo 类的signature为 <init>()V 的构造器
- 表达式的值为一个指向这个新建对象的引用。


对象的内存布局

1.HotSpot虚拟机,对象在内存中存储的布局可以分为三个区域:对象头Header,实例数据Instance Data,对齐填充Padding。

  1. 对象头包含两部分信息:第一,存储对象自身的运行时数据(哈希码,GC分代年龄,锁状态标志,线程持有锁,偏向线程ID,偏向时间戳),长度是非固定的数据结构。第二,存储类型指针(32bit),即对象指向它的类元数据的指针,可以通过这个指针确定这个对象是哪个类的实例。但并不是所有的虚拟机实现都必须在对象数据上保留类型指针。
    如果对象是Java数组,还存储数组长度。

  2. 实例数据是对象真正存储的有效信息,也就是定义的各种字段的内容。

  3. 对齐填充不是必然存在的,对象必须是8字节的整数倍,当对象实例数据部分不满足时就会有对齐填充。

对象的访问定位

我们需要通过栈上的reference数据来操作堆上的具体对象,目前主流的访问方式有两种:使用句柄和直接指针。

  1. 使用句柄,java堆中将会划分出一块内存来作为句柄池,Reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。这里写图片描述

  2. 使用直接指针:Java堆对象必须考虑如何放置访问类型数据的相关信息(对象头),而reference中存储的直接就是对象地址。这里写图片描述

这里写图片描述

原创粉丝点击