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

news2024/12/26 1:04:37

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

版本信息如下:

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

首先把总结图放在这。接下来分析源码~ 

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

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>方法,也即大家口中的构造方法。此时就完成了整个对象的创建

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/727642.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

搭建帮助中心5大注意事项

在现代互联网时代&#xff0c;为企业网站建立一个优雅实用的网站帮助中心变得尤为重要。一个好的网站帮助中心可以帮助企业解决客户的难点、痛点&#xff0c;提高客户满意度、期待值&#xff0c;从而更好地留住客户&#xff0c;增加收入。 如果没有帮助中心或者是帮助中心创建…

深入理解链表:一种动态的线性数据结构

文章目录 前言1. 概述2. 单向链表3. 单向链表&#xff08;带哨兵&#xff09;4. 双向链表&#xff08;带哨兵&#xff09;5. 环形链表&#xff08;带哨兵&#xff09;6. 结语 前言 链表是我们在日常编程中经常使用的一种数据结构&#xff0c;它相比于数组具有更好的动态性能。…

Spark(17):RDD、DataFrame、DataSet三者的关系

目录 0. 相关文章链接 1. 三者的产生 2. 三者的共性 3. 三者的区别 3.1. RDD 3.2. DataFrame 3.3. DataSet 4. 三者的互相转换 4.1. 互相转换图 4.2. DataFrame 和 DataSet 转换 0. 相关文章链接 Spark文章汇总 1. 三者的产生 在 SparkSQL 中 Spark 为我们提供了两…

Flutter TextField 输入框 简单使用

创建方式一&#xff1a; ///用于文本输入框 TextEditingController controller new TextEditingController();/// 设置TextField中显示的内容void setEditeInputTextFunction(String flagText) {controller .text flagText;}/// 清除TextField中显示的内容void clearEditeIn…

Web服务器群集:podman与docker技术集群

目录 一、理论 1.虚拟化 2.容器 3.podman 4.docker 5.podman与docker区别 二、实验 1.部署podman 2.部署docker 三、总结 一、理论 1.虚拟化 &#xff08;1&#xff09;概念 虚拟化&#xff1a;将应用程序和系统内核资源进行解耦&#xff0c;以操作系统级别进行隔离…

利用langchain-ChatGLM、langchain-TigerBot实现基于本地知识库的问答应用

目录 1 原理 2 langchain-ChatGLM的开发部署 2.1 安装环境 2.2 加载本地模型 3 langchain-TigerBot的开发部署 刷B站的时候&#xff0c;无意中看到吴恩达的一个langchain的教程&#xff0c;然后去github上搜了下&#xff0c;发现别人利用langchain和chatGLM做的基于本地知…

【C++11】lambda表达式 包装器

文章目录 1 lambda表达式1.1 引例1.2 lambda表达式的基本语法1.3 lambda表达式的底层原理 2 包装器3 bind 1 lambda表达式 1.1 引例 在C98中&#xff0c;如果想要对一个数据集合中的元素进行排序&#xff0c;可以使用std::sort方法&#xff1a; #include <algorithm> …

docker运行 mycli

1. 制作镜像 1.1 Dockerfile: FROM python:3.8 ENV MYSQL_HOST192.168.1.108 ENV MYSQL_PWDroot RUN apt-get update && apt-get install -y less RUN pip3 install mycli ENTRYPOINT ["mycli"] 注意 python:3.8 pip3 mycli 具有版本限制的,如果改了版…

【计算机网络】1.5——计算机网络的体系结构(网络分层模型)

计算机网络的体系结构 概述 计算机网络的体系结构是计算机网络及其构建所应完成功能的精确定义 考题 不属于网络体系结构所描述的内容的是 A、网络的层次 B、每层使用的协议 C、协议的内部实现细节 D、每层必须完成的功能 这些功能的「实现细节」&#xff0c;是遵守这种体系…

Web 前端 Day 1

课程大纲&#xff1a; html 结构 css 表现 Js 行为 jquery库 &#xff08;地位下降趋势 仍旧在用&#xff09; bootstrap 前端高端只是&#xff1a;angular angularjs html 超文本标记语言 相关解释 描述网页的语言 不仅有文字&#xff0c;还有图片、音频、视频等等 超…

某网站JS加密、OB混淆与CSS反爬实战分析

1. 写在前面 最近一段时间接触了一些小说网站的业务。发现很多的小说网站&#xff0c;甚至一些小站它们的安全防护措施做的都很到位&#xff01;例如上次说到的的五秒盾也是存在于一个小说小站。今天要讲的这个网站它集JS加密、ob混淆、CSS反爬于一体 目标站点&#xff1a; aH…

用真人模型制作3D虚拟人物,岂不是更真实?

3D虚拟人物是指利用计算机技术和图形学技术创建的一种能够模拟真实人体形态、行为和语言的虚拟实体。与传统的平面图像或视频不同&#xff0c;3D虚拟人物具有立体感和真实感&#xff0c;能够在虚拟环境中实现人机交互和情感交流&#xff0c;给用户带来全新的沉浸式体验。 随着…

文件批量改名新技巧:轻松将日期插入到文件名中,整理更有序!

在数字化时代&#xff0c;我们每天都面临着大量的文件&#xff0c;而合理整理和命名这些文件对于我们的工作和生活至关重要。特别是在需要存档或分享文件时&#xff0c;具有清晰的命名规则可以极大地提升工作效率和组织性。 首先&#xff0c;进入文件批量改名高手的文件批量重…

表征材料表面性质​的第一性原理计算方法:功函数

功函数计算是指通过计算材料表面或界面上的电子结构能量差来确定材料的界面特性的方法。在材料科学和表面科学领域&#xff0c;界面的性质对于材料的功能和性能具有重要影响。通过理解和控制界面&#xff0c;可以优化材料的电子传输、光学性能、催化活性等关键特性。 功函数表示…

网络协议【图解TCP/IP(笔记二)】

文章目录 网络协议随处可见的协议协议的必要性生活中的协议计算机中的协议分组交换协议协议的标准化 网络协议 随处可见的协议 在计算机网络与信息通信领域里&#xff0c;人们经常提及“协议”一词。互联网中常用的具有代表性的协议有IP、TCP、HTTP等。而LAN&#xff08;局域…

企业元宇宙虚拟场景开发公司广州华锐互动

元宇宙是一个虚拟的世界&#xff0c;可以模拟现实世界中的场景和交互方式。利用元宇宙搭建线上元宇宙互动展厅可以让企业在虚拟环境中展示产品、服务和品牌形象&#xff0c;与客户进行互动交流&#xff0c;提高企业的知名度和影响力。 通过搭建线上元宇宙互动展厅&#xff0c;利…

【python学习】matplotlib绘制仅有y轴的数值条码图/台阶图(隐藏特定坐标轴)

matplotlib绘制仅有y轴的数值条码图/台阶图 图形来源图形绘制思路绘图代码 图形来源 今天看文章&#xff0c;文章中有一幅图&#xff0c;图的表达形式很适合我目前的数据展示&#xff0c;于是想要用python画出来。 文章来源&#xff1a;Yun-Hua Cheng_2017_Scientific Report…

web学习笔记1

计算机&#xff1a; 课程大纲&#xff1a;html&#xff08;五条猫结构&#xff09; 结构 css 表现 JavaScript 行为 html&#xff1a;超文本标记语言 超&#xff1a;超链接&#xff0c;能从一个网页跳转到另一个网页 标记&#xff1a;文本要变成超文本&#xff0c;就需要各…

pwn05(应对简单栈溢出的常规套路)

目录 一、常规检查&#xff08;nc、file、checksec&#xff09; 二、IDA反编译&#xff0c;只找两个东西即可 1、寻找造成栈溢出的函数的地址到ebp的距离 2、 寻找我们所要利用的函数的地址&#xff08;即我们希望程序最后返回到哪里&#xff09; 三、编写并运行exp脚本 一…

eNSP-NAT网络地址转换服务

NAT网络地址转换服务 文章目录 NAT网络地址转换服务一、题目要求二、题目分析三、拓扑结构四、基础配置五、测试验证 一、题目要求 1.私网地址使用192.168.1.0/24进行子网划分 2.Telnet Server设备启动Telnet服务 3.isp设备仅配置IP地址 4.PC6,PC7,PC8均可以访问PC9 5.内网…