【Java】JVM学习(四)

这篇具有很好参考价值的文章主要介绍了【Java】JVM学习(四)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

对象的分配

JVM中对象的创建过程

【Java】JVM学习(四)

对象的内存分配

虚拟机遇到一条new指令时,首先检查是否被类加载器加载,如果没有,那必须先执行相应的类加载过程。

类加载就是把class加载到JVM的运行时数据区的过程。

1)检查加载

首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用(符号引用 :符号引用以一组符号来描述所引用的目标),并且检查类是否已经被加载、解析和初始化过。

符号引用 :符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,JAVA在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址(实际地址),就用符号引用来代替,而在类的解析阶段(后续JVM类加载会具体讲到)就是为了把这个符号引用转化成为真正的地址的阶段。

一个java类(假设为People类)被编译成一个class文件时,如果People类引用了Tool类,但是在编译时People类并不知道引用类的实际内存地址,因此只能使用符号引用(org.simple.Tool)来代替。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址。

2)分配内存

接下来虚拟机将为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

指针碰撞
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。

【Java】JVM学习(四)
空闲列表
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。
【Java】JVM学习(四)
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

如果是Serial、ParNew等带有压缩的整理的垃圾回收器的话,系统采用的是指针碰撞,既简单又高效。

如果是使用CMS这种不带压缩(整理)的垃圾回收器的话,理论上只能采用较复杂的空闲列表。

并发安全
除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

CAS机制
解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;

【Java】JVM学习(四)

CAS(Compare And Swap)是一种并发编程中用于实现无锁算法的机制。在JVM中,CAS是一种原子操作,用于保证多个线程同时对某个共享变量进行读取、比较和更新时的一致性。

CAS操作包括三个步骤:比较当前值、如果相等则进行更新,否则重新尝试。这个过程是原子的,不会被其他线程的干扰打断。CAS操作通常使用在并发环境下对共享变量进行原子性的操作,比如对计数器、标志位等进行更新。

在JVM中,CAS操作借助于底层硬件的支持来实现。它使用了处理器提供的原子指令(例如compare-and-swap指令),能够在特定的CPU指令级别上实现原子性的读取、比较和更新操作。当多个线程同时进行CAS操作时,只有一个线程能够成功执行更新操作,其他线程需要重新尝试或采取其他策略。

CAS的优势在于它避免了使用锁的开销,提高了并发性能,并且没有死锁的风险。然而,CAS也存在一些问题。由于CAS操作是基于当前值进行比较的,因此在高并发情况下,如果多个线程同时进行CAS操作并竞争同一个共享变量,可能会导致大量的重试和自旋,降低效率。

为了解决CAS操作的ABA问题(即:在比较值的过程中,共享变量可能被其他线程修改为相同的值,从而导致误判),JVM提供了java.util.concurrent.atomic包下的原子类,例如AtomicInteger、AtomicLong等,它们提供了CAS操作的封装,能够更方便地进行原子性操作。

分配缓冲

另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块私有内存,也就是本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),JVM在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用。

TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。

TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB。

3)内存空间初始化

(注意不是构造方法)内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(如int值为0,boolean值为false等等)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4)设置

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息(Java classes在Java hotspot VM内部表示为类元数据)、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

5)对象初始化

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。所以,一般来说,执行new指令之后会接着把对象按照程序员的意愿进行初始化(构造方法),这样一个真正可用的对象才算完全产生出来。

对象的内存布局

【Java】JVM学习(四)

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

对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

如果对象是一个java数组,那么在对象头中还有一块用于记录数组长度的数据。

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对对象的大小必须是8字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。

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

使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改.

直接指针
如果使用直接指针访问, reference中存储的直接就是对象地址。

这两种对象访问方式各有优势,使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。

对Sun HotSpot而言,它是使用直接指针访问方式进行对象访问的。文章来源地址https://www.toymoban.com/news/detail-509088.html

到了这里,关于【Java】JVM学习(四)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • jvm对象创建和内存分配优化

    1、类加载检测 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 new指令对应语言层面讲是,new、对

    2024年02月13日
    浏览(56)
  • JVM—对象的创建流程与内存分配

    对象创建的流程图如下: 内存分配的方式有两种: 指针碰撞(Bump the Pointer) 空闲列表(Free List) 分配方式 说明 收集器 指针碰撞(Bump the Pointer) 内存地址是连续的(新生代) Serial和ParNew收集器 空闲列表(Free List) 内存地址不连续(老年代) CMS收集器和Mark-Sweep收集器

    2024年04月10日
    浏览(51)
  • JVM对象创建与内存分配机制深度剖析

    (1)类加载检查 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程; 所以所类加载是 懒加载 ; new指令对应到语

    2024年02月13日
    浏览(49)
  • JVM 创建对象时分配内存的几种方法、分配方法的选择

            假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那 个指针向空闲空间方向挪动一段与对象大小相等的距离。         如果Java堆中的内存并不是规

    2024年02月10日
    浏览(45)
  • Java虚拟机(JVM)垃圾收集器、新生代、老年代、永久代以及内存分配策略

    在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。而新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。 新生代中一般保存新出现的对象,所以每次

    2024年02月04日
    浏览(84)
  • 深入理解Java虚拟机jvm-对象的内存布局

    在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例 数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈 希码(HashCode)、GC分代年龄、锁状态标志、

    2024年02月09日
    浏览(56)
  • JVM源码剖析之Java对象创建过程

    关于 \\\"Java的对象创建\\\" 这个话题分布在各种论坛、各种帖子,文章的水平参差不齐。并且大部分仅仅是总结 \\\"面试宝典\\\" 的流程,小部分就是copy其他帖子,极少能看到拿源码作为论证。所以特意写下这篇文章。 版本信息如下: 首先把总结图放在这。接下来分析源码~  用一个

    2024年02月12日
    浏览(48)
  • JVM 给对象分配内存空间

    指针碰撞 空闲列表 TLAB 为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。 指针碰撞:(Bump The Pointer) 堆的内存是绝对规整的,内存主要分为两部分,所有使用过的内存被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点

    2024年02月11日
    浏览(40)
  • JVM对象在堆内存中是否如何分配?

    1:指针碰撞:内存规整的情况下 2:空闲列表: 内存不规整的情况下 选择那种分配方式 是有 java堆是否规整而决定的。而java堆是否规整是否对应的垃圾回收器是否带有空间压缩整理的能力决定的。 因此当使用Serial,ParNew等带有压缩整理过程的收集器时,系统采用的分配算法是

    2024年02月16日
    浏览(44)
  • 创造与布局:剖析 Java 对象创建过程以及内存布局

    目录 上下文提及到了类的加载过程,详细介绍了加载类的每个阶段:Loading、Linking、Initialize,在其中也说明了静态变量赋值顺序 先赋予默认值、在 Initialize 初始化阶段赋予初始值 从类加载到双亲委派:深入解析类加载机制与 ClassLoader 该篇文章会详细实例对象的创建过程、对

    2024年02月11日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包