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;)Vflags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: new           #2                  // class A3: dup4: invokespecial #3                  // Method A."<init>":()V7: pop8: returnLineNumberTable:line 4: 0line 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.hqwc.cn/news/13551.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flutter开发微信小程序实战:构建一个简单的天气预报小程序

微信小程序是一种快速、高效的开发方式&#xff0c;Flutter则是一款强大的跨平台开发框架。结合二者&#xff0c;可以轻松地开发出功能丰富、用户体验良好的微信小程序。 这里将介绍如何使用Flutter开发一个简单的天气预报小程序&#xff0c;并提供相应的代码示例。 1. 准备工…

使用Docker安装RabbitMQ并实现入门案例“Hello World”

RabbitMQ官方文档&#xff1a;RabbitMQ Tutorials — RabbitMQ 一、RabbitMQ安装&#xff08;Linux下&#xff09; 你可以选择原始的方式安装配置&#xff0c;也可以使用docker进行安装&#xff0c;方便快捷&#xff01; 1. 安装docker 没有docker的先安装一下docker&#x…

OpenCV图像的仿射变换、旋转和缩放

以下是对代码的逐行解释: // 包含必要的OpenCV头文件和C++库文件 #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> using namespace cv;

密码学学习笔记(六):Hash Functions - 哈希函数2

哈希函数是怎么构成的&#xff1f; Merkle–Damgrd结构 哈希函数需要能够处理任意长度的输入。许多散列函数&#xff0c;例如MD5、SHA-1、SHA-2&#xff0c;都是由构建块&#xff08;称为压缩函数&#xff09;组成的&#xff0c;这些构建块可以处理特定的块大小&#xff0c;并…

Centos环境Access denied for user ‘root‘@‘%to database ‘xxx‘

Centos7解决数据库出现Access denied for user ‘root‘‘%to database ‘xxx‘ 问题 原因: root%表示 root用户 通过任意其他端访问操作 被拒绝! 授权即可: 1&#xff1a;进入数据库 mysql -u root -p 2.输入 mysql>grant all privileges on *.* to root% with grant o…

pdf转为ppt的超简单方法,就用这几个!

在我们的工作和生活中&#xff0c;PDF文件是不可或缺的文件格式之一。它以高准确性、整齐的页面排版和流畅的翻页而闻名&#xff0c;为我们处理文档提供了很大的帮助。然而&#xff0c;PDF文件的一个缺点是无法进行修改。当我们不小心输入错误数据或需要进行编辑时&#xff0c;…

SQL Server中如何将累积数值拆解

概要 本文通过一个计算汽车每日里程数的例子&#xff0c;展现如何通过汽车每日的总里程数&#xff0c;来计算汽车每日的里程数。 代码及实现 每辆汽车中都有一个里程数表&#xff0c;记录汽车从出场到当前行驶的里程数&#xff0c;下表是一样汽车的里程数表&#xff0c;该表…

Jetpack compose——深入了解Diffing

Diffing是什么 "Diffing" 是 Jetpack Compose 中用于优化性能的一种技术。它的工作原理是比较新旧 UI 树&#xff0c;并只更新实际发生变化的部分。这意味着即使你的应用有大量的 UI&#xff0c;Compose 也能保持高效的性能。 当 Composable 函数被重新调用&#x…

MybatisX插件自动生成sql失效问题的详细分析

mybatis框架提供了非常好用的逆向工程插件&#xff0c;但是根据数据库驱动版本的不同会出现一些问题。 在使用mybatisX插件的时候使用Generate mybatis sql无法实现自动生成sql 解决方案&#xff1a; 1.首先检查自己的数据库中表是否有主键&#xff0c;如果没有主键是不会生…

Jetpack Compose与Accompanist:改变Android UI开发的方式

在Android开发中&#xff0c;UI开发一直是一个重要的部分。Google推出的Jetpack Compose库为开发者提供了一种全新的声明式UI工具&#xff0c;使得UI开发变得更加简单和直观。而Accompanist库则为Jetpack Compose提供了一系列有用的扩展&#xff0c;进一步提升了开发效率。 Jet…

考研的尽头是考公?

2022年12月23日&#xff0c;作为中国诞生于互联网的职业考试培训行业市场领导者的粉笔有限公司&#xff08;“粉笔”或“公司”&#xff09; &#xff0c;早前通过港交所上次聆讯后开始招股。 据悉&#xff0c;粉笔计划发售20&#xff0c;000&#xff0c;000股股份&#xff08;…

English Learning - L3 综合练习 10 口语语法串讲与思维回顾 2023.07.5 周三

English Learning - L3 综合练习 10 口语语法串讲与思维回顾 2023.07.5 周三 [知识点 1] 名词性从句问题&#xff1a;到底什么是名词笥从句&#xff1f;例 1&#xff1a;我的东西你都可以随便用例 2&#xff1a;不管是谁&#xff0c;放你鸽子就是混蛋例 3&#xff1a;说那种话的…