Java对象创建和访问

程序浅谈 后端 一天前

Java对象创建过程

Java是一门面向对象的语言,在使用的过程中经常会创建各种类型的对象,而创建一个对象仅需要一个new关键字就可以,那么在虚拟机中对象创建又是怎么一个过程?

  • 虚拟机在遇到一个new指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应类的加载过程。在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后就可以完全确定,为对象分配内存空间过程等同于将一个确定大小的内存从Java堆中划分出来。假设Java堆中的内存是绝对规整的,已使用的内存放在一边,未使用放在另一边,中间放着一个指针作为分界点的指示器,那分配内存就是把这个指针向空闲空间那一边挪动一段与对象大小相同的距离。这种分配方式成为指针碰撞(Bump the Pointer)。如果Java堆中的内存并不是规整的,已使用的内存和空闲内存相互交错,那么虚拟机就必须维护一个列表,记录哪些内存是可用的,在给对象分配内存的时候从列表中找到一块足够大的内存分配给对象,并更新列表上的记录,这种分配方式称为空闲列表(Free List)。选择哪种算法由Java堆是否规整决定,而Java堆是否规整是由所采用的垃圾收集器是否带有压缩规整的功能决定。因此,在使用Serial、ParNew等带Compact过程的垃圾收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

  • 除了如何划分空间,还有一个问题就是对象创建在虚拟机中是非常频繁的行为,即使仅仅是修改一个指针指向的位置,在并发情况下不是线程安全的,可能出现正在给对象A分配内存,指正还没来得及修改,对象B又同时使用了原理的指针分配内存。解决这个问题有两种解决方案,一种是对分配内控空间的动作进行同步处理,虚拟机采用的是CAS分配失败后重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程的划分在不同的空间中进行,即为每个线程在Java堆中分配一块内存,成为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并重新分配时,才需要同步锁。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数设定。

  • 内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为0(不包括对象头),如果使用TLAB,这一过程可以提前到TLAB分配时进行。这一操作保证了对象的实例字段在Java的代码中不赋初始值就可以直接使用。接下来虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何能查到类的元信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头(Object Header)中。

  • 在上面的工作都完成之后,从虚拟机视角来看,一个新的对象已经产生,但从Java程序的角度看,对象的创建才刚刚开始,因为方法还未执行,所有的字段还为0。所以,一般来说(由字节码中是否跟随invokespecial指令决定),执行new执行之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算产生出来。

对象的访问

建立对象是为了访问对象,我们的Java程序需要通过栈上的reference数据来来操作对象的具体对象。由于Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个应用采用何种方式去定位、访问堆中的对象的具体位置,所以对象的访问方式取决于虚拟机的实现方式。目前主流的方式有使用句柄和直接指针两种:

  • 如果使用句柄访问的话,那么Java堆中会划分一块内存作为句柄池,reference中存储就是对象句柄的地址,而句柄中包含了对象实例数据与类型数据各自的地址信息,如图:
  • 如果使用直接时针访问的话,那么java堆对象的布局就要考虑如何放置类型数据相关信息,而reference中存储的就是对象的地址,如图:这两种对象访问方式各有优势,使用句柄访问最大的好处是reference中存储的是稳定句柄地址,在对象被移动时(辣鸡回收时会移动对象)只需要改动句柄中实例数据的指针,而reference不需要修改。而使用直接指针的方式最大的优势是访问速度更快,他节省了一次指针定位的开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。

转载来源:https://juejin.cn/post/6844903685491785735

Apipost 私有化火热进行中

评论