基础 | JVM - [Object & 锁升级]

§1 对象的生命周期

完整的对象生命周期分为 7 个阶段

  • 创建
  • 应用
  • 不可见:程序本身的作用域中已经看不见对应的对象
  • 不可达:不被任何强引用指向
  • 收集:被垃圾回收期回收
  • 终结:调用过 finalize() 方法,已经被回收
  • 空间重分配:原使用的内存空间被其他数据占据

需要注意,如果滥用 Finalizer.finalize() 可能导致对象 不可见,但可达,可能导致内存泄漏
因此,此对象依然占用空间,但 GC 时被认为不是垃圾,因此不会被回收

§2 对象创建过程

内存分配原则

  • 内存按 eden、survivor、old 优先级逐渐降低
  • 依次判断上述空间是否足够开辟空间给新对象
    • 若 eden 区足够,直接开辟
    • 否则依次向上判断,当判断出有空间可以开辟
      • 从上一级空间复制活跃对象进当前空间
      • 在上一级空间重复这个过程,直到 eden 区有空间
    • 若 old 区也没有足够空间,则触发 GC,然后回到上一步
    • 若 GC 后依然没有足够的空间,报 OOM

在这里插入图片描述

对象创建过程

§3 访问方式

对象的访问方式分为两种,示意图如下

  • 句柄池式:reference 的引用稳定,但引用对象时多了一次指针开销
  • 直接访问式:jvm 使用的访问方式,引用对象时可以节省一次指针开销,但 GC 后 reference 可能发生变化
    在这里插入图片描述

在这里插入图片描述

§4 对象的构成

总体来说,对象由下面三部分组成

  • 对象头(Header)
    • 对象标记(Mark Word)
      openjdk 里叫 markOop
    • 类型指针(Class Pointer)
      openjdk 里叫 klassOop
    • 长度(Length),数组才有
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

64 位系统中各部分内存占用

部位下级部位大小(byte)
Header12/16/20
Mark Word8
Class Pointer8/4(指针压缩)
Length4(数组才有)
Instance Data根据中字段的类型,比如两个 int 就需要 8
Padding补齐 8 字节的倍数
§4.1 Mark Word

概述与总览
Mark Word 是 java 对象对象头中的对象标记
一共占用 8 byte,共计 64 bit,各个 bit 位作用如下表
在这里插入图片描述

Mark Word 大致分为 3 种布局

  • 无锁/偏向锁
  • 轻量级锁/重量级锁
  • GC

锁标记位
无论上面那种布局,Mark Word 中都有固定的 2 bit 作为锁标志位

锁标记位的对应关系如下表
四种锁的详细介绍见 锁升级流程

锁标记位值对象锁状态说明
01无锁 / 偏向锁共用一个锁标记位值
在偏向锁标记位做出区分
00轻量级锁
10重量级锁
11GC无所谓锁不锁

无锁/偏向锁布局
偏向锁标记位
在锁标记位的值是 01 时,用来区分对象是无锁还是偏向锁状态
无锁和偏向锁在前

对象分代年龄

  • 只预留了 4 bit
  • 因此 JVM 最大分代年龄的最大值是 15

cms_free

  • 只占用 1 bit
  • cms 垃圾回收器使用

hashcode

  • markword 中的 hashcode 是通过 JDK 底层 C++ 的源码(在第一次调用 hashcode 时)计算并写入的
  • 如果重写了 hashcode() 方法,则 hashcode 不再写入 markword
    因为不保证此时 hashcode 不可变

epoch

  • 偏向状态标记,这是因为偏向锁不会主动释放锁,因此使用此信息标注 偏向线程是否正在处理同步代码块逻辑
  • epoch 与 klassOop.epoch 标志位不一致时,偏向锁为可重偏向状态
    • 可重偏向状态时,并发的线程可以尝试 CAS 抢占偏向锁
  • 偏向锁中线程 id 不是 null,且 epoch 与 klassOop.epoch 相等时,偏向锁为已偏向状态
    • 已偏向状态时,若并发线程 id == 偏向锁里记录的线程 id,则线程继续占有锁
    • 已偏向状态时,若并发线程 id != 偏向锁里记录的线程 id,则发生抢占,锁膨胀为轻量级锁

轻量级锁/重量级锁布局

  • 此布局下,除锁标记位的其他 bit 都用于存指向锁的指针
    • 无锁状态下的 markword 的各个部分都会被此指针替代
    • 直到锁释放,再将原始的 markword 写回原处
  • 这里的锁实际上是指对象内部锁、管程锁、monitor,由 ObjectMonitor 在 C++ 层面实现

ObjectMonitor
参考 基础 | 并发编程 - [Lock & synchronized]

GC布局

  • 此布局下,除锁标记位的其他 bit 都是空的
  • 都快被回收了,所以无所谓了
§4.2 Class Pointer
  • Class Pointer 是对象对象头中的类型指针
  • 用于指向元空间中的对应 class 的类元信息
  • 标准占用 8 byte,但通常 JVM 默认开启类型指针压缩 -XX:+UseCompressedClassPointers
  • 类型指针压缩后占用 4 byte
§4.3 Instance Data

Instance Data 用于存放对象的属性,包括父类中定义的属性

§4.4 Padding

Padding 是用于对齐对象存储空间的部分
这是因为JVM 要求对象的起始地址必须是 8 字节的整数倍

§5 锁升级

什么时候出现的
JDK 6

为什么会有锁升级

  • 因为 synchronized 在保证线程安全的前提下带来了性能下降的问题
  • synchronized 可以在极高的并发下保证线程安全,但通常并没有这么大的并发量,因此造成了性能的浪费
  • 因此在低并发的场景下提供更轻更快的加锁机制以提升性能
  • 同时,在并发数提高时,允许锁升级成完整版

为什么原始的 synchronized 性能低

  • java 的线程 最终是通过操作系统的原生线程实现的,默认运行于用户态
  • synchronized 是基于 监视器 monitor 实现的,monitor 又是基于操作系统的 系统互斥量Mutex Lock 实现的,需要操作系统介入
  • 这就导致了通过 监视器 monitor 阻塞唤醒线程时,实际上使用了操作系统的系统互斥量(Mutex Lock),即这是一次系统调用
  • 系统调用会导致线程在用户态和内核态之间切换,并且切换过程中需要切换线程上下文,消耗 CPU 时间
    切换线程的用户态和内核态,需要传递众多变量、参数、寄存器值等

线程的用户态与内核态

  • 线程最早不区分内核态和用户态,但这会使线程可以访问任意内存空间,很可能将系统玩坏
  • 因为出于安全和稳定性的考虑,将内存和指令划分了级别,即用户态和内核态
    实际上指令被划分为了 4 个级别,0-3,但 Linux 系统只使用最高和最低的两个
    • 用户态线程使用内存的用户空间,执行非特权指令
      特权指令指操作硬件或系统的指令
    • 内核态线程使用内存的内核空间,可以执行特权指令
  • 正常情况下,线程是工作在用户态的,下面操作需要完成用户态到内核态的切换
    总体上说,用户态和内核态之间的切换都是通过中断实现的,系统调用也是一种特殊的中断
    • 系统调用
      系统调用包括 进程控制、文件操作、硬件设备操作、硬件信息、通信相关
    • 中断
    • 异常
  • 用户态和内核态切换时需要完成一系列状态的保存
    • 用户态和内核态的切换是通过中断完成的,中断的首要任务就是保存现场
      包括 寄存器信息、栈顶地址、中断时的状态字等
    • 切换任务处理后,切换回原来线程状态时,又会把之前保存的现场还原回去


锁的 4 个级别横比

级别锁标志位markwork中其他信息线程数与常态并发数优点缺点适用场景
无锁01hashcode、分代年龄等-无线程安全问题
偏向锁01偏向的线程 id1+ / 0加解锁出现竞争时,增加锁撤销开销无并发的场景
轻量级锁00栈中 lock record 的指针2+ / 1低并发时不会阻塞长时间自旋时带来的 CPU 开销同步代码块逻辑很短,可以快速响应的场景
重量级锁10堆中 monitor 对象 指针2+ / 2+无长时间自旋时 CPU 开销线程阻塞,响应时间长同步代码块逻辑较长,前面的锁满足不了的场景

锁升级流程
总体上锁升级按下面趋势
无锁 → 偏向锁 → 轻量级锁 → 重量级锁

更多细节需要看下图
在这里插入图片描述

  • jdk 1.6 以后,默认开启偏向锁模式
    • 偏向锁模式下,会默认为对象开启偏向锁
    • 但对象未被 synchronized 作为锁时,markword 中没有记录线程,称为 匿名偏向锁
    • 偏向锁模式有一个 4s 的延时,此段时间中 new 的对象是 无锁状态,之后是匿名偏向锁状态
  • 一组比较容易进误区的状态转换
    • 无锁只能直接转到轻量级锁
    • 匿名偏向锁的对象在被 synchronized 作为锁时,可以转为偏向锁,进而可以转为轻量级锁、重量级锁
  • 锁其实是有降级的
    • 偏向锁、匿名偏向锁可以被重置为无锁
  • 重量级锁中依然会有cas,c++源码中会用到 Atomic.cmpxchg

无锁
没有被 synchronized 使用,不存在锁

偏向锁

  • 升级条件
    • 对象的 ObjectMonitor 开始被 synchronized 使用
    • 但因并发量较低,频繁被同一个线程争抢成功时
      线程是不止一条的,但通常并没有发生争抢,而是线程轮流获取锁
    • 从无锁升级为偏向锁
  • 偏向锁实际上约等于没有线程竞争,或竞争极低
    需要注意,偏向一个线程并频繁被同一个线程争抢成功 并不是只存在一个线程
    而是线程数较少,竞争不激烈,因此 JVM 倾向于让上一次抢到锁的线程再次抢到锁,以节省切换线程上下文的开销
  • 偏向锁获取
    • 线程第一次拥有偏向锁时,在 markword 中 记录偏向线程 id,并将 ==偏向锁标记置 1=
      不需要改锁标志位,因为无锁、偏向锁都是 01
    • 即使线程执行完同步块中的逻辑,偏向锁也不会被主动释放,除非遇到其他线程争抢并争抢成功
      即:偏向的线程相当于始终持有 偏向锁
    • 又有线程进入同步块时,直接在锁的 markword 中检查是否是自己记录的线程 id
    • 若与记录的一致,等同于再次竞争获取到锁,放行
    • 若不一致,通过 CAS 操作替换 markword 中记录的线程 id
      • 替换成功,即竞争成功,依然是偏向锁, markword 中记录新的线程 id
      • 替换失败,即竞争失败,可能需要升级为轻量级锁
  • 锁升级时机:线程与锁中 markword 记录的线程不是同一个,且尝试 CAS 替换失败后,可能升级
  • 偏向锁撤销
    • 偏向锁只有在其他线程与偏向线程发生争抢时才会撤销
      • 若偏向线程正在同步代码块中,偏向锁会被取消并进行锁升级
      • 若偏向现场退出同步代码块,则偏向锁中的线程会变更为竞争线程,依然保持偏向锁
    • 偏向锁的撤销需要等待 全局安全点
  • 相关配置
    • -XX:+UseBiasedLocking 开启偏向锁
    • -XX:+UseBiasedLockingStartupDelay=0 偏向锁立即生效,默认 4000 ms 延时
      按默认配置,JVM 启动后经过 4 秒后,偏向锁才生效

轻量级锁

  • 升级条件
    • 已经有其他线程频繁对偏向线程争抢
    • 并且争抢失败,并等到了全局安全点
    • 但因并发量较低,通常只有一个,且获取锁的冲突时间极短
    • 从偏向锁升级为轻量级锁
  • 轻量级锁的本质就是 自旋锁
    可以屏蔽系统调用导致的线程用户态、内核态切换
  • 与偏向锁的区别
    • 偏向锁约等于没有线程竞争,轻量级锁有
    • 偏向锁不会主动释放锁,轻量级锁每次退出同步代码块都会释放
  • 轻量级锁获取
    • JVM 在当前线程栈帧中创建 Displaced Mark Word,即锁记录(Lock Record),用于存储锁
    • 线程获取锁时,若发现是轻量级锁,会将自己的 markword 复制进 Displaced Mark Word
    • CAS 尝试将自己的 markword 替换为指向 Displaced Mark Word 的指针
    • CAS 成功,抢到轻量级锁
    • CAS 失败,没抢到轻量级锁,开始自旋
  • 轻量级锁释放
    • CAS 尝试将 Displaced Mark Word 中的内容赋值回锁对象的 markword
    • 如果没有出现过锁升级,此操作会成功
    • 否则,说明轻量级锁已经升级为了重量级锁,此时释放锁并唤醒阻塞线程
  • 自适应自旋锁(JDK 6 以上)
    • 自适应自旋锁的最大自旋次数不是固定不变的
      JDK 6 时,超过 10 次自旋或半数以上 CPU 核自旋会导致锁升级
    • 当前线程上次自旋成功了,则本次自旋最大次数会增加
      JVM 倾向于认为上次自旋成功的线程,本次也能成功
    • 当前线程上次自旋没有陈宫,则本次自旋最大次数会减少,避免 CPU 自旋消耗

重量级锁

  • 升级条件
    • 自适应自旋锁的自旋超过一定程度
      参考上文 自适应自旋锁
    • 从轻量级锁升级为重量级锁
  • 重量级锁获取
    • 编译时,编译器在同步块开始位置插入 monitor enter,退出时插入 monitor exit
    • 线程执行到 monitor enter 时,会尝试获取 ObjectMonitor 的所有权,如果成功就获取到了锁
    • 成功获取到锁后,会在 ObjectMonitor_owner 中存入当前线程的 id


锁升级过程中的 hashcode

  • 偏向锁和 hashcode
    • 偏向锁和 hashcode 是互斥的,但这里的 hashcode 专指一致性 hash
      一致性 hash 是指通过 System.identityHashCode() 或默认的 java.lang.Object.hashCode() 获取的 hashcode
      经过复写的 java.lang.Object.hashCode()
      • 不认为是一致性 hash
      • 不保证一次生成始终不变
      • 也不需要存在 markword 中
    • 计算过一致性 hash 的对象不可能进入偏向锁状态,因为一致性 hash 需要存入对象头的 markword
    • 偏向锁状态的对象,如果收到计算一致性 hash 的请求,会撤销偏向状态,并升级为 重量级锁
    • 升级后 markword 的存储位置同重量级锁
  • 轻量级锁和 hashcode
    • 在当前线程栈帧中创建 Displaced Mark Word,即锁记录(Lock Record)空间
    • 将锁对象的 markword 复制到此空间
    • 将锁对象的 markword 通过 CAS 指向此空间,若成功就是加锁成功,否则加锁失败自旋
    • 释放锁时通过 CAS 尝试将 Displaced Mark Word 中的内容赋值回锁对象的 markword
  • 重量级锁和 hashcode
    • 重量级锁中的 ObjectMonitor_header 字段可以保存 hashcode
    • 退出重量级锁时,会将 ObjectMonitor_header 的字段重新赋值给锁对象的 markword

锁消除

  • 锁消除是指 JVM 在运行时对代码中要求的锁进行消除的行为
  • 这是因为 JVM 判断,对应的代码段不可能存在 共享数据的的多线程竞争
  • 通常 JVM 是根据逃逸分析进行判断的
    • 逃逸分析是通过 指向对象的指针(即对象的引用)的范围,分析对象是否可能逃逸出方法进而被其他线程引用
    • 若对象满足逃逸分析,比如对象是局部变量,仅在方法内部生效,则
      • 被认为不会被其他线程访问,并消除相关的锁
      • 被认为没有必要在堆中创建,可以直接使用栈帧中额内存空间

锁粗化

  • 锁粗化是指 JVM 在运行时自动增加同步代码块涵盖的范围
  • 锁粗化长出现于
    • 在一套逻辑(比如一个方法)中,对同一个对象反复加解锁
    • 加解锁操作在循环体中完成

§4 JOL

依赖

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
    <scope>provided</scope>
</dependency>

查看当前虚拟机详情

VM.current().details();

在这里插入图片描述

查看当前虚拟机的对象对齐数

VM.current().objectAlignment();

在这里插入图片描述

分析对象的内存布局并打印

class A {
    private int i;
    private char b;
}

public class JolDemo {
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new A()).toPrintable());
    }
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值