JAVA基础篇--JVM--3对象结构

前言:
Jvm在加载类时会产生该类的类对象并放入到堆内存中,引用放入方法区中;在程序运行的过程中也会使用到各个类的实例对象,并将改实例对象放到堆内存中;那么放入到堆内存中的对象结构又是什么样的?它怎么设计才能为后面使用后可以方便程序的回收;

对象结构示意图:
在这里插入图片描述

1 Mark word:

  • 存入对象的哈希码,分代年龄(gc 时对象的年龄,每次gc后如果存活都会加1);锁状态标志(对象的锁标志,当并发的情况下,可以通过改状态标志判断象是否有没被线程占用);

  • mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位;

  • 例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit:对象是否启用偏向锁标记;

  • 32位 对象头结构:

  • 在这里插入图片描述
    1.1 hashCode(identity_hashcode):
    25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中;
    1.2 分代年龄(age):
    4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因(小于 0.等于 0 的话,就直接入老年代;等于 16 的话,就是从不进入老年代);
    1.3 是否偏向锁和锁标志位:
    在这里插入图片描述
    是否偏向锁(biased_lock):对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁;
    对象的四个状态(无锁,偏向锁,轻量级锁,重量级锁)的演化过程:

  • 该开始对象创立没被当作同步锁时是一个普通对象,markwork字宽存对象的hashCode,分代年龄,锁标志位是01,是否偏向锁那一位是0。

  • 当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。

  • 当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。

  • 当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁(偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态)。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行下一步骤。偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态;
    jdk15会去掉偏向锁;

  • 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行下一步骤。

  • 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,**自旋次数由JVM决定。**如果抢锁成功则执行同步锁代码,如果失败则继续执行下一步骤。

  • 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

  • 进入重量级锁时,是依靠monitor类来实现的。markWork存储指向了monitor监视类对象的指针,即上图重量级锁指针,由他来控制锁

  • 后期补充:在标识位为11时表示这个对象被垃圾回收线程GC持有;

  • 其他名词释义:
    线程id(thread):持有偏向锁的线程ID;
    epoch:偏向时间戳。
    指向栈中锁记录的指针(ptr_to_lock_record):指向栈中锁记录的指针。
    指向互斥量(重量级锁)的指针:ptr_to_heavyweight_monitor:指向管程Monitor的指针。

补充:Java 锁降级:
重量级锁降级发生于STW阶段,降级对象为仅仅能被VMThread访问而没有其他JavaThread访问的对象;
参考:https://blog.csdn.net/wekajava/article/details/120306478

2 Class pointer:
堆中 的对象,用来标识方法区中的哪个类数据(堆指向方法区),32位4字节,64位开启指针压缩或最大堆内存<32g时 4字节,否则8字节;

3 数组长度:
当对象为数组类型时,单独开辟空间来存储数组对象的数组长度;
思考:对象头中为什么要单独存储数组的长度;

4 实例数据:
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的;
类型和占用字节的关系表:
在这里插入图片描述
引用类型在32位系统上每个占用4Byte, 在64位系统上每个占用8Byte,开启(默认)指针压缩占用4Byte;

5 对齐填充:
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全;
对齐填充的含义:适当冗余空间,可以直接读取8byte(64位系统)/4byte(32位系统)的整数倍空间,读取后不需要对数据在进行切割,以空间换取时间;

补充:
(1)字节和位的关系:
bit: Binary digit(二进制数位)的缩写,意为“位”或“比特”,是计算机运算的基础;
byte: 意为字节"是计算机文件大小的基本计算单位; 关系: 1Byte=8bit (简写: 1B=8b)
注意bit代表二进制数位,取值范围位: 0或1.
在计算机科学中,bit是表示信息的最小单位,叫做二进制位;一般用0和1表示。
Byte叫做字节,由8个位(8bit)组成一个字节(1Byte),用 于表示计算机中的一个字符。bit与Byte之间可以进行换算,其换算关系为:1Byte=8bit(或简写为:1B=8b);
在实际应用中一般用简称, 即1bit简写为1b(注意是小写英文字母b),1Byte简写为1B(注意是大写英文字母B)。
目前bit和byte的比较
bit: 计算机中的最小存储单元 存储内容总是0或1 所有二进制状态的实体都可以使用1bit表示 8bits组成1byte 不能够单独寻址
byte: 1byte包含8bits 可以存储所有ASCII所有字符(这是它包含8bits的初衷) 十进制整数范围[-128,127]或[0, 255] 最小的可寻址存储单元;
(2)压缩指针:
什么是指针:
Java中并没有显示的使用指针,而且也不允许编程的过程中使用指针,但实际上,一个对象的访问就是通过指针来实现的,一个对象会从实际的存储空间的某个位置开始占据一定的存储体。该对象的指针就是一个保存了对象的存储地址的变量,并且这个存储地址就是对象在存储空间中的起始地址。在许多高级语言中指针是一种数据类型,在Java中是使用对象的引用来替代的;
指针为什么要压缩:
在堆中,32位的对象引用(指针)占4个字节,而64位的对象引用占8个字节。也就是说,64位的对象引用大小是32位的2倍。64位JVM在支持更大堆的同
时,由于对象引用变大却带来了性能问题:

增加了GC开销:64位对象引用需要占用更多的堆空间,留给其他数据的空间将会减少,从而加快了GC的发生,更频繁的进行GC。
降低CPU缓存命中率:64位对象引用增大了,CPU能缓存的oop将会更少,从而降低了CPU缓存的效率。
为了能够保持32位的性能,oop必须保留32位。那么,如何用32位oop来引用更大的堆内存呢?答案是——压缩指针(CompressedOops);
所以压缩指针之所以能改善性能,是因为它通过对齐(Alignment),还有偏移量(Offset)将64位指针压缩成32位。换言之,性能提高是因为使用了更小更节省空间的压缩指针而不是完整长度的64位指针,CPU缓存使用率得到改善,应用程序也能执行得更快。
既然压缩指针这么好,那么是不是jvm 都会用到压缩指针呢:
压缩指针的条件:
(1)首先指针压缩的前提是64位系统,32位直接不进行压缩;
(2)只用当堆内存大小在4G以上32G以下;
(3)Jvm开启指针压缩参数:
-XX:+UseCompressedOops (jvm默认启用)
注意1:XX:+UseCompressedClassPointers 开启压缩对象头里的类型指针Klass Pointer(jvm默认启用)

注意2:当系统为64位,但是堆内存大小小于4G或者大于32G 都不会进行指针压缩,这个时候直接使用低32,高32位直接不用;

注意3:32位内最多可以表示4GB,64位地址分为堆的基地址+偏移量,当堆内存<32GB时候,在压缩过程中,把偏移量/8后保存到32位地址。在解压再把32位地址放大8倍,所以启用CompressedOops的条件是堆内存要在4GB*8=32GB以内;

注意4:设置对齐填充的大小,打破32G堆内存限制:
配置最大堆内存超过 32 GB(当 JVM 是 8 字节对齐),那么压缩指针会失效。 但是,这个 32 GB 是和字节对齐大小相关的,也就是-XX:ObjectAlignmentInBytes配置的大小(默认为8字节,也就是 Java 默认是 8 字节对齐)。-XX:ObjectAlignmentInBytes可以设置为 8 的整数倍,最大 128。也就是如果配置-XX:ObjectAlignmentInBytes为 16,那么配置最大堆内存超过 64 GB 压缩指针才会失效;

注意5:被压缩的指针和不被压缩的指针:
哪些信息会被压缩?
1.对象的全局静态变量(即类属性)
2.对象头信息:64位平台下,原生对象头大小为16字节,压缩后为12字节
3.对象的引用类型:64位平台下,引用类型本身大小为8字节,压缩后为4字节
4.对象数组类型:64位平台下,数组类型本身大小为24字节,压缩后16字节
哪些信息不会被压缩?
1.指向非Heap的对象指针
2.局部变量、传参、返回值、NULL指针

(3)HotSpot对象模型:
HotSpot中采用了OOP-Klass模型,它是描述Java对象实例的模型,它分为两部分:

  • 类被加载到内存时,就被封装成了klass,klass包含类的元数据信息,像类的方法、常量池这些信息都是存在klass里的,你可以认为它是java里面的java.lang.Class对象,记录了类的全部信息;
  • OOP(Ordinary Object Pointer)指的是普通对象指针,它包含MarkWord 和元数据指针,MarkWord用来存储当前指针指向的对象运行时的一些状态数据;元数据指针则指向klass,用来告诉你当前指针指向的对象是什么类型,也就是使用哪个类来创建出来的;

那么为何要设计这样一个一分为二的对象模型呢?这是因为HotSopt JVM的设计者不想让每个对象中都含有一个vtable(虚函数表),所以就把对象模型拆成klass和oop,其中oop中不含有任何虚函数,而klass就含有虚函数表(类中可能存在重写父类的方法),可以进行method dispatch。
虚函数:
在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的[成员函数],用法格式为:virtual 函数返回类型 函数名(参数表) {[函数体]};实现[多态性],通过指向派生类的基类[指针]或引用,访问派生类中同名覆盖成员函数。
从上面解释上我们抓住几个关于虚函数的关键字 基类、派生类、同名覆盖(重写),因此我们可以理解为虚函数其实就是描述我们子类重写的父类方法。
在虚函数声明定义这块,C++可以通过virtual关键字来进行直接声明,而在Java中,并没有提供我们关键字来声明虚函数,但是我们通过虚函数的定义,我们可以理解为被override的方法都是virtual的;

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值