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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Eclipse显示层级目录结构(像IDEA一样)

有的小伙伴使用IDEA习惯了&#xff0c;可能进入公司里面要求使用eclipse&#xff0c;但是eclipse默认目录是并列显示&#xff0c;而不是层级显示。部分人用起来感觉十分不方便。我们可以更改一下设置。 1、打开eclipse&#xff0c;找到这里 2、选择PackagePresentation 3、选…

Github-提交PR指南

1. Fork你将要提交PR的repo 2. 将你fork下来的repo克隆到你的本地 git clone your_repo.git Cloning into ultralytics... remote: Enumerating objects: 8834, done. remote: Counting objects: 100% (177/177), done. remote: Compressing objects: 100% (112/112), done. …

第二步:STM32F407ZGT6资源介绍

1.1 STM32F407ZGT6资源描述 内核&#xff1a; 32位 高性能ARM Cortex-M4处理器 时钟&#xff1a;高达168M,实际还可以超屏一点点 支持FPU&#xff08;浮点运算&#xff09;和DSP指令 IO口&#xff1a; STM32F407ZGT6: 144引脚 114个IO 大部分IO口都耐5V(模拟通道除外) …

C# .NET 如何调用 SAP RFC 接口

1.分析传参结构 SAP 传参格式对应 .NET 参数格式 SAP 参数.NET 参数参数类型import(导入)——关联类型为数据元素Param单个变量参数import(导出)——关联类型为结构体Struct结构体tableTable表 下面是 SAP 对应参数类型&#xff1a; 2.web.config 配置 配置文件需要客户端…

win10安装pytorch GPU

我记得以前安装过深度学习库GPU版本&#xff0c; 需要安装cuda什么的&#xff0c;翻了下还真写过一篇win10安装tensorflow的文章&#xff0c;但是流程不止不详细&#xff0c;还不清晰。这次就再记录一遍 这次安装的是pytorch&#xff0c;这么多年似乎pytorch要逐渐统一深度学习…

【算法与数据结构】232、LeetCode用栈实现队列

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;这道题要求我们用栈模拟队列&#xff08;工作上一定没人这么搞&#xff09;。程序当中&#xff0c;pus…

spring之BeanFactory

spring之BeanFactory DefaultListableBeanFactory示例代码类继承实现结构 BeanFactory是Bean工厂&#xff0c;所以很明显&#xff0c;BeanFactory会负责创建Bean&#xff0c;并且提供获取Bean的API。 DefaultListableBeanFactory 在Spring源码中&#xff0c;BeanFactory接口存…

自定义的车牌号键盘组件

<template><view class"keyboard-wrap" v-if"kbShow"><view class"head"><view class"done" tap"done"><text class"iconfont iconxiala-"></text>关闭</view></vi…

spring boot + Apache tika 实现文档内容解析

Apache tika是Apache开源的一个文档解析工具。Apache Tika可以解析和提取一千多种不同的文件类型(如PPT、XLS和PDF)的内容和格式&#xff0c;并且Apache Tika提供了多种使用方式&#xff0c;既可以使用图形化操作页面&#xff08;tika-app&#xff09;&#xff0c;又可以独立部…

Python实现微信发送文件实例

新建Python文件&#xff1a;wx_file.py&#xff0c;代码如下 # -*- coding: utf-8 -*- # Author : CxiuM # Time : 2023-07-06 10:12 # Name : wx_operation.py"""微信群发消息"""import os import time import subprocessimport requests …

【ElasticSearch】DSL查询语法

文章目录 1、DSL查询分类2、DSL基本语法3、全文检索查询4、精确查询5、地理查询6、复合查询--相关性打分算法7、复合查询之Function Score Query8、复合查询之BooleanQuery 1、DSL查询分类 Elasticsearch提供了基于JSON的DSL&#xff08;Domain Specific Language&#xff09;…

Java版本工程项目管理系统源码

Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下&#xff1a; 首页 工作台&#xff1a;待办工作、消息通知、预警信息&#xff0c;点击可进入相应的列表 项目进度图表&#xff1a;选择&#xff08;总体或单个&#xff09;项目显示…

Librosa库——语音识别,语音音色识别训练及应用

很多同学以为语音识别是非常难的&#xff0c;其实并不然&#xff0c;起初我也是这么认为&#xff0c;但后来发现语音识别是最简单的&#xff0c;因为同学们可能不知道Python有一个音频处理库Librosa&#xff0c;这个库非常的强大&#xff0c;可以进行音频处理、频谱表示、幅度转…

CSS(持续更新!~)

二&#xff1a; 进阶&#xff1a; 只打算起到装饰作用的图片就建议就背景图片 块级标签就是&#xff1a;独占一行的标签&#xff08;比如div&#xff09;并且可以加宽加高 行内元素&#xff1a;就是不会独占一行的标签&#xff08;比如a&#xff0c;span等等&#xff0c;不可以…

TensorFlow项目练手(二)——猫狗熊猫的分类任务

项目介绍 通过猫狗熊猫图片来对图片进行识别&#xff0c;分类出猫狗熊猫的概率&#xff0c;文章会分成两部分&#xff0c;从基础网络模型->利用卷积网络经典模型Vgg。 基础网络模型 基础的网络模型主要是用全连接层来分类&#xff0c;比较经典的方法&#xff0c;也是祖先…

MinGW编译OpenCV 过程记录

1.下载源码opencv-3.4.10.zip &#xff0c;可以在OpenCV官网下载Releases - OpenCV 解压缩如下: 2.下载Mingw64工具&#xff0c;需要支持posix 并设置系统环境目录&#xff0c;下载的文件名x86_64-8.1.0-release-posix-sjlj-rt_v6-rev0.7z (可以在网上找) 3.使用Cmake工具构建…

微信小程序个人中心展示样式(2)

这是之前的详细的看这里 因为这是好多年前写的了&#xff0c;好多人私信我代码有问题。正好今天有时间简单的还原下代码 话不多说先看图(图片样式自己搞奥~~~~我也好久没弄了这就是个参考demo) 以下是一个使用微信小程序开发的个人中心展示详情的示例&#xff1a; 在微信开发…

基于PyQt5的桌面图像调试仿真平台开发(10)色彩矩阵

系列文章目录 基于PyQt5的桌面图像调试仿真平台开发(1)环境搭建 基于PyQt5的桌面图像调试仿真平台开发(2)UI设计和控件绑定 基于PyQt5的桌面图像调试仿真平台开发(3)黑电平处理 基于PyQt5的桌面图像调试仿真平台开发(4)白平衡处理 基于PyQt5的桌面图像调试仿真平台开发(5)…

解决问题:通配符的匹配很全面, 但无法找到元素 ‘context:component-scan‘ 的声明~

异常描述如下&#xff1a; 产生异常原因&#xff1a; 因为在配置文件中没有找到<context:component-scan />元素的声明&#xff0c;解决办法&#xff1a;将XML配置文件中的声明改为下述代码&#xff1a; <beans xmlns"http://www.springframework.org/schema/b…

01 | 一条 SQL 查询语句是如何执行的?

以下内容出自 《MySQL 实战 45 讲》 一条 SQL 查询语句是如何执行的&#xff1f; 下面是 MySQL 的基本架构示意图&#xff0c;从中可以清楚地看到 SQL 语句在 MySQL 的各个功能模块中的执行过程。 大体来说&#xff0c;MySQL 可以分为 Server 层和存储引擎层两部分。 Server …