HotSpot虚拟对象(对象创建 内存分配 栈上分配 TLAB 对象内存布局和对象访问等 )

对象的创建

整个对象的创建过程如下所示:

在这里插入图片描述

  1. 首先进行类加载的检查:虚拟机遇到new指令的时候,首先去检查该指令的参数是否能够在常量池中定位到这个类的符号引用,并且检查该符号引用代表的类是否已经被加载过、解析和初始化过。若没有,必须先执行相应的类加载的过程。
  2. 分配内存:类加载检查通过之后,虚拟机为对象分配内存。(内存大小在类加载完后就能确定)。分配的方式有“指针碰撞”和“空闲列表”两种,若java堆是规整的,采用“指针碰撞”;反之采用空闲列表。(两种内存分配的方式见下文“内存的分配”)
  3. 初始化零值:内存分配完之后,虚拟机将分配到的内存空间初始化为零值。保证了对象的实例字段在java代码中可以不赋初值就直接使用。
  4. 设置对象头:初始化零值之后,虚拟机要对对象进行必要的设置:对象头的设置。(具体包括的内容见下文“对象的内存布局”)
  5. 执行init方法:最后,虚拟机的视角来看,新的对象已经产生了。但是从java程序的视角来看,init方法还没有执行,所有字段还都是零。so,一般来说,执行new指令之后就会接着执行init方法,把对象按照程序员的意愿进行初始化。真正可用的对象就完全产生了。

总结一下,遇到new指令之后先进行类加载的检查,检查就是检查常量池中是否有该new类的符号引用,然后判断该符号引用是否已经被加载、解析、初始化过;
然后给该类的对象分配内存;然后赋值为0;然后设置对象头;最后执行init方法。

内存的分配

分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
GC器的算法是“标记清除”(不规整、有内存碎片)还是“”标记整理(规整),复制算法也是规整的。
在这里插入图片描述
在这里插入图片描述

内存分配时候出现的并发问题

在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  1. CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是 设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  2. TLAB: 为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。

关于TLAB

TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。相当于线程的私有对象。

  1. 堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的
  2. Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),
    其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,
    在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配
  3. TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效

如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。

TLAB的本质其实是三个指针管理的区域**:start,top 和 end,**每个线程都会从Eden分配一块空间,例如说100KB,作为自己的TLAB,其中 start 和 end 是占位用的,标识出 eden 里被这个 TLAB 所管理的区域,卡住eden里的一块空间不让其它线程来这里分配。

TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。从这一点看,它被翻译为 线程私有分配区 更为合理一点

当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。

关于栈上分配

**如果确定一个对象的作用域不会逃逸出方法之外,那可以将这个对象分配在栈上,**这样,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,无须通过垃圾收集器回收,可以减小垃圾收集器的负载。

JVM允许将线程私有的对象打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统性能。

栈上分配的技术基础:

一是逃逸分析:逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。

二是标量替换:允许将对象打散分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。

只能在server模式下才能启用逃逸分析,参数-XX:DoEscapeAnalysis启用逃逸分析,参数-XX:+EliminateAllocations开启标量替换(默认打开)。Java SE 6u23版本之后,HotSpot中默认就开启了逃逸分析,可以通过选项-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果。

对象的内存布局

对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充。

首先,对象头,对象头主要包括两种信息:

  1. 用于存储对象自身的自身运行时数据。哈希码、GC分代年龄、锁状态标志等;
  2. 类型指针:对象指向他的类元数据的指针、该指针可以让虚拟机判断该对象是哪一个类的实例。

然后 实例数据。对象真正存储的有效信息。也是程序中所定义的各种类型的字段内容。

最后 对齐填充主要是占位,可以没有。因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍、8字节or16字节),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问

建立对象就是为了使用对象,我们的Java程序通过栈上的 reference (引用)数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:

  1. 句柄: 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

在这里插入图片描述

  1. 直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。

在这里插入图片描述

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

猜你喜欢

转载自blog.csdn.net/mulinsen77/article/details/89436809