JVM源码剖析之Java对象创建过程

这篇具有很好参考价值的文章主要介绍了JVM源码剖析之Java对象创建过程。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

关于 "Java的对象创建" 这个话题分布在各种论坛、各种帖子,文章的水平参差不齐。并且大部分仅仅是总结 "面试宝典" 的流程,小部分就是copy其他帖子,极少能看到拿源码作为论证。所以特意写下这篇文章。

版本信息如下:

jdk版本:jdk8u40
为了源码的简单,使用字节码解释器:C++解释器
为了源码的简单,垃圾回收器使用serial new/old

JVM源码剖析之Java对象创建过程,Java底层,源码解读,java,jvm,Hotspot源码首先把总结图放在这。接下来分析源码~ 

用一个非常简单的案例来打开新世界的大门。 

public class demo{
  public static void main(String[] args)  {
     new A();
  }
}
class A{
  static{
    System.out.println("123");
  }
}

通过javac命令编译后的字节码如下:

Constant pool:
   #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V
   #2 = Class              #15            // A
   #3 = Methodref          #2.#14         // A."<init>":()V
   #4 = Class              #16            // demo
   #5 = Class              #17            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = Utf8               SourceFile
  #13 = Utf8               demo.java
  #14 = NameAndType        #6:#7          // "<init>":()V
  #15 = Utf8               A
  #16 = Utf8               demo
  #17 = Utf8               java/lang/Object
{
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #2                  // class A
         3: dup
         4: invokespecial #3                  // Method A."<init>":()V
         7: pop
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 8
}

可以清楚的看到,main方法中就简简单单的几条字节码指令,而在Hotspot中执行器分为解释器和JIT,所以为了分析的简单,我们使用c++解释器。src/share/vm/interpreter/bytecodeInterpreter.cpp 文件中的run方法。

CASE(_new): {
        // 拿到new指令携带的指向常量池的下标
        u2 index = Bytes::get_Java_u2(pc+1);
        ConstantPool* constants = istate->method()->constants();
        // 判断当前类是否已经解析
        if (!constants->tag_at(index).is_unresolved_klass()) {
          Klass* entry = constants->slot_at(index).get_klass();
          Klass* k_entry = (Klass*) entry;
          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 (result == NULL) {
              need_zero = true;
            retry:
            /*
              这里很简单,由于类是对象的模板,所以开辟对象的大小都已经是确定值。
              在Java堆初始化就已经使用mmap系统调用开辟了一大段空间,并且根据垃圾回收器和垃圾回收策略决定好空间的分布
              所以当前只需要从开辟好的空间中得到当前对象所需的空间的大小作为当前对象的内存。
              并发的情况下就使用cmpxchg_ptr保证Java堆内存的原子性。
              而c/c++很妙的地方在于可以直接操作内存,可以动态对内存的解释做改变(改变指针类型)
              所以得到一小段空间并返回基地址(对象的起始地址),而这片空间直接使用oop来解释。
              如果:后续给对象中某个属性赋值,这将是一个很简单的寻址问题,已知基地址 + 对象头的常数偏移量 + 偏移量(类中保存了对象排布)= 属性的地址
            */
              HeapWord* compare_to = *Universe::heap()->top_addr();
              HeapWord* new_top = compare_to + obj_size;
              if (new_top <= *Universe::heap()->end_addr()) {
                // cas确保Java堆空间的原子性,并发的情况下失败了就重试。
                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);
              // 对象头部存在klass的指针。
              result->set_klass(k_entry);
              // 发布,让其他线程可见
              OrderAccess::storestore();
              // 把对象地址放入到0号操作数栈中。
              SET_STACK_OBJECT(result, 0);
              UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
            }
          }
        }
        // 上面仅仅是优化开辟,如果优化开辟的条件不通过,此时走漫长的开辟过程
        CALL_VM(InterpreterRuntime::_new(THREAD, METHOD->constants(), index),
                handle_exception);
        // 发布,让其他线程可见
        OrderAccess::storestore();
        // 把对象地址放入到0号操作数栈中。
        SET_STACK_OBJECT(THREAD->vm_result(), 0);
        THREAD->set_vm_result(NULL);
        UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
      }

这里是对new字节码指令的解释,过程比较复杂,代码中已经附上了详细的注释了。这里也对其做一个简单的总结:

  1. 取到new字节码指令携带的常量,用于指向常量池,拿到类信息。
  2. 如果已经解析,如果已经初始化,如果支持快速开辟对象,此时会拿到Java堆中Eden空间中未开辟的空间,然后CAS尝试开辟对象空间(因为Java堆空间是共享的,所以存在多线程的竞争问题,所以使用CAS保证开辟对象空间的原子性)。开辟成功后,将内存初始化为0,设置对象头,设置Klass指针,然后保存到操作数栈中。
  3. 如果不满足已经解析、已经初始化、快速开辟对象,此时就会走InterpreterRuntime::_new方法走慢开辟逻辑,也是我们下面需要分析的代码。
  4. 当InterpreterRuntime::_new方法开辟好对象后,会放入到操作数栈,然后执行完毕。
IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, ConstantPool* pool, int index))

  // 确保类已经加载并且解析
  // 如果没有加载和解析,那么就去加载和解析类,得到最终的Klass对象
  Klass* k_oop = pool->klass_at(index, CHECK);
  instanceKlassHandle klass (THREAD, k_oop);

  // 确保不是实例化的一个抽象类
  klass->check_valid_for_instantiation(true, CHECK);

  // 确保类已经完成初始化工作
  klass->initialize(CHECK);

  // 在Java堆内存中开辟对象
  oop obj = klass->allocate_instance(CHECK);

  // 使用线程变量完成 值的传递
  thread->set_vm_result(obj);
IRT_END

在Hotspot中使用二分模型Klass / oop 来表示类和对象,而类是对象的模板。在上文的案例中,在main方法中 new A对象,所以需要确保类A已经被加载、解析完毕,生成好对应的Klass。而生成好Klass类对象后,需要对类做初始化过程,也即链接类中所有的方法和调用父类以及本身的<clinit>方法(也即A类的static块)。本文重点赘述对象的创建过程,类的创建过程详细请参考:JVM规范手册第4章、第5章内容 以及Hotspot虚拟机的实现。

当类加载、解析、初始化完毕后,会调用klass->allocate_instance 方法尝试在Java堆内存中开辟对象。

instanceOop InstanceKlass::allocate_instance(TRAPS) {

  // 因为类是对象的模板,所以可以从类中得到一个对象的大小
  int size = size_helper();  
  KlassHandle h_k(THREAD, this);
  instanceOop i;

  // 尝试在Java堆中开辟对象
  i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
  return i;
}

oop CollectedHeap::obj_allocate(KlassHandle klass, int size, TRAPS) {
  // 尝试在Java堆中开辟对象
  HeapWord* obj = common_mem_allocate_init(klass, size, CHECK_NULL);
  // 开辟好Java对象后,做初始化工作,比如:设置对象头、设置Klass指针。
  post_allocation_setup_obj(klass, obj, size);
  return (oop)obj;
}

HeapWord* CollectedHeap::common_mem_allocate_init(KlassHandle klass, size_t size, TRAPS) {
  // 尝试在Java堆中开辟对象
  HeapWord* obj = common_mem_allocate_noinit(klass, size, CHECK_NULL);
  // 内存清零
  init_obj(obj, size);
  return obj;
}


HeapWord* CollectedHeap::common_mem_allocate_noinit(KlassHandle klass, size_t size, TRAPS) {

  HeapWord* result = NULL;

  bool gc_overhead_limit_was_exceeded = false;
  result = Universe::heap()->mem_allocate(size,
                                          &gc_overhead_limit_was_exceeded);
  return result;

  ………… 
  省略JVMTI的模块处理
}


HeapWord* GenCollectedHeap::mem_allocate(size_t size,
                                         bool* gc_overhead_limit_was_exceeded) {
  return collector_policy()->mem_allocate_work(size,
                                               false /* is_tlab */,
                                               gc_overhead_limit_was_exceeded);
}


HeapWord* GenCollectorPolicy::mem_allocate_work(size_t size,
                                        bool is_tlab,
                                        bool* gc_overhead_limit_was_exceeded) {
  GenCollectedHeap *gch = GenCollectedHeap::heap();

  HeapWord* result = NULL;

  // 循环创建对象,因为可能一次创建不成功。
  for (int try_count = 1, gclocker_stalled_count = 0; /* return or throw */; try_count += 1) {
    HandleMark hm; // discard any handles allocated in each iteration

    // First allocation attempt is lock-free.
    Generation *gen0 = gch->get_gen(0);

    // 是否能够直接在gen0代(年轻代)开辟对象。
    // 需要满足条件:
    // 1、如果创建的对象大于年轻代的大小阈值,会直接去其他代创建
    // 2、如果eden空间不足够开辟当前对象的话会去其他代创建
    if (gen0->should_allocate(size, is_tlab)) {
      // 尝试在eden开辟对象
      // 如果因为eden空间不够了,会尝试去其他代创建此对象
      result = gen0->par_allocate(size, is_tlab);
      if (result != NULL) {
        assert(gch->is_in_reserved(result), "result not in heap");
        return result;
      }
    }
    
    /* 代表在年轻代开辟对象失败了,后续要根据策略选择其他代开辟此对象,必要时发生GC */

    unsigned int gc_count_before;  // read inside the Heap_lock locked region
    {
      MutexLocker ml(Heap_lock);

      // 是否需要在其他代开辟此对象(大对象直接在老年代开辟(防止在年轻代一直复制浪费性能))
      bool first_only = ! should_try_older_generation_allocation(size);

      /*
        得出2点
        1、如果创建的对象大于年轻代的大小阈值,会直接去其他代创建
        2、如果eden空间不足够开辟当前对象的话会去其他代创建
      */
      // 尝试在其他代开辟对象。
      result = gch->attempt_allocation(size, is_tlab, first_only);
      if (result != NULL) {
        return result;
      }

      // 记录一下发生GC前的次数
      gc_count_before = Universe::heap()->total_collections();
    }

    // 因为在年轻代和老年代创建对象都失败了,所以需要GC回收一下内存了。
    // 然后再尝试去开辟对象。
    VM_GenCollectForAllocation op(size, is_tlab, gc_count_before);
    VMThread::execute(&op);
    
  }
}

调用栈比较深,最终会在Java堆中创建对象,正常情况下满足:对象在年轻代的Eden空间创建,如果对象大于年轻代的创建大小阈值(因为年轻代使用复制算法,太大的对象一直拷贝影响性能),如果Eden的空间不足够创建此对象,此时就会去老年代创建此对象,如果老年代也开辟不了对象,此时就会发生GC,发生GC后再去尝试开辟对象。

由于调用栈特别深,考虑到篇幅,所以这里直接给出Eden创建对象的代码。src/share/vm/memory/space.cpp 文件中 par_allocate_impl方法

inline HeapWord* ContiguousSpace::par_allocate_impl(size_t size,
                                                    HeapWord* const end_value) {
  do {
    HeapWord* obj = top();
    // 是否还有空间容纳当前对象,如果没有空间了就直接返回null,交给其他代去创建。
    if (pointer_delta(end_value, obj) >= size) {
      HeapWord* new_top = obj + size;
      // 因为存在并发,所以使用平台原子性指令
      HeapWord* result = (HeapWord*)Atomic::cmpxchg_ptr(new_top, top_addr(), obj);

      // 根据CAS的规范,只有result == obj才代表成功
      // 其他情况下,就是发生并发,导致CAS失败,所以进入下一轮循环。
      if (result == obj) {
        assert(is_aligned(obj) && is_aligned(new_top), "checking alignment");
        return obj;
      }
    } else {
      return NULL;
    }
  } while (true);
}

代码非常的简单,是不是跟解释器解释执行new字节码指令的快速创建对象的逻辑一摸一样呢?

到这,已经分析完 new 字节码指令,但是从上文的案例对应的字节码来说,是不是还有dup和invokespecial指令。

这两个指令就非常的简单了,我们知道new 指令会把对象放在操作数栈中,dup指令就是复制一份放在操作数栈中,此时操作数栈就存在2份创建的对象了。而invokespecial指令会消耗操作数栈中一份对象,并且执行对象的<init>方法,也即大家口中的构造方法。此时就完成了整个对象的创建文章来源地址https://www.toymoban.com/news/detail-532254.html

到了这里,关于JVM源码剖析之Java对象创建过程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • JVM中对象创建过程

    在JVM中对象的创建,我们从一个new指令开始: 这个过程大概图示如下: 虚拟机收到new指令触发。 类加载检查:如果类没有被类加载器加载,则执行类加载流程(将class信息加载到JVM的运行时数据区的过程),对象所需内存大小在类加载完后可以完全确定。 对象分配内存:从

    2024年03月22日
    浏览(48)
  • JVM运行时区域——对象创建内存分配过程

            新创建的对象 , 都存放在伊甸园区域 ,当垃圾回收时,将伊甸园区域的垃圾数据销毁,然后将存活的对象转移到幸存者0区域,之后创建的新的对象还是存放在伊甸园区域,等到再次垃圾回收后,将伊甸园区域和幸存者0区域中存活的对象一起转移到幸存者1区域中

    2024年02月15日
    浏览(47)
  • Linux源码解读系列是一套深入剖析Linux内核源码的教程,旨在帮助读者理解Linux操作系统的底层原理和工作机制

    Linux源码解读系列是一套深入剖析Linux内核源码的教程,旨在帮助读者理解Linux操作系统的底层原理和工作机制。该系列教程从Linux内核的各个模块入手,逐一分析其源码实现,并结合实际应用场景进行讲解。通过学习本系列,读者可以深入了解Linux操作系统的底层机制,掌握

    2024年01月21日
    浏览(50)
  • Java进阶(4)——结合类加载JVM的过程理解创建对象的几种方式:new,反射Class,克隆clone(拷贝),序列化反序列化

    1.类什么时候被加载到JVM中,new,Class.forName: Class.forName(“包名.类名”); 2.创建对象的方式,反射,本质是获得类的类对象Class; 3.克隆clone,深拷贝,浅拷贝的对比; 4.序列化和反序列化的方式; Hello h; // 此时没有用Hello,jvm并没有进行类加载 看到new : new Book() Class.forName:

    2024年02月12日
    浏览(44)
  • JVM面试题-JVM对象的创建过程、内存分配、内存布局、访问定位等问题详解

    内存分配的两种方式 指针碰撞 适用场合:堆内存 规整 (即没有内存碎片)的情况下。 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。 使用该分配方式的GC收集器:

    2024年02月08日
    浏览(53)
  • 详细解读Java中Map集合的底层原理(干货+源码解读)

    本文将为大家详细讲解Java中的Map集合,这是我们进行开发时经常用到的知识点,也是大家在学习Java中很重要的一个知识点,更是我们在面试时有可能会问到的问题。 文章较长,干货满满,建议大家收藏慢慢学习。文末有本文重点总结,主页有全系列文章分享。技术类问题,

    2024年02月06日
    浏览(78)
  • 深入JVM了解Java对象实例化过程

    new :最常见的方式、 Xxx 的静态方法, XxxBuilder/XxxFactory 的静态方法 Class的newInstance 方法:反射的方式,只能调用空参的构造器,权限必须是public Constructor的newInstance(XXX) :反射的方式,可以调用空参、带参的构造器,权限没有要求 使用 clone() :不调用任何的构造器,要求当

    2023年04月24日
    浏览(39)
  • Spring Boot源码解读与原理剖析:深入探索Java开发的奥秘!

    关注+点赞+评论,评论区回复“Spring Boot源码解读与原理剖析:深入探索Java开发的奥秘!” 每篇最多 评论3条 !!采用抽奖助手自动拉取评论区有效评论送书两本, 开奖时间:9月11号 承载着作者的厚望,掘金爆火小册同名读物 《Spring Boot源码解读与原理剖析》 正式出书!

    2024年02月10日
    浏览(52)
  • 【JVM】详细解析java创建对象的具体流程

    目录 一、java创建对象的几种方式 1.1、使用new  1.2、反射创建对象 1.2.1、Class.newInstance创建对象 1.2.2、调用构造器再去创建对象Constructor.newInstance  1.3、clone实现 1.4、反序列化 二、创建对象的过程 2.1、分配空间的方式 1、指针碰撞 2、空闲列表 3、怎么选择分配方式 三、

    2024年02月15日
    浏览(56)
  • 深入剖析 Git 对象底层原理

    在我们日常使用 Git 时,通常的操作是: 在写完一段代码后,执行 git add 命令,将这段代码添加到暂存区中 然后再执行 git commit 和 git push 命令,将 本地 Git 版本库中的提交同步到服务器中的版本库中 Git 在中间做了什么,它如何存储不同的文件和内容,以及如何区分不同分支

    2024年01月20日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包