当前位置: 首页 > 后端技术 > Java

你有没有认真地理解过你创建的“Java对象”?

时间:2023-04-01 21:18:48 Java

?对象在JVM中是如何存储的对象头中有什么??作为Java开发者,我们的日常生活中可能没有对象,但是我们每天在工作中都会创建大量的Java对象。你有没有试过,你想了解你创造的这些“对象”吗?让我们从四个方面重新认识自己的“客体”。创建对象的6种方法。JVM中创建对象时发生了什么。JVM中对象的内存布局。对象的访问和定位。创建对象的最常见和最常规的方法也是最简单的方法。通过使用这个方法,我们可以调用任何我们想调用的构造函数(默认是使用无参构造函数)Personp=newPerson();使用Class类的newInstance(),只能调用空参数的构造函数,权限必须是public//获取类对象ClassaClass=Class.forName("priv.starfish.Person");Personp1=(人)aClass.newInstance();Constructor的newInstance(xxx),对构造函数没有要求.newInstance();clone()深拷贝,需要实现Cloneable接口并实现clone(),不调用任何构造函数Personp3=(Person)p.clone();反序列化通过序列化和反序列化技术从文件或网络中获取对象的二进制流。每当我们序列化和反序列化一个对象时,JVM都会为我们创建一个单独的对象。在反序列化中,JVM不使用任何构造函数来创建对象。(序列化的对象需要实现Serializable)//准备一个文件,用于存储对象的信息/序列化对象并写入磁盘oos.writeObject(p);//反序列化FileInputStreamfis=newFileInputStream(f);ObjectInputStreamois=newObjectInputStream(fis);//反序列化对象Personp4=(Person)ois.readObject();第三方库ObjeneslsJava已经支持通过Class.newInstance()动态实例化Java类,但这需要Java类有合适的构造函数。很多时候Java类不能这样创建,例如:构造函数需要参数,构造函数有副作用,构造函数抛出异常。Objenesis可以绕过上述限制。这里只针对普通Java对象讨论创建对象的步骤,不包括数组和Class对象(普通对象和数组对象的创建指令不同。创建类实例的指令:new,创建数组的指令:newarray,anewarray,multiawarray).newinstruction当虚拟机遇到一条新指令时,首先检查这条指令的参数是否可以在Metaspace的常量池中定位到某个类的符号引用,并检查该符号引用所代表的类是否已经被加载、解析并初始化(即判断分类器信息是否存在)。如果不是,则必须先以双亲委派方式执行相应的类加载过程。分配内存接下来,虚拟机将为新一代对象分配内存。一个对象需要的内存大小是在类加载完成后才完全确定的。如果实例成员变量是引用变量,则只分配引用变量空间,大小为4字节。有两种分配方式:“BumpthePointer”和“FreeList”,取决于使用的垃圾收集器是否具有compaction功能。如果内存是规则的,使用“指针碰撞”为对象分配内存。意思是所有已用内存在一侧,空闲内存在另一侧,中间放一个指针作为分界点的指示。分配内存只是将指针移动到空闲的一侧,距离等于对象的大小。如果垃圾收集器使用基于Serial或ParNew的压缩算法,则使用此方法。(一般使用带排序功能的垃圾收集器,使用指针碰撞。)Java指针碰撞。如果内存不规律,虚拟机需要维护一个列表。该列表将记录可用的内存。为对象分配内存时从链表中找到足够大的空间分配给对象实例,并更新链表的内容。这种分配方式就是“空闲列表”。在使用CMS等基于Mark-Sweep算法的收集器时,通常使用空闲链表。Javafreelist分配?我们都知道堆内存是线程共享的,所以在分配内存的时候会存在并发安全问题。JVM是怎么解决的呢??一般有两种解决方案:将分配内存空间的动作做同步处理,采用CAS机制,配合失败重试的方法保证更新操作的原子性每个线程在Java堆,然后直接在自己的“私有”内存分配中给对象分配内存,当这部分区域用完了,再分配新的“私有”内存。这种方案称为“TLAB”(线程本地分配缓冲区)。这部分Buffer是从堆中分出来的,但是是本地线程独享的。这里值得注意的是,我们说TLAB是线程独享的,但是在“分配”这个动作中是线程独享的,在读、垃圾回收等动作中是线程共享的。而且使用上没有区别。另外,TLAB只作用于新一代的EdenSpace。对象在创建的时候首先放在这个区域,但是在新生代无法分配内存的大对象会直接进入老年代。因此,在编写Java程序时,分配多个小对象通常比大对象更有效率。虚拟机是否使用TLAB是可选的,可以通过设置-XX:+/-UseTLAB参数来指定,在JDK8中默认开启。初始内存分配完成后,虚拟机需要将所有分配的内存空间初始化为零值(不包括对象头)。这一步确保对象的实例字段可以直接在Java代码中使用,而无需分配初始值。程序可以访问这些字段的数据类型对应的零值。比如byte、short、long转换为对象后的初始值为0,Boolean的初始值为false。对象的初始设置(设置对象的对象头)接下来,虚拟机需要对对象进行必要的设置,比如对象是哪个类实例,如何找到类的元数据信息,对象的hashcode,以及对象的GCGenerationalage等信息。此信息存储在对象的对象头中。根据虚拟机当前的运行状态,比如是否开启偏向锁等,对象头会有不同的设置方式。方法初始化以上工作完成后,从虚拟机的角度来看,已经生成了一个新的对象,但是从Java程序的角度来看,对象的创建才刚刚开始,\method尚未执行,所有字段仍为零。初始化成员变量,执行实例化代码块,调用类的构造函数,将对象在堆中的地址赋值给引用变量。因此,一般来说,执行完new指令后,按照程序员的意愿执行init方法初始化对象(应该是将构造函数中的参数赋值给对象的字段),这样一个真正可用的对象才是完全生成。出来。对象的内存布局在HotSpot虚拟机中,对象在内存中的布局可以分为三个区域:对象头(Header)、实例数据(InstanceData),以及它的填充(Padding)。对象内存布局对象头HotSpot虚拟机的对象头包含两部分信息。第一部分用于存储对象本身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。另一部分对象的类型指针,即对象指向其类元数据的指针,虚拟机通过这个指针来判断对象是哪个类的实例(并不是所有的虚拟机实现都必须保留对象数据上的类型指针,即也就是说,不需要通过对象本身来查找对象的元数据信息)。如果对象是Java数组,那么对象头中还必须有一段数据用来记录数组的长度。?元数据:描述数据的数据。关于数据和信息资源的描述性信息。在Java中,元数据主要表现为注解。?实例数据实例数据部分是对象实际存储的有效信息,也是在程序代码中定义的各种类型的字段内容,无论是继承自父类还是定义在子类中,都需要记录。这部分的存储顺序会受到虚拟机默认的分配策略参数和Java源码中字段定义顺序的影响(相同宽度的字段总是分配在一起)。规则:相同宽度的字段总是一起分配父类中定义的变量会出现在子类之前variabletofillalignmentpadding部分不一定存在,也没有特殊意义,只是起到占位符的作用。由于HotSpotVM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。对象头部分恰好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分不对齐时,需要通过对齐填充来完成。让我们通过一个简单的例子来加深我们的理解}}publicclassPerson{intid=1008;字符串名称;部门部门;{name="匿名用户";//name赋值是一个字符串常量}}publicclassDepartment{intid;Stringname;}Java中对象的内存布局4.对象的访问与定位我们创建对象的目的一定是为了使用它,JVM是如何通过栈帧中的对象引用来访问其内存中的对象实例的呢?由于引用类型在Java虚拟机规范中只规定了对一个对象的引用,并没有定义这个引用应该如何定位以及对象在Java堆中的具体位置。因此,不同虚拟机实现的对象访问方式会有所不同。主流的接入方式有两种:手柄接入。如果使用句柄访问方式,会在Java堆中分配一块内存作为句柄池。引用存储了对象的句柄地址,句柄中包含了对象实例数据和类型数据的具体地址信息。使用handle方法的最大好处是引用存储了一个稳定的句柄地址。当对象被移动时(垃圾回收时移动对象是很常见的行为),只会改变句柄中的实例数据指针,而引用本身不需要改变。修订。通过句柄访问Java对象的直接指针(Hotspot就是使用这种方式)。如果采用这种方式,Java堆对象的布局必须考虑如何放置访问类型数据的相关信息,对象地址直接存放在引用中。使用直接指针方式最大的好处是速度更快,省去了一次指针定位的时间开销。通过指针访问Java对象