首页 » PHP教程 » php对象可以赋值技巧_技能探秘深度解析对象在 JVM 中的创建过程

php对象可以赋值技巧_技能探秘深度解析对象在 JVM 中的创建过程

访客 2024-12-08 0

扫一扫用手机浏览

文章目录 [+]

工具的创建过程,可以用如下图来简要概括。

创建工具大致分为 5 个步骤:

php对象可以赋值技巧_技能探秘深度解析对象在 JVM 中的创建过程

1.检讨类是否加载,如果没有就先实行类的加载2.分配内存3.初始化零值4.设置头工具5.实行初始化方法,例如布局方法等

下面我们一起来看下每个步骤详细的事情内容。

php对象可以赋值技巧_技能探秘深度解析对象在 JVM 中的创建过程
(图片来自网络侵删)
2.1、类加载检讨

当须要创建一个类的实例工具时,比如通过new xxx()办法,虚拟机首先会去检讨这个类是否在常量池中能定位到一个类的符号引用,并且检讨这个符号引用代表的类是否已经被加载、解析和初始化,如果没有,那么必须先实行类的加载流程;如果已经加载过了,会在堆区有一个类的 class 工具,方法区会有类的干系元数据信息。

为什么在工具创建时,须要有这一个检讨判断?

紧张缘故原由在于:类的加载,常日都是

关于类的加载过程,在之前的文章中已经有所先容,有兴趣的朋友可以翻看之前的文章。

2.2、分配内存

类加载成功后,虚拟机就能够确定工具的大小了,此时虚拟机会在堆内存中划分一块工具大小的内存空间出来,分配给新生工具。

虚拟机如何在堆等分配内存的呢?

紧张有两种办法:

1.指针碰撞法2.空闲列表法

下面我们一起来看看干系的内存分配办法。

2.2.1、指针碰撞法

如果内存是规整的,那么虚拟机将采取指针碰撞法来为工具分配内存。

指针碰撞法,大略的说便是所有用过的内存在一边,空闲的内存在另一边,中间放着一个指针作为分界点的指示器,分配内存时会把指针向空闲一方挪动一段,直到能容纳工具大小的位置。

如果垃圾网络器选择的是 Serial、ParNew 这种基于压缩算法的,虚拟机会采取这种分配办法。

2.2.2、空闲列表法

如果内存不是规整的,已利用的内存和未利用的内存相互交错,那么虚拟机将采取空闲列表法来为工具分配内存。

空闲列表法,大略的说便是在虚拟机内部掩护了一个列表,会记录哪些内存块是可用的,在分配的时候会从列表中找到一块能容纳工具大小的空间,划分给工具实例,并更新列表上的内容。

如果垃圾网络器选择的是 CMS 这种基于标记-打消算法的,虚拟机会采取这种分配办法。

2.2.3、内存分配安全问题

我们知道,虚拟机是支持多个线程同时分配内存的,是否会有线程安全的问题呢?

答案是:肯定存在的。
比如用指针碰撞法时,虚拟机正在给工具 A 分配内存,但指针还没来及修正,此时又有一个线程给工具 B 分配内存,同时利用了原来的指针来分配,末了的结果便是这个区域只分配来一个工具,另一个工具被覆盖了。

针对内存分配时存在的线程安全问题,虚拟机采取了两种办法来进行处理:

CAS+重试机制:通过 CAS 操作移动指针,只有一个线程可以移动成功,移动失落败的线程重试,直到成功为止TLAB (thread local Allocation buffer):也称为本地线程分配缓冲,这个处理办法思想很大略,便是当线程开启时,虚拟机会为每个线程分配一块较大的空间,然后线程内部创建工具的时候,就从自己的空间分配,这样就不会有并发问题了,当线程自己的空间用完了才会从堆等分配内存,之后会转为通过 CAS+重试机制来办理并发问题

以上便是虚拟机办理工具内存分配时存在的线程安全问题的方法。

2.3、初始化零值

初始化零值,顾名思义,便是对分配的这一块内存初始化零值,也便是给实例工具的成员变量赋于零值,比如 int 类型赋值为 0,引用类型为null等操作。
这样工具就可以在没有赋值情形下利用了,只不过访问工具的成员变量都是零值。

2.4、设置头工具

初始化零值完成之后,虚拟机就会对工具进行必要的设置,比如这个工具是哪个类的实例、如何才能找到类的元数据信息、工具的哈希码、工具的 GC 分代年事等信息,这些信息都会存放在工具头中。
这部分数据,官方称它为“Mark Word”。

在 HotSpot 虚拟机中,工具在内存中存储的布局可以分为 3 块区域:工具头 (Header)、 实例数据 (Instance Data) 和对齐添补位 (Padding)。

以 32 位的虚拟机为例,工具的组成可以用如下图来简要概括。
(64位的虚拟机略有不同)

各部分区域功能如下:

工具头:分为 Mark Word 和元数据区,如果是数组工具,还有记录数组长度的区域。
这三块保存着工具的 hashCode 值,锁的状态,类元数据指针,工具的分代年事等等信息。
实例数据:顾名思义,用于保存工具成员变量的值,如果变量是引用类型,保存的是内存地址对齐添补位:由于 HotSpot 虚拟机哀求工具的起止地址必须是 8 字节的整数倍,也便是哀求工具的大小为 8 字节的整数倍,如果不敷 8 字节的整数倍,那么就须要通过对齐添补进行占位,补够 8 字节的整数倍。

我们重点来看下 Mark Word 的组成,不同的操作系统环境下占用的空间不同,在 32 位操作系统中占 4 个字节,在 64 位中占 8 个字节。

以 32 位操作系统为例,Mark Word 内部构造如下:

各部分的含义如下:

identity_hashcode:25 位的工具标识哈希码。
采取延迟加载技能,调用System.identityHashCode()方法获取,并会将结果写到该工具头中。
当工具被锁定时,该值会移动到管程 Monitor 中。
age:4 位的 Java 工具年事。
在GC中,如果工具在 Survivor 区复制一次,年事增加 1,当工具达到设定的阈值时,将会晋升到老年代。
默认情形下,并行 GC 的年事阈值为 15,并发 GC 的年事阈值为 6。
由于 age 只有4位,以是最大值为15,这便是为什么-XX:MaxTenuringThreshold选项最大值为 15 的缘故原由。
lock:2 位的锁状态标记位。
工具的加锁状态分为无锁、倾向锁、轻量级锁、重量级锁等几种标记,不同的标记值,表示的含义也不同。
biased_lock:工具是否启用倾向锁标记,只占 1 个二进制位。
为 1 时表示工具启用倾向锁,为 0 时表示工具没有倾向锁。
倾向锁是一种锁的优化手段,开启倾向锁,某些时候可以省去线程频繁申请锁的操作,提升程序实行性能。
thread:持有倾向锁的线程 ID,如果该线程再次访问这个锁的代码块,可以直接访问epoch:倾向锁在 CAS 锁操作过程中的标识ptr_to_lock_record:在轻量级锁时,指向栈中锁记录的指针ptr_to_heavyweight_monitor:在重量级锁时,指向管程 Monitor 的指针

个中lock参数中不同的标记值,表示的含义如下。

lock标记位,常日会在利用到synchronized关键字的工具上发挥浸染。
随着线程之间竞争激烈程度,工具锁会从无锁状态逐渐升级到重量级锁,个中的变革过程,可以用如下步骤来概括。

1.初期锁工具刚创建时,还没有任何线程来竞争,锁状态为 01,倾向锁标识位是0(无线程竞争它),此时程序实行效率最高。
2.当有一个线程来竞争锁时,先用倾向锁,表示锁工具偏爱这个线程,这个线程要实行这个锁关联的任何代码,不须要再做任何检讨和切换,这种竞争不激烈的情形下,效率也非常高。
3.当有两个线程开始竞争这个锁工具时,情形会发生变革,锁会升级为轻量级锁,两个线程公正竞争,哪个线程先霸占锁工具并实行代码,锁工具的 Mark Word 就实行哪个线程的栈帧中的锁记录。
轻量级锁在加锁过程中,用到了自旋锁。
所谓自旋,便是指当有其余一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给壅塞,直到那个得到锁的线程开释锁之后,这个线程就可以立时得到锁,实行效率有所衰减。
4.如果竞争这个锁工具的线程越来越多,会导致更多的切换和等待,JVM 会把该工具的锁升级为重量级锁。
这个便是大家常说的同步锁,此时工具中的 Mark Word 会再次发生变革,会指向一个监视器 (Monitor) 工具,这个监视器工具用凑集的形式来登记和管理排队的线程。
Monitor 依赖操作系统的 MutexLock(互斥锁)来实现线程排队,线程被壅塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换线程,比较其它级别的锁,此时锁的性能最差。

关于synchronized关键字事理剖析,我们会在后续的文章中再次先容。

2.5、实行init方法

实行 init 方法是工具创建的末了一步,虚拟机会给工具的成员变量设置用户指定的初始值,并且会实行布局方法等。

2.6、小结

以上便是工具的创建过程,末了我们通过工具来看下工具创建后的大小。

可以添加第三方jol包,利用它来打印工具的内存布局情形。

<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version></dependency>

编写一个测试类。

public class ObjectHeaderTest { public static void main(String[] args) { System.out.println("=========打印Object工具的大小========"); ClassLayout layout = ClassLayout.parseInstance(new Object()); System.out.println(layout.toPrintable()); System.out.println("========打印数组工具的大小========="); ClassLayout layout1 = ClassLayout.parseInstance(new int[]{}); System.out.println(layout1.toPrintable()); System.out.println("========打印有成员变量的工具大小========="); ClassLayout layout2 = ClassLayout.parseInstance(new ArtisanTest()); System.out.println(layout2.toPrintable()); } / ‐XX:+UseCompressedOops 表示开启压缩普通工具指针 ‐XX:+UseCompressedClassPointers 表示开启压缩类指针 / public static class ArtisanTest { int id; //4B String name; //4B byte b; //1B Object o; //4B }}

输出结果:

=========打印Object工具的大小========java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total========打印数组工具的大小=========[I object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363) 12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 16 0 int [I.<elements> N/AInstance size: 16 bytesSpace losses: 0 bytes internal + 0 bytes external = 0 bytes total========打印有成员变量的工具大小=========com.example.thread.o4.ObjectHeaderTest$ArtisanTest object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 12 f2 00 f8 (00010010 11110010 00000000 11111000) (-134155758) 12 4 int ArtisanTest.id 0 16 1 byte ArtisanTest.b 0 17 3 (alignment/padding gap) 20 4 java.lang.String ArtisanTest.name null 24 4 java.lang.Object ArtisanTest.o null 28 4 (loss due to the next object alignment)Instance size: 32 bytesSpace losses: 3 bytes internal + 4 bytes external = 7 bytes total03、工具的访问办法

工具创建完之后,剩下的事情便是利用工具。
Java 程序紧张通过虚拟机栈上的 reference (引用) 数据来操作堆上的详细工具。

工具的访问办法有虚拟机实现而定,不同的虚拟机实现访问办法不同,目前主流的访问办法有:

句柄访问直接指针访问3.1、句柄访问

句柄访问,虚拟机会在 Java 堆中划分出一块内存来作为句柄池,reference 中存储的便是工具的句柄地址,句柄中则包含了类数据的地址和实例数据的地址信息。

利用句柄办法最大的好处便是,reference 中存储的是稳定的句柄地址,在工具被移动时(垃圾网络时移动工具是非常普遍的行为),只会改变句柄中的实例数据指针,而 reference 不须要被修正。

3.2、直接指针访问

直接指针访问,reference 中直接存储的便是工具地址,而工具中存储了所有的实例数据和类数据的地址。

利用直接指针办法,最大的好处便是速率更快,它节省了一次指针定位的韶光开销。

就 HotSpot 虚拟机而言,它利用的是直接指针访问办法来定位工具,从其它虚拟机实现来看,利用句柄访问办法也是十分常见的。

04、小结

本文紧张从虚拟机层面,对工具的创建过程,访问办法进行了一次知识整合和总结,如果有描述不对的地方,欢迎大家留言指出,不胜感激。

标签:

相关文章