Skip to content

JVM 笔记

Java 中的强引用、软引用、弱引用和虚引用分别是什么?

按照被 GC 回收的难易程度大致为 强引用 > 软引用 > 弱引用 > 虚引用

  • 强引用:普通赋值就是强引用:Object obj = new Object();这个永远不会被GC回收,即使OOM!!!也不会被回收
  • 软引用:通过 SoftReference 进行包装引用,内存足够的时候不回收,快OOM的时候才进行回收,适合做缓存
  • 弱引用:通过 WeakReference 进行包装引用,GC总是会回收这个,WeakHashMap 的键就是用这个包装的
  • 虚引用:通过 PhantomReference 进行包装引用,如果我们对着它使用 get,我们并不会拿到任何东西,这个东西的作用就是监控一个对象什么时候被回收

软引用在什么时候会被回收?是内存不足就立刻回收吗?

不是,内存充足的情况下并不会回收被 SoftReference 包装的实例,在内存吃紧,快要抛出 OOM 的时候,才会去回收被 SoftReference 包装实例,但也不是会被立刻回收,JVM实际上只是保证在抛出 OOM 的时候不会有被 SoftReference 包装的实例存于内存,至于什么时候回收,HotSpot 有个 -XX:SoftRefLRUPolicyMSPerMB 参数,默认 1000,也就是,每有1MB空闲内存,软引用可以多活一秒,相应的,内存越少,软引用在内存中存活的时间越短,所以应该是内存不足,对 SoftReference 包装的实例的回收操作变的越积极

为什么 PhantomReference 的 get() 方法永远返回 null?

因为虚引用的目的就是监控对象被回收的时机,这个对象就是用来被回收的,如果可以再次获取这个对象,就意味这个对象可以被复活,这样就违背了虚引用的目的

对象头是啥?

Java 对象在堆内存中存放时,分为三个部分:对象头,具体数据,对齐填充

对象头主要有两部分组成

  • Mark Word:标识字段:64字节的空间,存放对象运行时的信息,里面的信息会随着对象锁的状态不同而改变
  • Klass Word:类型指针:指向自己类型的指针,标识这个对象是属于什么类的,开启指针压缩之后会从 8 字节,压缩到 4 字节。

如果这个对象是数组的话,会在后面加一个数组长度的数据

Mark Word:

作为对象头最重要的组成部分,他会根据锁的等级不同保存不同的信息:

锁状态25 bit31 bit1 bit (空闲)4 bit1 bit (偏向位)2 bit (锁标志)存储内容描述
无锁 (Normal)不使用Identity HashCode-分代年龄001对象的哈希码、GC年龄、状态位
偏向锁 (Biased)线程 ID (54 bit)Epoch (2 bit)-分代年龄101持锁线程ID、偏向时间戳、状态位
轻量级锁 (Lightweight)指向栈中锁记录 (Lock Record) 的指针 (62 bit)-00指向当前线程栈帧中 Lock Record 地址
重量级锁 (Heavyweight)指向堆外 Monitor 对象的指针 (62 bit)-10指向与之关联的 ObjectMonitor 地址
GC 标记 (Marked)不使用 (此时 Mark Word 内容无意义)-11仅用于 GC 时的标记(如 CMS)

哈哈,md我不知道怎么合并表格,凑活看吧

反正差不多就是这样,他会根据锁的不同变更存储的数据,,这样可以节省内存

Epoch 是什么?

Epoch 是 Mark Word 在偏向锁状态用于记录版本号的区域,用于优化批量偏向锁撤销的情况。他还存在于类元数据中,分别记录该对象的版本号和该类的版本号,两个版本号的初始数值都是 0

如果发生重大竞争(默认情况下是每个类频繁发生二十次对象撤销的时候),JVM就会更新版本号。首先,更新类的版本号,将其加一,然后被使用的对象版本号也加一。如果当前对象没有被使用,则其不变。当然上述所有的对象都是处于偏向锁加锁中的。

如果一个新线程去获得这个对象锁,而且线程 ID 检查不通过,就会去检查该类的版本号和该对象的版本号是否对应,如果对应就说明该对象的偏向锁有效,需要触发撤销流程,反之说明偏向锁过期,不触发撤销流程,直接 CAS 自己的线程ID

说说轻量锁的 Lock Record?

当解析到 _monitorenter 字节码的时候会在当前线程的栈帧中创建这个 Lock Record。然后将这个引用 CAS 到 Mark Word 的地方,CAS 成功就说明设置成功,这时候就转型为轻量级锁,如果失败就说明发生竞争了,这个时候就膨胀为重量级锁

针对重入操作,Lock Record 会在重入的时候继续被创建,但是 Displaced 区域为 null,表示这是一个重入操作,解锁的时候发现这个,直接给弹出就行

数据结构:

  • Displaced Mark Word:存储着无锁状态下的 Mark Word,用于锁撤销时的数据恢复
  • Object Reference:存储锁对象的对象头的指针

说说 Monitor ?

在 Java 中,作为 synchronized 的底层实现,用于保证多线程运行时的线程安全,每一个 Java 对象都有一个 Monitor。

其实这玩意在 HotSpot 中是 C++ 实现的,叫做 ObjectMonitor,里面东西还挺多的。

大概的数据结构:

  • _header:备份 Mark Word 的数据
  • _owner:存储现在持有锁的线程
  • _object:存储当前的锁对象
  • _recursions:记录同一个线程重入的次数,用于实现重入机制
  • _cxq:用于存放刚到但是没有抢到锁的线程
  • _EntryList:存储没有抢到锁的的 BLOCKED 线程
  • _WaitSet:存储正在等待的 WAITING 线程

没错,他维护了两个抢不到锁而陷入等待的线程链表,注释上说同一个线程要么在 _cxq 上面,要么在 _EntryList 上

解锁操作:

当持锁线程准备释放锁时,会根据策略的不同选择不同的唤醒等待线程的策略:

  • 如果 recursions 不为0,说明当前是重入状态,_recursions--,然后直接返回
  • 如果 QMode == 2 并且 cxq 队列不为空,则直接唤醒 cxq 队列头部的线程,绕过 entityList 然后返回
  • 如果 QMode == 3 并且 cxq 队列不为空,则将 cxq 队列的线程迁移到 _EntryList 的头部
  • 如果 QMode == 4 并且 cxq 队列不为空,则将 cxq 队列的线程迁移到 _EntryList 的尾部
  • 最后如果没有返回的话,唤醒 _EntryList 头部的线程,然后返回

维护两个队列的目的就是提升性能,_cxq 的 CAS 压力很大,分离解耦可以减少 CAS 的压力

wait 操作:

这个没啥说的,就是将自己加入到 _WaitSet 中,然后释放锁

notify 操作:

从 _WaitSet 头部拿取线程,根据策略不同放到 cxq 或者 entityList 头部或者尾部 然后唤醒

Contact me: 1943284256@qq.com