JVM 第四节 垃圾回收和锁
JVM 第四节
垃圾回收
大部分对象朝生夕死,但是存留下来的会生存很久.
有点像是心灵鸡汤.但确实是如此.
- 堆空间分为两大块
- 新生代
- Eden
- Survivor1
- Survivor2
- 老年代 Tenured
- 新生代
不同的代,垃圾对象的回收频率不一样.因此也会采用不同的算法.
标志-复制法
新生代:
Eden 会占较大的空间.Survivor1 ,2 会占相对较少的空间.
这里可以有两个指针 from , to .分别指向 Survivor1,2
当GC时,Eden 与 from 中存活的对象放入 to
这里有一个小优化,
GC后,可以将 from , to 的指针对调.这样就可以保证 to 指向的那块内存一直都是空的
新生代的对象转移到老年代
- 有一个 阈值 (默认15),GC下存活次数超过之后,可以将这个对象转移到老年代
- 某个 survivor 的占用的空间超过 50% ,存活次数较高的对象也会转移到老年代
优点
- 理想情况下,需要复制的对象很少,效率很高
- 不用对整个堆空间进行扫描
缺点
- 假如有老年代的对象引用了新生代的对象,则会进行GC Roots 进行全堆扫描
JVM 中利用卡表来解决这个问题
synchronized 及 JVM 锁
synchronized
- 可以直接声明一个 synchronized 代码块
- 标记静态方法 clazz
- 标记成员方法 this
当声明 synchornzied 代码块时,编译而成的字节码会包含 monitor_enter,monitor_exit
这两个指令都会消耗操作数栈上的一个引用类型元素(也就是synchronized 引用的那个)
重量级锁
重量级锁是JVM中最基础的锁实现. JVM 会在阻塞加锁失败的线程,并且在目标被释放的时候,
唤醒这些线程.
Java线程的阻塞及唤醒,都是依靠 OS 来操作完成的.这些操作设计系统调用,从用户态到内核态,
其开销非常大.
所以为了避免昂贵的线程阻塞,唤醒操作,JVM提供了 自旋状态
其实就是一种为了不阻塞,让程序一直运行下去.而去跑一些无用的指令.
希望在跑无用指令时,能过得到锁被释放出来.
类似,等红绿灯. Java线程的阻塞相当于熄火,自旋相当于怠速.
如果红灯时间长,熄火合适,但是时间较短的话,怠速合适.
但是,我们知道谁也无法预支未来,更别说机器了.
所以,JVM 只能自适应地采用自旋.具体算法,我也不清楚…
但是,自旋还带来了一个副作用.
不公平的锁机制: 可能处于阻塞状态的线程,并没有办法立刻获得锁,
反而是 自旋的线程 优先获得了锁.
想像一下,你开车等红灯熄火了,旁边的车怠速.
突然绿灯亮了,你还需要点火,挂档啥的. 旁边的车,一脚油门走了.气不气!
轻量级锁
JVM 中也存在,多个线程在不同时间请求同一把锁. 存在较少的竞争.
针对这种情况, JVM 提供了轻量级锁.避免重量级锁的阻塞及其唤醒.
对象头中的标记字段,最后两位被用来表示对象的锁状态.
- 00 轻量级锁
- 01 无锁或偏向锁
- 10 重量级锁
- 11 GC的标记
JVM 在加锁时,会判断是否已经是重量级锁,如果不是则会在当前栈帧中划出一片区域,
作为该锁的锁记录.并且将锁对象的标记字段赋值到该锁记录中.
JVM 会尝试使用 CAS (compare and swap)
操作来替换锁对象的标记字段.
通过对比期望值
- 是,则获得锁,继续执行.
- 不是
- 该线程获得了同一把锁,将锁记录清零.代表该锁被重复获取.
- 其它线程持有该锁. JVM 会将这个锁提高到重量级锁.
偏向锁
这是一个异常乐观的锁,乐观不足已形容.所以叫它偏向.
它是认为 始终只有一个线程请求某一把锁.