当前位置: 首页 > 科技观察

JVM从入门到放弃Java对象创建过程

时间:2023-03-15 08:49:52 科技观察

架构对象创建Java是一种面向对象的编程语言,通常只通过new关键字来创建对象。对象创建过程当虚拟机遇到一条字节码new指令时,它首先检查这条指令的参数是否可以在常量池中定位到一个类的符号引用。并检查这个符号引用所代表的类是否被虚拟机类加载器加载。如果没有,则必须先执行类加载过程。类检查通过后,虚拟机再为新生成的对象分配内存。对象所需的内存大小是在加载类时确定的。(对象内存分配后会有单独的简短解释)。内存分配完成后,虚拟机将分配的内存空间(不包括对象头)初始化为零,即清理并初始化这块内存空间。接下来,虚拟机还需要初始化对象,比如元数据(对象是那个类的实例)、对象的哈希码、对象的GC分代年龄、偏向锁状态等信息。这些信息被用来存储在对象头(ObjectHeader)中。完成以上过程后,其实虚拟机中内存的创建就已经完成了,只不过我们从Java中执行new创建对象的角度来说才刚刚开始。我们还需要调用构造函数来初始化对象(可能还需要在this前后调用父类的构造函数,初始化块等)。执行Java对象的初始化。即在.class的角度来看就是调用()方法。如果构造函数中调用了其他方法,那么其他方法也会被执行。当构造函数中的所有关联方法都执行完毕后,Java对象的创建才真正完成。对象的整体创建过程如下:对象内存分配对象内存分配过程如下图所示:为对象分配空间的任务本质上是从内存中为Java对象分配一定大小的内存块Jvm的领域。(默认是在堆上分配)。指针碰撞假设Java堆中的内存是绝对有规律的,所有使用过的内存都放在一边,未使用的内存放在另一边。中间放置了一个指针,指示它们的分界点。分配的内存只是将指针向空闲方向移动与Java对象大小相等的距离。这种分配方法称为“DumpThePointer”。freelist但是如果Java堆中的内存是不规律的,已用内存块和空闲内存块相互交错,那么简单的指针碰撞是没办法的,虚拟机必须维护一个可用内存列表领域。记录哪些内存块可供使用。分配对象内存时,从链表中找到足够大的内存空间分配给实例对象,并更新链表上的记录。这种分配方式称为“空闲列表”(FreeList)。选择内存分配方式时什么时候用指针冲突,什么时候用freelist?选择哪种分配方式是由Java堆是否规则决定的,而Java堆是否规则是由所使用的垃圾收集器是否具有空间整理(Compact)能力决定的。在使用带有指针压缩排序过程的Serial、ParNew等收集器时,系统采用的分配算法是指针碰撞,简单高效。使用基于Sweep算法的CMS收集器时,理论上只能使用复杂的freelist来分配内存。并发内存分配方案在频繁分配对象的过程中,即使只是修改了指针指向的位置,在并发的情况下也不是线程安全的。有可能内存正在分配给对象A,指针还没来得及修改。对象B同时使用原来的指针进行内部分配。解决这个问题有两种方案:一种是同步内存分配空间的动作——实际上,虚拟机使用CAS+失败重试来保证更新操作的原子性。另一种是根据线程将内存分配动作划分到不同的空间,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(ThredLocalAllocationBuffer,TLAB),那如果一个线程要分配内存,就在那个线程中分配内存,在那个线程的本地缓冲区中分配。只有当本地缓冲区用完时,分配新缓冲区时才需要同步锁。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数设置。对象的内存布局在HotSpot虚拟机中,对象在内存中的布局可以分为三个区域:对象头(Header)、实例数据(InstanceData)和对齐填充(Padding)。下图是普通对象实例和数组对象实例的数据结构:对象头结构MarkWord(64bit)结合openjdk源码markOop.hpp我们可以看到:两个指针变量说明:ptr_to_lock_record:在轻量级锁中state,指向栈上锁记录的指针。当锁定获取是无竞争的时,JVM使用原子操作而不是OS互斥体,一种称为轻量级锁定的技术。在轻量级锁的情况下,JVM通过CAS操作在对象的MarkWord中设置一个指向锁记录的指针。ptr_to_heavyweight_monitor:在重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,轻量级锁必须升级为一个监视器来管理等待线程。在重量级锁的情况下,JVM在对象的ptr_to_heavyweight_monitor中设置一个指向Monitor的指针。markOop.hpp中我们可以看到文件的注释如下://部分省略//32bits://--------//hash:25------------>|age:4biased_lock:1lock:2(normalobject)//JavaThread*:23epoch:2age:4biased_lock:1lock:2(biasedobject)//size:32---------------------------------------->|(CMS空闲块)//PromotedObject*:29---------->|promo_bits:3----->|(CMSpromotedobject)////64bits://--------//unused:25hash:31-->|unused:1age:4biased_lock:1lock:2(normalobject)//JavaThread*:54epoch:2unused:1age:4biased_lock:1lock:2(biasedobject)//PromotedObject*:61--------------------->|promo_bits:3----->|(CMS提升的对象)//大小:64---------------------------------------------------->|(CMS空闲块)////未使用:25哈希:31-->|cms_free:1age:4biased_lock:1lock:2(COOPs&&normalobject)//JavaThread*:54epoch:2cms_free:1age:4biased_lock:1lock:2(COOPs&&biasedobject)//narrowOop:32unused:24cms_free:1unused:4promo_bits:3----->|(COOPs&&CMSpromotedobject)//unused:21size:35-->|cms_free:1未使用:7------------------>|(COOPs&&CMSfreeblock)//部分省略klassklass对应Java的CLass类,在jvm中会生成一个对象kclass实例对象的元数据信息存储在Java类对象中。在jdk1.8中,这块存放在元空间中,klass类型指针存放在对象头中,即对象指向其类元数据的指针。虚拟机使用这个指针来判断对象是哪个类的实例。数组长度(仅适用于数组对象)。如果对象是数组,则对象头中必须有一段数据记录数组的长度。实例数据实例数据部分是对象实际存储的有效信息,也是程序代码中定义的各类字段和方法的内容。无论是从父类继承还是在子类中定义,这里一一记录。Alignmentpadding第三部分alignmentpadding不一定存在,也没有什么特殊意义,只是起到占位符的作用。因为HotSpotVM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,换句话说,对象的大小必须是8字节的整数倍。对象头部分恰好是8字节的倍数(1或2倍)。因此,当对象实例数据部分没有对齐时,需要通过对齐填充来完成。对象大小在32位系统下计算,存放Class指针的空间为4字节,MarkWord为4字节,对象头为8字节。64位系统下,存放Class指针的空间为8字节,MarkWord为8字节,对象头为16字节。64位启用指针压缩时,Class指针的存储空间为4字节,MarkWord为8字节,对象头为12字节。数组长度4字节+数组对象头8字节(对象引用4字节(不启用64位指针压缩为8字节)+数组标记为4字节(不启用64位指针压缩为8字节))+对齐4=16字节。静态属性不计入对象大小。打印对象状态JOL(JavaObjectLayout)是一个用于分析JVM中对象布局的开源工具。使用Unsafe、JVMTI和可服务性代理(SA)解码实际对象布局、足迹和引用。这使得JOL比其他依赖堆转储、规范假设等的工具更准确。maven仓库依赖于以下内容:org.openjdk.joljol-core0.161.检查对象的内部信息,包括:字段布局、标题信息、字段值、对象内的对齐/填充。ClassLayout.parseInstance(obj).toPrintable()2.查看对象的外部信息:包括引用:GraphLayout.parseInstance(obj).toPrintable()3.查看对象占用的内存空间大小:GraphLayout.parseInstance(obj).totalSize()16完整代码:importorg.openjdk.jol.info.ClassLayout;导入org.openjdk.jol.info.GraphLayout;publicclassObjectTest2{publicstaticvoidmain(String[]args){Objectobj=newObject();System.out.println(ClassLayout.parseInstance(obj).toPrintable());System.out.println();System.out.println();System.out.println(GraphLayout.parseInstance(obj).toPrintable());System.out.println();System.out.println();System.out.println(GraphLayout.parseInstance(obj).totalSize());}}对象访问定位句柄访问使用句柄访问方法,Java可能会在堆中分配一块内存作为句柄池。引用存储对象的句柄地址,句柄中包含对象实例数据和类型数据的具体地址信息。其结构如图:直接访问对于直接指针访问,对象在Java堆中的内存布局必须考虑如何放置访问类型数据的相关信息。引用中存储的地址直接是对象地址。如果只访问对象本身,就不需要间接访问的额外开销,如下图所示:与两种对象访问方式相比,对象访问方式有其自身的优势。使用句柄访问的最大好处是引用存储了稳定的句柄地址。当对象被移动时(垃圾回收时移动对象是很常见的行为),只会改变句柄中的实例数据指针,而引用本身不需要修改。使用直接指针访问最大的好处就是速度更快,省去了一次指针定位的时间开销,因为在Java中对象访问是非常频繁的,所以这种开销也是一笔极其可观的执行成本。就本书讨论的主要虚拟机HotSpot而言,主要是使用第二种方式通过对象访问对象(例外情况,如果使用Shenandoah收集器,会有额外的转发),但是从整个软件开发,在各种语言和框架中使用句柄访问也是很常见的。常见的。参考《深入理解 JVM 虚拟机 第三版》周志明https://www.cnblogs.com/jhxxb/p/10983788.htmlhttps://www.cnblogs.com/maxigang/p/9040088.htmlhttps://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdfhttps://github.com/openjdk/jol