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 bit | 31 bit | 1 bit (空闲) | 4 bit | 1 bit (偏向位) | 2 bit (锁标志) | 存储内容描述 |
|---|---|---|---|---|---|---|---|
| 无锁 (Normal) | 不使用 | Identity HashCode | - | 分代年龄 | 0 | 01 | 对象的哈希码、GC年龄、状态位 |
| 偏向锁 (Biased) | 线程 ID (54 bit) | Epoch (2 bit) | - | 分代年龄 | 1 | 01 | 持锁线程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 头部或者尾部 然后唤醒